The difference is that the third form is more restrictive, while the first and second forms could benefit from a null check.
The first (and second) form can accept an argument that is not an array, as long as the argument converts to an int pointer.
void changeArray(int array[]) {
array[0] = 1111;
}
int main() {
changeArray(nullptr); // Causes a segmentation fault, but does compile.
}
The third form cannot.
void changeArray(int (&array)[]) {
array[0] = 1111;
}
int main() {
// changeArray(nullptr);
// error: invalid initialization of non-const reference of type 'int (&)[]'
// from an rvalue of type 'std::nullptr_t'
}
An argument that is an array (of int) would be accepted by any of these functions.
I understood that 1 and 2 are practically the same, passing a pointer to the first element.
Not just practically the same. They are the same. Presumably, that is why you named the second function changeArrayByPointer instead of changeArray, since otherwise you'd get a function redefinition error. (If you want to test this out, try putting the declaration void changeArray(int array[]); before your main function and the definition void changeArray(int *array) { array[0] = 1111; } after. The definition links to the declaration, despite looking different.) See also Difference between passing array, fixed-sized array and base address of array as a function parameter.
In contrast, the first and third forms are distinct signatures, so they can overload the name changeArray. (However, if both overloads are in scope, trying to call one with an array argument is ambiguous.)
int (&array)[]is only valid from c++20. So the question is basically, why c++20 allowed this usage.int (&array)[]andint& arrayare not same in c++. See demo.