摘 要:二十一世纪的今天,计算机技术在迅速发展,C/C++在计算机语言中应用是最为广泛的,也是最复杂的一种语言,此种语言能帮助人们充分掌握和理解计算机程序设计,有其重要的基本技能,本文主要对C、 C++、左值、右值及其关系进行了分析。
关键词:C C++ ;左值; 右值
C/C++是当今最复杂的计算机语言,往往花费较多时间才能取得一丝进步。C/C++基础语言设施中,表达式的左值性是与我们平时的编码工作关系较为密切的一个。掌握好它,许多平时看不透的问题会迎刃而解,还对于提高代码质量及增强C/C++的理解也具有很大的现实意义。
1. 左值性
C++关于左值性的定义是: 一个表达式不是左值就是右值。这个定义与C完全不一样,具体原因与C/C++的类型划分方法有关。C/C++中的类型系统可以有多种划分方法。而且由于C++规定引用类型属于左值,同时也把函数纳入左值范畴,因此C++的左值包括了对象模型划分方法中的所有类型,再使用类似C那种左值定义的描述方法是多余的,C++仅需要指出一个表达式不是左值就是右值就行了,但C就不行,因为C存在既不是左值也不是右值的表达式:函数指示符!
2. 左值,右值
2.1 左值表达式
左值具有对象或不完整类型,在C++中还具有函数或引用类型。但是,并非具有上述类型的表达式就是左值,关键是左值必须指示出一个对象,在C++中则包含非静态成员函数之外的函数,无论该对象(或实体)是否有效。
2.2 右值表达式
C中所有完整表达式的结果都是右值,包括函数调用返回值及强制转换的结果等等都属于右值,而对于子表达式计算产生的中间结果或临时对象,很多人以为也都是右值,但实际上,它们不一定是右值。
但C++的完整表达式其实是C++某些运算符的强制规定,例如内置赋值运算符、前置增量和前置减量运算符等等,这类运算符的结果被强制规定为左值,原因无它,仅仅规定而已。无论C还是C++都存在右值对象,。因此,右值只是不要求是对象,并非不能是对象。
2.3 函数调用表达式和强制转换
对于函数调用表达式和强制转换表达式的结果,在C中都属于右值;C++由于增加了引用类型,因此返回引用的函数调用表达式和强制转换表达式都属于左值,示例如下:
int& fun1( int & r ){ return r; }
int fun2( void ){ return 10; }
int i = 20;
fun2( ) = 30; //A
cout << ( fun1( i ) = 30 ); //B
( int & )10 = 20; //C
( const int & )10 = 20; //D
( int & )i = 40; //E
( double & )i = 50; //F
cout << ( double & )i; //G
A: fun2的返回值是一个右值,不能作为内置赋值表达式的左操作数,因此A是错误的;
B: fun1返回一个引用,属于左值,因此可以作为内置赋值运算符的左操作数;
C: C试图将一个右值强制转换为引用,但是,只有const引用才能引用一个右值,因此错 误;
D: D比C进步了一点,强制转换为const引用,但仍然是错误的,因为const引用属于不可修改的左值,不能通过const引用修改其引用的对象;
E: E将一个int变量强制转换为int引用并被修改。这个表达式容易出现误解,以为i被临时转换为一个引用,其实不然,( int& )i只是产生一个引用到i的临时引用,i是被引用的对象而非引用本身,被修改的是i的值;
F、G:F和G与E一样,都产生一个引用到i的临时引用,但存在两个问题,一是由于i的类型与double&所引用的类型不同,i的底层布局从double&的角度看来是double,F中的50先被转换为double,再存进i,存进i的内容并非int格式的50,而是浮点数格式的50,如果此时打印i的值,结果将为一个"混乱"的整数;二是由于double和int的二进制宽度不一定相同,如果double宽度大于int,则F和G都将导致未定义行为。
对于C++新增的static_cast、reinterpret_cast、const_cast三种强制转换方式,由于C++将C风格强制转换和函数风格强制转换都转换为上述三种方式,因此结果与上述例子相同。而对于dynamic_cast,若目标类型为引用,结果为左值,否则为右值。
2.4 后缀表达式
2.4.1 E1.E2形式的后缀表达式
若E2为静态数据成员或引用数据成员,无论E1的左值性如何,E1.E2的结果都是左值;若E2为非静态非引用数据成员,C和C++标准都规定如果E1为左值,则E1.E2也是左值;当E1为右值时,从原理上说,右值对象的一部分也应该是一个右值,因此在C中,无论E2的左值性如何,E1.E2皆为右值;那么C++中的结果又如何呢?按道理应该顺理成章也为右值吧,但令人惊讶的是,C++98和C++2003都没有对此作出规定!因此在C++98和C++2003中,无论编译器将这种情况作为左值或右值都没有违反标准。这种情况显然是一个漏洞,在C++新标准C++11的制定过程中,WG21的专家在其Defect Reports中承认了这一点,并在C++11中将结果修正为一个prvalue(pure rvalue,纯右值)。
2.4.2 E1->E2形式的后缀表达式
由于C中的右值地址无法获得,因此C中的E1总是指向左值的指针,所以C中的E1->E2后缀表达式的结果总是左值;但C++有很大不同,C++中的E1->E2后缀表达式是转换为等价形式(*E1).(E2)进行计算的,因此E1->E2的左值性与E1.E2相同。
2.5 运算符的左值性
在C中,除解引用和下标之外的运算符都不产生左值,下标运算符由于会转换成等价的*( E1 + E2 )表达式,实际上也属于解引用。而C++观念有所不同,有些运算符也可以产生左值。请看下述例子:
int a = 10;
int b = 20;
++a = 30; //A
( a = 40, b ) = 50; //B
a == 40 ? a : b = 60; //C
上面A、B和C三条语句在C中是非法的,因为都不是左值,但在C++中都是合法的。对于C,b = 60并非整个条件表达式的第三子表达式,60赋予的是a == 40 ? a : b这个条件表达式的结果a。
3.左值转换
左值转换又称为从左值到右值的转换。左值转换的发生一般基于两个原因:
第一:某些运算符仅要求右值操作数,例如+、-双目运算符,如果给予它们一个左值,这时候就需要把左值转换为右值;
第二:某些指示符,例如数组和函数,由于不属于标量类型,即所代表的实体本身不能被视为数值,这些指示符欲参与表达式计算,就必须将其数值化,数值化的结果,不同于一般对象的值,而是实体地址。
因此,基于上述原因,左值转换分为三类,分别是从左值到右值的转换、从数组到指针的转换和从函数到指针的转换。C++基于函数重载解析的需要
,将上述三类左值转换概念化了,但C仅提出了条款,没有提出概念。
3.1 从左值到右值的转换
由于函数非引用返回值属于右值,所以如果函数返回内置类型且带有cv修饰,该cv修饰将被忽略。
正因为从左值到右值的这种转换结果的存在,我们可以用一个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++无法对此情况进行分级。
3.2 从数组到指针的转换
C和C++的数组到指针转换条款涵义大体相同,但C90和C99有些差别。C90规定,除了作为sizeof、&及用于初始化字符数组的字符串字面量几种情况外,一个具有数组类型的左值表达式被转换为指向数组首元素的右值指针。这个条款不仅规定了首元素地址这个数值结果,还规定了转换结果的类型:元素指针。
数组名不是指针常量,但在表达式中及一定条件下,它可以隐式转换为右值指针,转换的结果不一定是常量,要视情况而定。数组名属于左值,不是右值,而且是一个不可修改的左值,因为数组类型属于聚集类型,不是标量类型,数组对象的内容无法视作一个数值。
在本节第二段的条款内容中,提到了三种不进行转换的例外情形,请看例子:
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','