C/C++左值性精髓(三)左值转换 zz from CSDN supermegaboy

C/C++左值性精髓

(三)左值转换

 

        左值转换又称为从左值到右值的转换。左值转换的发生一般基于两个原因:

第一:某些运算符仅要求右值操作数,例如+、-双目运算符,如果给予它们一个左值操作数,这时候就需要把左值转换为右值;

第二:某些指示符,例如数组和函数,由于不属于标量类型,即所代表的实体本身不能被视为数值,这些指示符欲参与表达式计算,就必须将其数值化,数值化的结果不同于一般对象的值,而是实体地址。

        因此,基于上述原因,左值转换分为三类,分别是从左值到右值的转换、从数组到指针的转换和从函数到指针的转换。C++基于函数重载解析的需要,将上述三类左值转换概念化了,但C仅提出了条款,没有提出概念。

1. 从左值到右值的转换

 

        先看一个例子:

 

int i = 10;

 

编译器在内存中开辟一段具有sizeof( int )个字节的空间,用整数10初始化,并将该空间命名为i,i属于左值。当把i放在下面的表达式中时:

 

i + 1;

 

        由于+双目运算符仅要求右值,i这个符号并不是它需要的,此时编译器就从i所代表的对象中取出整数10,交给+运算符进行运算。从i所代表的对象中取出整数10的过程就是一个从左值到右值的转换过程。在这一过程中,原本的左值i被右值10代替,但是,i本身的性质并无改变,i仍然是一个左值,有些人会误解为i已经成了一个右值,将转换的结果理解成了性质的改变,其实是犯了本末倒置的错误。

        如果将i的定义改为cv受限形式,例如:

 

const int i = 10;

 

那么计算i + 1时,i转换的右值是否也带有const呢?所谓cv,指的是const和volatile两个修饰符。在C中,从左值到右值的转换结果不带有cv受限形式,即使左值是cv受限的,即C中不存在cv受限的右值;而C++稍有不同,允许存在cv受限的右值类对象,但右值内置类型与C一样。

        由于函数非引用返回值属于右值,所以如果函数返回内置类型且带有cv修饰,该cv修饰将被忽略。请看如下代码:

 

const int foo( void );

int i = foo();

 

虽然foo返回的类型是const int,但它赋予i的数值是int类型的,而非const int。

        正因为从左值到右值的这种转换结果的存在,我们可以用一个cv受限的左值赋予或初始化一个非cv受限的左值,例如:

 

const int i = 10;

int j;

j = i;

 

虽然i是cv受限的,但i转换的右值不带有cv,因此可以成功赋予非cv受限的j。

        另一方面,基于相同的原因,形参的cv修饰符并不构成C++的函数重载条件,如下所示:

 

int foo( int i );

int foo( const int j );

 

上面两个foo函数是相同的函数重复声明,并非重载,因为无论是否cv受限的整数,都可以作为i和j的实参,且都属于精确匹配,C++无法对此情况进行分级。

2. 从数组到指针的转换

 

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

        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是右值。

3.从函数到指针的转换

 

        将函数转换为指针的目的,与数组到指针的转换一样,都是为了将符号数值化,以利于表达式计算。该条款规定:

 

        A function designator is an expression that has function type. Except when it is the operand of thesizeofoperator or the unaryoperator, a function designator with type ‘‘function returningtype’’ is converted to an expression that has type ‘‘pointer to function returningtype’’.

 

除了几种例外,一个具有函数类型的函数指示符被转换为指向该函数实体的指针。在C中,严格来讲,函数到指针的转换并不属于左值转换,因为C中的函数既不是左值,也不是右值,也正因为这个原因,C中的条款内容并没有指出转换的左值性。但对于C++,函数属于左值,因此该转换属于左值转换,结果是一个右值指针。

        关于C++函数的左值性,有一个例外,就是非静态成员函数不是左值。笔者最初对此感到非常迷惑,因为从抽象本质上说,非静态成员函数并没有不符合C++左值涵义之处。笔者曾经向C++的创始人Bjarne Stroustrup博士发了一封email,向他请教这个问题,BS在回复中说,他认为这个规定是一种不太优雅的技术处理方式,以区别普通函数和非静态成员函数。就是说,这是一个人为规定。由于非静态成员函数被剔除出左值范畴,也导致非静态成员函数不存在从函数到指针的转换,非静态成员函数指针必须通过&运算符获得,例如:

 

struct A

{

void foo( void );

};

 

void ( A::*p )( void ) = A::foo;      //A

void ( A::*q )( void ) = &A::foo;     //B

 

A是错误的,因为非静态成员函数的隐式转换不存在,B才是正确的。

        对于使用函数指针进行函数调用,存在两种方式,分别为:

 

p();           //A

( *p )();      //B

 

两种方式都是合法的。因为,函数调用表达式要求其后缀表达式操作数的类型是函数指针,使用普通函数名进行函数调用时,其实是先将函数名转换为函数指针再进行调用的。这是第一种方式成立的原因。而第二种方式,*是解引用运算符,对一个函数指针进行解引用的结果是该指针指向的函数类型,然后该函数类型又通过函数到指针的转换变回函数指针类型,最后再进行函数调用。两种方式其实是殊途同归。第二种方式还可以产生某些有趣的形式,例如:

 

( ***p )();

( **********p )();

( ************************p )();

 

这些解引用运算符可以用上述类似的方式无限填充下去,但结果都是一样的,只需要不断进行解引用和函数指针转换就行了。


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值