我们知道,结构体类型是可以很轻松的复制的,比如说:
struct St {
int m1;
double m2;
};
void demo() {
St st1;
St st2 = st1; // OK
St st3;
st1 = st3; // OK
}
但数组却并不可以,比如:
int arr1[5];
int arr2[5] = arr1; // ERR
明明这里 arr2 和 arr1 同为 int [5] 类型,但是并不支持复制。照理说,数组应当比结构体更加适合复制场景,因为需求是很明确的,就是元素按位复制。
-
数组类型传参
由于数组不可以复制,导致了数组同样不支持传参,因此我们只能采用 “首地址 + 长度” 的方式来传递数组:
void f1(int *arr, size_t size) {}
void demo() {
int arr[5];
f1(arr, 5);
}
而为了方便程序员进行这种方式的传参,C 又做了额外的 2 件事:
-
提供一种隐式类型转换,支持将数组类型转换为首元素指针类型(比如说这里 arr 是 int [5] 类型,传参时自动转换为 int * 类型)
-
函数参数的语法糖,如果在函数参数写数组类型,那么会自动转换成元素指针类型,比如说下面这几种写法都完全等价:
void f(int *arr);
void f(int arr[]);
void f(int arr[5]);
void f(int arr[100]);
所以这里非常容易误导人的就在这个语法糖中,无论中括号里写多少,或者不写,这个值都是会被忽略的,要想知道数组的边界,你就必须要通过额外的参数来传递。
但通过参数传递这是一种软约束,你无法保证调用者传的就是数组元素个数,这里的危害详见后面 “指针偏移” 的章节。
-
分析和思考
之所以 C 的数组会出现这种奇怪现象,我猜测,作者考虑的是数组的实际使用场景,是经常会进行切段截取的,也就是说,一个数组类型并不总是完全整体使用,我们可能更多时候用的是其中的一段。举个简单的例子,如果数组是整体复制、传递的话,做数组排序递归的时候会不会很尴尬?首先,排序函数的参数难以书写,因为要指定数组个数,我们总不能针对于 1,2,3,4,5,6,... 元素个数的数组都分别写一个排序函数吧?其次,如果取子数组就会复制出一个新数组的话,也就不能对原数组进行排序了。
所以综合考虑,干脆这里就不支持复制,强迫程序员使用指针 + 长度这种方式来操作数组,反而更加符合数组的实际使用场景。
当然了,在 C++ 中有了引用语法,我们还是可以把数组类型进行传递的,比
如:
void f1(int (&arr)[5]); // 必须传int[5]类型
void demo() {
int arr1[5];
int arr2[8];
f1(arr1); // OK
f1(arr2); // ERR
}
但绝大多数的场景似乎都不会这样去用。一些新兴语言(比如说 Go)就注意到了这一点,因此将其进行了区分。在 Go 语言中,区分了 “数组” 和 “切片” 的概念,数组就是长度固定的,整体来传递;而切片则类似于首地址 + 长度的方式传递(只不过没有单独用参数,而是用 len 函数来获取)
func f1(arr [5]int) {
}
func f2(arr []int) {
}
上面例子里,f1 就必须传递长度是 5 的数组类型,而 f2 则可以传递任意长度的切片类型。
而 C++ 其实也注意到了这一点,但由于兼容问题,它只能通过 STL 提供容器的方式来解决,std::array 就是定长数组,而 std::vector 就是变长数组,跟上述 Go 语言中的数组和切片的概念是基本类似的。这也是 C++ 中更加推荐使用 vector 而不是 C 风格数组的原因。