C++手稿:数组传参与降级行为总结

C++ 内存 常量 引用 指针 数组 运算符 函数指针 类型转换

指针和数组是C++中最不直观的概念之一,而数组传参和数组指针降级则需要更加地小心。 本文便来总结一下多维数组传参的问题,以及数组指针的降级行为。本文主要介绍两个要点:

  • 数组名在多数情况下会被降级为首元素的指针,但数组不是指针!
  • 多维数组可以视作一个普通数组,只不过元素类型也是数组。多维数组也存在降级行为。

至于指针数组、数组指针、函数指针、常量指针和指针常量等基础性的问题, 请参见C++手稿:指针与引用一文。

数组降级行为

多数情况下数组名会被转换为首元素的指针, 数组的这一行为称为降级(decay,degrade)。 数组降级发生在赋值时、传参时、以及函数返回时。 例如:

int a[5];
a == &a[0];     // 数组名降级为首元素地址
int *p = a;     // 等号左右的类型是兼容的

可见,数组名a的类型int[]兼容于指针p的类型int*。 因为数组名的降级行为,我们常常把数组名看做指针, 但是在C++中数组和指针是不同的概念, 数组是一组同类型元素的有序集合;指针是变量的地址。 请看例子:

int a[5];
int b[] = {1, 2, 3};
int *p = a;

p++;    // p指向第二个元素
a++;    // Error!

sizeof(a);  // 5
sizeof(p);  // 8    64位平台
sizeof(b);  // 12

怎么样?这都说明数组是数组,指针是指针。

解引用运算符

数组的解引用太常见了以至于我们忽略了在这个过程中指针是怎样转换为值的。 数组元素指针的解引用有两种方式。一种是下标运算符:

int a[5] = {1, 2, 3, 4, 5};
a[2] == 3;

[]运算符可以使得指针类型的a解引用为int类型的值。 解引用的另一种方式是解引用运算符*

int a[5] = {1, 2, 3, 4, 5};
int *p = a;
p++;
*p == 2;    // 解引用运算符

double *d = (int*)a;
d++;
*(int*)d == 3;  // double(64位)的空间是int(32位)的2倍

指针自加时,增加的地址偏移量为所指类型的大小。d所指类型为 double,其大小为8字节,等于两个int大小。

上述代码是C风格的,在C++中我们不应作不必要的类型转换,也不应假设C++的内存布局,见Effective C++: Item 27

数组传参

C++不同于Java,数组永远不会以值传递,这条规则源于C++的设计原则。 传递数组时总是会发生数组降级,传递的是第一个元素的指针。下面的声明是完全等价的:

void f(int*);
void f(int[]);
void f(int[5]);

即数组长度不是参数类型的一部分,你的函数f是不会知道参数数组到底有多长的, 编译器也不知道!如果你传递的是引用,那么数组长度会标识参数类型:

// 只接受长度为10的int数组
void f(int (&arr)[10]);

其实在面向对象C++中,数组一般用STLvector来实现。上述问题都不存在了, 把vector当做对象即可。

多维数组传参

多维数组的创建和声明都很简单,略过不谈了。下面讨论二维数组的传参方式。 通过参数传递二维数组有三种方式:元素指针int**、数组指针数组int (*)[N]、 和多维数组int[][N]

传递元素指针

第一种:元素指针int **,即传递多维数组中元素类型的指针。 和上一小节中讨论的一样,下面的声明也是完全等价的:

void f(int **);
void f(int *arr[]);
void f(int *arr[5]);

// 或者略去参数名的方式
void f(int *[]);
void f(int *[5]);

参数类型都是int**,其中*arr[5]可以理解为由int *构成的数组, 大小为5,命名为arr

传递数组指针数组

第二种:数组指针数组int (*)[N],即指向int [N]的指针构成的数组。

// f 接受一个指针(可以由数组降级而来),它指向长度为5的int数组
void f(int (*arr)[5]);

int a[2][5], b[2][4];
f(a);       // OK
f(b);       // Error: no conversion from 'int [2][4]' to 'int (*)[5]'

上面的变量a被传参时发生了降级,从int[2][5]降级为int (*)[5]了。

传递多维数组

第三种:多维数组int[][N],即第二维长度为N的二维数组。其实传递的还是指针,见下文。

void f(int arr[][5]);
int a[2][5], b[2][4];
f(a);       // OK
f(b);       // Error: no conversion from 'int [2][4]' to 'int (*)[5]'

看到没有?数组降级啦!错误信息中可以看到int[][5]被编译器视作了int (*)[5]。 二维数组本质上还是一个数组,只不过数组元素变成了int[5]。 数组降级的行为仍然适用,下面几个声明仍然完全等价:

void 
void f(int (*arr)[5]);
void f(int arr[][5]);
void f(int arr[2][5]);
Harttle

致力于简单的、一致的、高效的前端开发

看看这个?