数组和指针在作为实参传入T[] 或T*的形参时没有区别
void f(int pi[]) { cout << sizeof(pi) << endl; }
int a[5] = { 1,2,3,4,5 }; f(a);
上述代码输出的是4(32位系统)或8(64位系统),总之不是sizeof(int) * 5(数组大小)。
为什么明明形参是数组形式的int [],实际上和指针形式的int *无异呢?关键原因就在于,数组是不能作为左值的。
函数传参并不是相当于赋值而是相当于初始化。于是,原因在于,数组不能直接用来初始化另一个数组,当然,也不能作为左值。
数组只能用初始化列表来初始化,字符数组可以例外地用字符串字面值初始化。
也就是说,你不能定义两个数组,比如int a[10], b[10];然后用a = b; 来给a赋值。
而函数的实参传给形参是要做1次赋值的,虽然很多书上区分了所谓的传值和传地址,但实际上两者是一样的,传地址不过是在指针类型之间进行赋值,见下述代码
void f1(int i) { i = 100; } void f2(int* pi) { *pi = 100; } int main() { int i = 0, j = 0, k = 0; f1(i); // 类似于 int i0 = i; i0 = 100; f2(&j); // 类似于 int* p = &j; int* p0 = p; *p0 = 100; return 0; }
但是,在C++里传递数组也是可行的,见下面代码
template <int N> void f(int (*pi)[N]) { cout << sizeof(*pi) << endl; } int main() { int a[3] = { 1,2,3 }; f(&a); return 0; }
输出结果是12,即sizeof(int)*3。
像这样蹩脚的传递数组指针,当然,更简单的方式是使用引用,把f()的参数*pi改成&pi,然后sizeof(*pi)改成sizeof(pi);main()里的f(&a)可以直接用f(a),两者本质上是一样的,只不过简化了代码。虽然需要强调的是引用和指针(以及java等语言中的引用)也有一些差异,这里不详细讨论。
这样的做法本质是像下面这样。
int a[3] = { 1,2,3 }; int (*p)[3] = a; // 形参传递给实参 cout << sizeof(p) << endl; // 函数体内的代码
OK,这里就可以正视数组和指针的区别了,对于数组int a[N];
a的类型是int [N],不能作为左值(也就是不能直接给a赋值),那么&a取得的则是指向int [N]的指针,表示为int (*)[N],大小为N*sizeof(int)。
其实指针本质上就是地址,之所以有各种各样的指针类型,是为了让编译器了解,你如果想用*p来取得指针指向的对象,到底该读取多大内存?
char s[5] = "1234"; char* pch = &s[0]; cout << *pch << endl; // 1 short* psh = &s[0]; cout << *psh << endl; // 12849 int* pi = (int*)&s[0]; cout << *pi << endl; // 875770417
如果把s[3]和s[4]都置为' '(对应ASCII码为0),那么*pi和*psh结果一样,都是12849。注意,这里是因为我的机器是小端的,数据的低位保存在内存的低地址中,我让高位变成0了,自然就和没有高位没区别了。
0x0000abcd int* 或char (*)[4]
0x abcd short*或char (*)[2]
0x cd char*
打个比方,内存中布局就类似上面那样(abcd是我随便定的值),3个指针都指向同样的地址,但是类型不同导致编译器了解你在解引用(*p)时,到底想从内存中读取多少,到底是从当前地址开始1位,2位还是4位?
再来看看在STL里的应用,STL的一个重要概念是迭代器,而指针也是一种迭代器(随机访问迭代器),在STL函数库(<algorithm>)中,往往接收的类型是用2个迭代器表示的数据范围(比如数组int a[3]; 用a和a+3就能表示首尾),而STL提供了传递函数的方法(可以是函数对象、函数指针或lambda表达式),这些函数接收的参数却不是迭代器,而是迭代器解引用后的对象。
因为我们实际上是要对容器中存储的数据进行操作,而不是去对数据存放的位置进行操作,取得存放位置的目的只是为了取得数据,这些工作在STL函数内部就完成了,调用者只需要告诉STL函数该如何处理数据即可(不需告诉STL函数怎么取得地址)。比如下面给出的for_each示例代码
template<class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function fn) { while (first!=last) { fn (*first); ++first; } return fn; // or, since C++11: return move(fn); }
对于数组int a[N];只需要写std::for_each(a, a + N, [](int& i) { i = rand(); });就可以批量生成随机数了(当然,实现这个功能有个更好的函数是generate)
不过对于二维数组来说,一切都要更为复杂了,虽然很少用到,但是在处理二维字符数组表示的C风格字符串列表时,还是需要知道这点。
char str[M][N];
其中M是字符串数量上限,N是字符串长度上限,假如要批量打印str表示的字符串列表,代码如下
std::for_each(&str[0], &str[M], [](char (&s)[N]) { printf("%s ", s); }
还是按照上面的分析方法,str类型是char [M][N],那么str[0]类型是char [N],&str[0]类型是char (*)[N],是指向N个字符的指针类型,也就是说对这样类型的指针p,每次执行++p时会移动N个字节(sizeof(char)),str[0][0](即*str[0])移动N个字节就到了str[0][N],也就是str[M][0](即*str[1])。
那么迭代器的类型是char (*)[N],那么解引用的类型就是char (&)[N],即引用一个长度为N的字符数组。
再回顾之前说的,数组在作为实参传递给形参T[]时,实际上传递的是数组的首地址,也就是说,printf接收的实际上是&s[0],即char*类型,因此可以用%s输出。
然而实际上还有个陷阱,比如我要对这些字符串进行排序。
std::sort(&str[0], &str[M], [](char (&s1)[N], char (&s2)[N]) { return strcmp(s1, s2) < 0; }
比如这里传入的迭代器是指针类型char (*)[N],而自定义排序函数接收的也是解引用的类型char (&)[N],编译时却报了几十行错误。我昨天在测试ls -l的排序方法时在这里纠结了很久。
原因在于std::sort的内部实现,对于这种比较排序,内部实现经常有交换操作。
比如模板参数Iter表示迭代器类型,在函数内部可能就会有这样的代码
typedef std::iterator_traits<Iter>::value_type T; // 迭代器指向对象的类型 Iter it1, it2; if (*it1 > *it2) { // 交换迭代器指向的对象 T temp = *it1; *it1 = *it2; *it2 = temp; }
这里的Iter是char (*)[N]的话,类型T就是char [N],也就是数组类型。
问题来了,再回顾我这篇博客全篇唯一加红加粗的文字——数组是不能作为左值的。
那么,如果这里的类型T是char*呢?当然就是可以交换了,只不过原数组还是没变。
解决方式即定义一个长度为M的数组,数组元素类型是char (*)[N],然后再排序