题目一:函数参数中的数组大小

先看下面这段代码,思考 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;
}

答案是 440

虽然 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 并不是一次简单的赋值操作。因为 argptr_ 的数据类型本来就不一样,所以 arg 必须先被转换成和 ptr_ 一样的类型。反汇编可以证明这一点:

	int *ptr_=arg;
01108C8E  lea         eax,[arg]
01108C91  mov         dword ptr [ptr_],eax  

这就是所谓「数组名退化为指针」的真正来源。对编译器来说,数组类型并不是 floatint 这样的基本数据类型,所以它不能像 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_;。这说明 intint* 在运算上根本不是同一种类型:对指针来说,+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;
}