题目一:函数参数中的数组大小
先看下面这段代码,思考 sizeof 分别会输出什么。
void func(int arg[10])
{
cout << sizeof (arg)<<endl;
}
int main()
{
int arg[10];
func(arg);
cout << sizeof arg<<endl;
system("pause");
return 0;
}
答案是 4 和 40。
虽然 func 在声明时把参数写成了整型数组,但一进入函数体,arg 就已经退化成了 int* 指针,也就是说这个函数参数只占 4 字节内存。
可以从反汇编角度验证这一点:
func(arg);
00374D2E lea eax,[arg]
00374D31 push eax
00374D32 call func (03714E7h)
00374D37 add esp,4
由于 C 函数默认采用 cdecl 调用约定,调用结束后的 add esp,4 中的 4 就是 func 传入参数的总大小——正好对应一个指针的字节数。
为什么声明的 int a[10] 变成了 int*?
这里涉及到所谓的「数组名退化」。数组名确实是首地址,它的值等于该数组首元素的地址,但数组名本身的数据类型并不是指针,只是它的值等于指针而已。因此 func 的声明实际上等价于:
void func(int *arg);
也就是说,即使你在形参位置写成数组,编译器也会把它退化为 int* 类型。换句话说,数组可以看成一种基本数据类型,它和指针是有区别的。
正因为如此,很多标准库函数才会采用类似下面这种签名,在参数里额外显式地指定「数组」长度:
void *memcpy(void *dest, const void *src, size_t n);
void *memset(void *s, int ch, size_t n);
题目二:指针赋值与数组类型
再看一段代码,这次 sizeof 会输出什么?
int main()
{
int arg[10] = {5,5,5,5,6,8,7,41,2,5};
int *ptr_;
ptr_ = arg;
cout << sizeof ptr_;
system("pause");
return 0;
}
答案是 4。
arg 是数组类型,而 ptr_ 是 int*,所以 sizeof ptr_ 自然是 4。但问题来了:为什么 sizeof arg 会是 40?
关键在于 ptr_ = arg 并不是一次简单的赋值操作。因为 arg 和 ptr_ 的数据类型本来就不一样,所以 arg 必须先被转换成和 ptr_ 一样的类型。反汇编可以证明这一点:
int *ptr_=arg;
01108C8E lea eax,[arg]
01108C91 mov dword ptr [ptr_],eax
这就是所谓「数组名退化为指针」的真正来源。对编译器来说,数组类型并不是 float、int 这样的基本数据类型,所以它不能像 mov eax, arg 那样直接拷贝值——因为两边根本不是同一个类型。
再看下面这段代码:
int main()
{
int arg[10] = {1,2,3,4,5,6,7,8,9,0};
int *ptr_=arg;
int int_ptr = (int)arg;
int_ptr += 4;
cout << *((int*)int_ptr);
system("pause");
return 0;
}
int_ptr += 4 的结果等价于 cout << *++ptr_;。这说明 int 和 int* 在运算上根本不是同一种类型:对指针来说,+1 意味着跳到当前内存单元的下一个单元,也就是 arg 的下一个地址单元,也就是 arg[1] 所在的内存空间。
用 C++11 的 decltype 进一步验证
最后用一段 C++11 的代码,可以非常轻松地证明:数组的第一个元素并不是指针。
int main()
{
int arg[10] = {1,2,3,4,5,6,7,8,9,0};
decltype(arg) arg_test;
cout << sizeof (arg_test);
system("pause");
return 0;
}