今天去看了看传说中的华为招聘,结果没呆多久,做了几道笔试题和一道上机题,然后就回来了。没什么大意思,不过这道上机题还是个亮点,哈哈。
题目看起来是很简单的,就是给一个字符串,要求将其中的空格字符替换为逗号。
而且程序模板都已经定好,main的内容写好了,转换函数的原型也定义好了。实在是非常简单,简单得让我有点纳闷。很快写好程序:
运行一下,问题出来了,程序会在这行代码出现异常:
memcpy(*pOutputString, pInputString, lInputStringLen);
异常内容是Access violation访问空指针,查看一下各变量值。发现问题是*pOutputString的值为0x00000000。
为什么会出现这样的错误?按理说根据传入参数,*pOutputString的值应该是main中定义的数组的地址,数组已经存在于栈中了,那么这个地址就不可能是0x00000000。
问题究竟在哪里,想了好一会,才明白了个中原尤。那就是对于数组的传递和取变量,编译器是做过特殊处理的,数组变量可以如指针一般使用,但并不等同指针。当对它取址时,数组变量将回归其原始涵义。这使得对数组变量取址与直接使用数组变量的效果相同。
我们先从程序的调试输出来找出一些痕迹,在main中设置一个断点。然后运行程序,中断后在Watch查看三个表达式的值:
这里我们能看到,pOutputString与&pOutputString的Value值相同,都是0x0012ff0c。是否是说在地址0x0012ff0c处存了值'0x0012ff0c’,显然不是的,我们看最后一个表达式,这个表达式将地址为0x0012ff0c的内存输出,可以看到,该处连接的四个字节的值均为零。实际上,0x0012ff0c就是数组第一个元素所在的位置,而数组中的元素已被置零。那为什么pOutputString与&pOutputString的值会相同?
为了让这个问题更直白,我们先看看数组与其它数据结构的异同。当我们在程序定义一个int、char、strcut或数组时:
int iTemp = 0;
int iTemps[10] = {0};
其实编译器对这两者所做的工作是类似的,都从栈中保留一块内存区域,用来分别保存一个整数和一组整数。要注意的是iTemp和iTemps只是两个编译器支持的供开发者使用的符号,编译时,编译器将把这两个符号映射为内存的地址。在内存分配上,编译器只是保留保存数据内容大小刚刚好的内存块,并没有为数组分配其它的空间。这也就是说,虽然我们都说数组可以视为等价为指向数组首元素的指针,但这个指针并不是一个真实存在的变量,而是一个类似字面量或立即数的概念。我们可以尝试编译下面的代码:
char pInputString[] = "Hello world ";
char pOutputString[13] = {0};
pOutputString = pInputString;
最后一行代码将得到错误:error C2106: '=' : left operand must be l-value。
我们可以再试试:
1 = 1;
将得到同样的错误,左值必须是一个有内存存储支持的量。
一个真正的指针变量是需要占用目标数据额外的内存存储的,当我们如下定义一个指针变量时:
int *pTemp = new int;
编译将保留两块内存:在堆中保留一块内存用来存放一个整数,在栈中保留一块大小与地址总线位数相同大小的内存用于存储堆中目标数据的地址。
当我们使用pTemp符号时,引用的是栈中的地址。当使用*pTemp符号时,引用的是堆中的整数。
对数组变量来说,在目标数据以外没有受到任何内存的支持。即使编译器被要求将它当作一个指针值,也只能作为右值,而不能作为左值。
因此,我们可以想像对数组变量作取地址操作会出现什么问题,对一个不存在于内存中的值取其存储地址,这是一个矛盾。因此,编译器在这个时候需要将数组变量当作普通数据变量一样的符号来处理,取址时返回其目标数据存储位置的首地址,而这正好与我们直接使用数组变量时的效果一样。这就是刚开始时我们从Watch中观察到的现象的原因。
好了,现在知道问题所在了。我们常说数组会退化为指针,但这只是编译器为了避免内存浪费而设计的特例。它本身依然只是一个变量。以前看《C和指针》等书时,均对数组作了特别的讨论,然而从未想到这点上。很多东西,只有在现实上遇到,才会有深刻的体悟。
然而另外,这道题我还是没正确答出,作为函数的实现者,在不更改函数原型的条件(考题明确要求)下,我想不出一种办法来解决这个问题。当调用者对一个数组变量取址并作强制转换为一个(T **)时,这一切看似合情合理,而我不知道是应该兼容这种状况(将入参char **再强制转换为char*),或是检测这种情况,再或者任其异常。
后话:之前百度了一下,搜了一下对数组变量取地址的相关信息,找到这样的一个帖子,说以下代码:
int a[3][4];
cout<<&a<<endl;
cout<<a<<endl;
cout<<sizeof(&a)<<endl;
cout<<sizeof(a)<<endl;
的输出是:
0012ff40
0012ff40
48
48
对于sizeof(&a)的输出,个人以为不应该是48,无论如何,变量取址的结果都是一个地址值,它的大小应该是4个字节(在32位机器上)。
还好在跟帖中也有人说明在另外一些编译器(GCC 4.4.0,vc2008,icl 11.1)上结果是4。
看来以后得注意一下。