数组和指针是C/C++语言的一对欢喜冤家。两者有时可等同看待,有时不能等同看待。这是让C++新手颇为头痛的一件事。
一般情况下,即使是一个C++开发新手也都明白“所有作为函数参数的数组名总是可以通过编译器转化为指针”。也正因为如此,才导致了数组和指针的混乱局部。《C专家编程》对指针和数组的等同情况进行了总结。如图4-2所示。
什么时候数组和指针等价?
C语言标准对此作出如下说明:
- 规则1:表达式中的数组名(与声明不同)被编译器当做一个指向该数组首元素的指针。
- 规则2:下标总是与指针的偏移量相同。
- 规则3:在函数形式参数声明中,数组名被编译器当作指向数组首元素的指针。
“表达式中的数组名”就是指针,如果把规则1和规则2合在一起,可得出结论:对数组下标的引用总是可以写成“一个指向数组起始地址的指针加上偏移量”。按照这个结论,假设我们声明:
int a[10] = {1, 2,3,4,5,6,7,8,9,10};
int *p = NULL;
int i = 2;
可通过下面任何一种方式访问a[i],且都是等价
第一种 第二种 第三种
p = a; p = a; p= a+I;
p[i]; *(p+i); *p;
然而,事实上,对数组的应用,例如a[i]在编译时总是被编译器改写成*(a+i)这种指针的形式。编写过类[]操作符重载的都会了解这一情形,在[]操作符重载时,程序基本上都是返回*(指针+偏移)这种形式。
“作为函数参数的数组名”等同于指针,这是规则3。当数组名作为实参传递给函数时,编译器进行了两步处理:1、数组不会发生复制;2、使用数组名称时,数组名会自动转化为指向第一个元素的。针。例如,可通过下面三种方式指定数组形参:
void Print_Val(int *) {/**/}
void Print_Val(int []) {/**/}
void Print_Val(int [10]){/**/}
通常,在函数声明或定义时,将数组形参直接定义为指针要比使用数组语法定义更好,这种做法可以明确的表示,函数操作的是指向数组元素的指针,而不是什么数组。
除此之外,如果采用数组语法定义时,如果函数定义时包含了数组长度会特别容易引起误解。因为数组长度在函数内部是无意义的,编译器会忽略数组形参指定的长度。
下面是一个因数组长度引起误解的例子:
void PrintVal(const int ia[10])
{
// 这段代码假设数组ia包含10个元素。但实际上并非如此。
for (int i = 0; i < 10; i++)
{
cout << ia[i] << endl;
}
}
int main()
{
int i = 0;
int ia[2] = {1, 2};
PrintVal(&i); // 由于&i是int *,因此这句代码可以编译通过
PrintVal(ia);
}
可看出,虽然PrintVal假定所传递的数组长度至少应包含10个元素。但是C++没有任何机制限制这个假设。所以导致PrintVal(&i);和PrintVal(ia);这样的代码可以编译通过。
但问题也就出现了,PrintVal假定接收的数组长度至少为10个元素。但是main函数中传递的实参数组元素个数均为达到10个。所以这两个调用都是错误的。
然而,为什么C/C++语言在函数参数传递时把形参当作指针呢?C++采用这种处理方式主要出发点是出于效率的考虑。
在C++中,所有非数组形式的数据实参均以传值的形式(对实参作一份拷贝并传递给调用的函数,函数不能修改实参的实际变量的值,而只能修改传递给它的那份拷贝)调用,然而如果数组传值时拷贝整个数组,无论在时间上还是在空间上的代价都是巨大的。如果数组中存放的是类对象,在拷贝时会附带着类拷贝构造函数的调用,开销就更大了。
不仅如此,函数的形参采用传址调用方式还可以简化编译的实现。还有一举两得的效果。类似的,函数的返回值也不能是一个数组,而只能是指向数组的指针。
最后,数组和指针何时等价进行总结:
- 采用a[i]这种形式访问数组,编译总会把其“改写”成像*(a+i)这种指针访问。
- 指针始终是指针,任何时候也不会改写成数组,但可采用数组下标方式访问指针。
- 作为函数的形式参数,一个数组的声明就是一个指针,一个指向数组第一个元素的指针。
请谨记
- 作为函数形参时,指针和数组等价,数组会退化为一个指向数组首元素的指针。
- 数组名(与声明不同)被编译器当做一个指向该数组首元素的指针,而且是const指针。
- 无论采用a[i]或*(a+i)形式访问,编译器在编译时均会改写成*(a+i)指针形式访问。