数组与指针的区别,以及在STL中传递数组/指针

数组和指针在作为实参传入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]都置为'\0'(对应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\n", 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],然后再排序

posted @ 2017-03-21 18:28  Harley_Quinn  阅读(1192)  评论(0编辑  收藏  举报