C/C++左值性精髓(三)左值转换----从数组到指针的转换

数组和指针这两种实体,是最令初学者感到痛苦和纠结的一对“冤家”。对两者内涵及联系的不断挖掘的过程,就相当于一次思维风暴。只有彻底理解对象、类型派生方式、左值性和常量等几种低层语言设施,才能获得对数组和指针的完整认识。那么,数组与指针之间千丝万缕的联系究竟是什么原因产生的呢?根本原因就在于下面要谈到的从数组到指针的转换条款。

        C和C++的数组到指针转换条款涵义大体相同,但C90和C99有些差别。C90规定:

        Except when it is the operand of the sizeof operator or the unary & operator, or is a character string literal used to initialize an array of character type. or is a wide string literal used to initialize an array with element type compatible with wchar-t, an lvalue that has type “array of type” is converted to an expression that has type “pointer to type” that points to the initial element of the array object and is not an lvalue.

除了作为sizeof、&及用于初始化字符数组的字符串字面量等几种情况外,一个具有数组类型的左值表达式被转换为指向数组首元素的右值指针。这是一个隐式转换过程。这个条款不仅规定了首元素地址这个数值结果,还规定了转换结果的类型:元素指针。例如:

int a[10];

int *p = a;

上式中的a先从数组类型int[10]隐式转换为int*指针,所代表的值为a[0]的地址,然后用这个int*类型的地址初始化p。

        正由于数组到指针转换条款的存在,表达式中的数组名(除几种情况外)与指针具有结果等效性。请看示例:

char a[10];

char *p = a;

char *q;

q = a + 1;

q = p + 1;

a + 1与p + 1是等效的。要注意的是,这种等效性是体现在表达式计算中的,数组到指针的转换条款表述的仅仅是数组在表达式中的行为,而非本质,转换的目的是将数组类型的表达式数值化,使它们能够参与表达式计算,从而极大地丰富表达式的内容。

        可惜的是,由于对此条款认识不足或者根本不了解有此条款,关于数组的本质产生了种种误解。最典型的一种误解是:数组名是一个指针常量,它属于右值。这种误解源于三类错误:

1. 将数组与指针的等效关系理解成等价关系。等价是相同事物的不同表现形式,而等效是不同事物的相同效果。数组与指针是互不相同的两种实体,它们在表达式中的行为体现的是等效而非等价,仅从某一方面相似的表面语法就将两者的本质简单等同是错误的,数组名不是指针,数组名仅仅是可以转换为指针而已。

2. 将数学中变量和常量概念的惯性思维生硬套到C/C++上,以为不变或者不可变的量就是常量。实际上,C/C++关于变量和常量的概念与数学有很大差别,不变的量不一定是常量,可变的量也不一定是变量。C/C++的变量涵义是一个有名对象,由对象的声明产生,对象的名字就是变量名。数组名作为数组对象的名字其实是符合C/C++关于变量的定义的,因此数组名其实是一个变量,但转换的结果是一个符号地址。

3. 错误地将赋值表达式的行为作为左值定义。前面在“左值的前世今生”一节中已经讨论过,标准C/C++的左值定义是基于对象模型的,在判断一个表达式是否左值时并不以赋值表达式中的行为为依据。对于数据抽象,C/C++关于左值的定义是具有对象类型或非void不完整类型的表达式,数组名作为具有数组类型的表达式,符合左值的定义。

        因此,数组名不是指针常量,但在表达式中及一定条件下,它可以隐式转换为右值指针,转换的结果不一定是常量,要视情况而定。数组名属于左值,不是右值,而且是一个不可修改的左值,因为数组类型属于聚集类型,不是标量类型,数组对象的内容无法视作一个数值。

        在本节第二段的条款内容中,提到了三种不进行转换的例外情形,请看例子:

int a[ 10 ];

char *p = “abcdefg”;           //A

char b[] = “abcdefg”;          //B

size_t size = sizeof( a );       //C

int ( *q )[ 10 ] = &a;           //D

int *k = a;                      //E

由于C/C++将字符串字面量实现为字符数组,因此字符串字面量的类型实际上是数组类型,表达式中的字符串字面量也可以转换为指向其首元素的右值指针,语句A正反映了这种转换,p被“abcdefg”的首元素地址初始化;语句B中的“abcdefg”作为字符数组b的初始化器,这是条款所规定的例外情形,此时“abcdefg”不转换为指针,B相当于如下初始化形式:

char b[] = { ‘a’,’b’,’c’,’d’,’e’,’f’,’g’,’\0’};

对于C和D,sizeof及&的操作数a也不进行转换,所以sizeof( a )的结果是整个数组的大小,&a是数组的首地址,其地址值与E中的a的转换结果一样,但两者的类型是不一样的,&a作为数组首地址,类型是指向数组的指针:int( * )[10],而a的转换结果是指向首元素的指针,因此类型是int*。

        C90的条款限定了只对左值数组进行转换,但事实上,也存在右值数组,右值数组并不是内因的,而是受到了外界的影响使数组呈现出右值性,例如作为右值对象的一部份。C99和C++的转换条款皆允许左值和右值数组的转换,而C90禁止右值数组转换,请看笔者从自己的blog中节选出来的一段代码:

struct Test

{

    int a[10];

};

struct Test fun( struct Test* );

int main( void )

{

    struct Test T;

    int *p = fun( &T ).a;                  //A

    int (*q)[10] = &fun( &T ).a;           //B

    printf( "%d", sizeof( fun( &T ).a ) ); //C

    return 0;

}

struct Test fun( struct Test *T )

{

    return *T;

}

在这个例子里,fun( &T )返回一个Test类型的右值对象,fun( &T ).a就是一个右值数组,是一个右值表达式。在C89/90中,由于规定左值数组才能进行数组到指针的转换,因此A中的fun( &T ).a不进行数组到指针的转换,A语句在C90中是非法的,但C99和C++不再区分数组的左右值性,因此A在C99和C++中都是合法的;语句C中的fun( &T ).a是sizeof运算符的操作数,这种情况下fun( &T ).a并不进行数组到指针的转换,因此C在所有C/C++标准中都是合法的;B语句中的a作为&运算符的操作数属于转换的例外情况,虽然不进行转换,但B仍然是非法的!为什么?其实B违反了另一条规定,对于数据抽象,&的操作数要求是左值,而fun( &T ).a是右值。

你可能感兴趣的:(c/c++,左值)