我不知道大家是不是和我一样有这样的认识:一级指针和一维数组的语义行为是等价的,或者从编译器的角度看,两者是一样的。我想很少有人清楚编译器如何工作,又或许在实际工作中把两者当成同一物没有出现过问题,也就不会去细究两者的异同。
1. 什么是语义行为?
语义行为就是编译器用一系列操作对某个产生式(production rule)的解释。对于一个指针int *p,它的语义行为有:
- p所指向的元素类型是int;
- p变量本身有一个地址,其值为另一个合法的地址,两者并无关系。
另一方面,int p[]有两层含义:
- p 的类型是int[],对p不做越界检查;
- p的值就是p的地址(试试输出p和&p,看看两者是否一样);
- p[2]的计算方法是*(p的值+2*sizeof(int)),注意这里说的是p的值。
如果表达式为int p[10],它相对int p[]要做越界检查。对于高维数组,其语义和一维的完全一样,只是地址计算上稍微繁琐。
2. 不区分一维数组和一维指针会导致问题吗?
一种常见的混用方式是:
{
printf( “%dn”, p[2] );
}
int main()
{
int v[] = {1, 2, 3};
f( v );
return 0;
}
这段程序不会有问题,因为参数传递是按值复制的,就是说p的值为v的值(就是v数组的地址)。但下面一段程序就要出错了:
A.c
B.c:
这段代码错误的原因在于:v作为数组时,其v的值语义为v的地址。现在v被告知是一个指针,于是v的值就是v的地址中所包含的值,上例中就是1。v[2]相当于1[2],即取地址为3中的值,自然会segmentation fault。
3. 二维数组和二级指针的若干问题
看下面的示例代码:
void f( int **v )
{
printf( “%dn”, v[0][1] );
}
void g( int v[2][3] )
{
printf( “%dn“, v[0][2] );
}
int main()
{
int v[][3] = { {1,2,3}, {3,4,5} };
int **t = v;
f( v );
g( t );
return 0;
}
很多人可能认为(包括我),二维数组的变量被编译器当做二级指针看待。其实这是错的,上例中f(v)的调用将会导致一个segmentation fault。根据数组的语义,int p[2][3]实际上是p指向一个int[2][3]类型的变量,据此来看看在f中发生的事情:v[0][1]实际上在尝试解析内存地址2,因为参数v的值就是传入数组的起始地址,所以v[0]就是1,然后解析地址1[1],固然导致错误。
那么,如何改造f,可以得到正确的输出呢?f可以如下实现:
void f( int **v )
{
int (*t)[3] = v;
printf( “%dn“, *((t+1)[0]) );
}
但如果f在一个已经发布的函数库中(如getopt的第二个参数),那么我们便不能改动f。怎么办呢?此时就需要在调用f前做一件事情:
int **p;
p = (int**)malloc( sizeof(int) * 2 );
for ( i = 0; i < 2; ++i )
p[i] = (int*)malloc( sizeof(int) * 3 );
for ( i = 0; i < 2; ++i )
for ( j = 0; j < 3; ++j )
p[i][j] = v[i][j];
f( p );
为什么这样?p与v不同就在于数据没有连续存放,中间引入了一个转换层。因此,p的地址解析需要通过两步完成,而v只用一步。除此,p还比v多浪费了2个void*的空间用于存放中间转换层数据。
上文中g(t)的调用是对的这个很容易看出,因为t只是起了个保存v的数据的作用,并没有做解析,然后将其传递给了类型匹配的参数。那用到t转换一次的意义在哪里呢?如果将g改造如下:
void g( int v[3][2])
{
printf( “%dn“, v[0][2] );
}
此时v的类型为int[3][2]而并非起初定义的int[2][3],程序仍然能编译通过。原因就在于用t做转储时,丢失了范围语义信息,所以再转换回来时编译器已经糊涂了,也只好默认它们能正常工作,只是释放一个warning而已。
以上可见,从语义的角度来学习一个语言,会在理解上有本质的提高。对于遇到的“奇怪”问题,多做些这样的分析才可以得到正确的解释。