C++学习笔记(IV) 之 表达式

表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。

C++定义了一元运算符(unary operator)、二元运算符(binary operator)和三元运算符。作用于一个运算对象的运算符是一元运算符,以此类推。

有些符号既能作为一元运算符也能作为二元运算符,如 ‘*’,作为解引用时一元,作为乘法操作时二元。

对于含有多个运算符的复杂表达式来说,要结合运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。

C++语言规定的转义序列包括如下:

         
换行符 \n  进纸符 \f
回车符 \r  双引号 \"
横向制表符 \t  单引号 \'
纵向制表符 \v  反斜线 \\
报警(响铃)符 \a  问号 \?
退格符 \b    
       

指定字面值的类型

L‘a’        //宽字符型字面值,类型是wchar_t

u8"Hello!"    //utf-8字符串字面值(utf-8用8位编码一个Unicode字符)

42ULL       //无符号整型字面值,类型是unsigned long long

1E-3F       //单精度浮点型字面值,类型是float

3.14159L     //扩展精度浮点型字面值,类型是long double

左值和右值

C++的表达式要不然是右值(rvalue, 读作"are-value"),是不然便是左值(lvalue, 读作"ell-value")。此两个词从C语言继承而来,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。

在C++中,当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。一个重要的原则:在需要右值的地方可以用左值代替(有个例外,见右值引用),但不能把右值当成左值(即位置)来使用。

  • 赋值运算符需要一个左值作为其左侧运算对象,结果仍是左值。
  • 取地址符作用于左值运算对象,返回一个指向该运算对象的指针,这个指针是右值
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值
  • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值

关键字decltype对左值与右值返回值结果也是不同的。

 

求值顺序

  优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。对于那些没有指定执行顺序的运算符来说,如果表达式指向修改了同一个对象,将会引发生错误并产生未定义的行为。
eg: <<运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:

int i = 0;
cout << i << " " << ++i <<endl;//未定义

  因为程序是未定义的,所以我们无法推断它的行为。编译器可能先求++i的值再求i的值,此时输出的结果是 1 1;也可能先求i的值再求++i的值,此时输出的结果是 0 1;甚至编译器还可能做完全不同的操作。因为表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。有4种运算符明确规定了运算对象的求值顺序。它们分别是逻辑与(&&)运算符逻辑或(||)运算符条件(? :)运算符逗号(,)运算符

I.  逻辑与(&&)运算符,规定先求左侧运算对象的值,只有当左侧运算对象的值为时才继续求右侧运算对象的值。
II. 逻辑或( ||  )运算符,规定先求左侧运算对象的值,只有当左侧运算对象的值为时才继续求右侧运算对象的值。
III.条件(? :)运算符,格式如:cond ? expr1 : expr2 规定,如果条件cond为真,对expr1求值;否则对expr2求值。
IV.逗号(,)运算符,按照从左到右的顺序依次求值。逗号运算符的结果是右侧运算对象的值,当且仅当右侧运算对象是左值时逗号运算符的结果是左值。

运算对象的求值顺序与优先级和结合律无关。

建议:处理复合表达式

I. 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
II. 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

 

算术运算符

整数相除结果仍是整数。

运算符%,参与取余 运算的运算对象必须是整数类型:

int ival = 26;
double dval = 2.6;
ival % 6;    //正确:结果为4
ival % dval; //错误:运算对象是浮点类型

根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等。隐含的意思是,如果m%n不等于0,则它的符号和m相同。

逻辑和关系运算符

  关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象(算术类型或指针类型)表示假,否则表示真。对于这个两类运算符来说,运算对象和求值结果都是右值。

有时逻辑与运算符,可以利用它的左侧运算对象来确保右侧运算对象求值过程的正确性和安全性。

index != s.size() && i != isspace(s[index])

检查index是否达到string对象的末尾,以此确保只有当index在合理范围之内时才会计算右侧运算对象的值。

 成员运算符

点运算符与箭头运算符都可用于访问成员,其中,点运算获取类对象的一个成员;箭头运算符与点运算符有关,表达式ptr->mem等于(*ptr).mem:

string s1("some string"), *p = &s1;
auto n = s1.size();    //运行string对象s1的size成员
n = (*p).size();        //运行p所指对象的size成员            
n = p->size();          //等价于(*p).size()

因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没加括号,代码的含义就大不相同了:

//运行p的size成员,然后解引用size的结果
*p.size();    //错误:p是一个指针,它没有名为size的成员

这条表达式试图访问对象p的size成员,但是p本身是一个指针且不包含任何成员,所以以上述语句无法通过编译。

箭头运算符是作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。

条件运算符

条件运算符(? : )允许我们把简单的if-else逻辑嵌入到单个表达式当中,条件运算符形式如下:

cond ? expr1 : expr2;

其中,cond是判断条件的表达式,而expr1expr2是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求cond的值,如果条件为真时对expr1求值并返回该值,否则对expr2求值并返回该值。

eg:

string finalgrade = (grade < 60) ? "fail" : "pass";

 

当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。

finalgrade = (grade > 90) ? "high pass"
                          : (grade < 60) ? "fail" : "pass";

条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合。因此在上面的代码中,靠右边的条件运算构成了靠左边的条件运算的 分支。

WARNING:
  随着条件运算嵌套层数的增加,代码的可读性急剧下降。因此,条件运算的嵌套最好别超过两到三层。

 位运算符

  位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制拉的功能,后面我们提到的名为bitset的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用于bitset类型。

                    位运算符表
结合律 运算符 功能 用法  说明
L ~ 位求反 ~expr  
L << 左移 expr1 << expr2  
L >> 右移 expr1 >> expr2  
L & 位与 expr & expr  
L ^ 位异或 expr ^ expr  
L | 位或 expr | expr  

如果运算对象是“小整数”,则它的值会自动被提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。

WARNING:
  关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型。

 sizeof运算符

   sizeof运算符返回一条表达式或一个类型名字所占的字节数。满足右结合律,其返回是size_t类型的常量表达式。

sizeof (type)
sizeof expr

  在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的是,sizeof并不实际计算其运算对象的值

  在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof运算符的结果部分地依赖于其作用的类型:

  • 对char或者类型为char的表达式执行sizeof运算,结果得1.
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小
  • 对指针执行sizeof运算得到指针本身所占空间的大小
  • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效
  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。sizeof运算不会把数组转换成指针来处理
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间

  因为执行sizeof运算能得到整个数组的大小,所以可以用数据的大小除以单个元素的大小得到数组中元素的个数。

逗号运算符

  逗号运算符(comma operator)含有两个对象,从左向右依次求值。先对于左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正在结果是右侧表达式的值。若右侧运算对象是左值,最终结果也是左值。

类型转换

其他隐式类型转换

数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:

int ia[6];//含有6个整数的数组
int* ip = ia;//ia转换成指向数组首元素的指针

当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof、typeid等运算符的运算时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。

指针的转换:C++规定了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。在有继承关系的类型间还有另外一种指针转换的方式。

转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:

char *cp = get_string();
if(cp) /* ... */        //如果指针cp不是0,条件为真
while (*cp) /* ... */   //如果*cp不是空字符,条件为真

 显式转换

有时我们希望显式地将对象强制转换成另外一种类型。

 命名的强制类型转换

一个命名的强制类型转换具有如下形式:

cast-name<type>(expression);

其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-namestatic_castdynamic_castconst_castreinterpret_cast中的一种。dynamic_cast支持运行时类型识别。

I). static_cast
  任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。

eg: 

//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;

当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。

II). const_cast

  const_cast只能改变运算对象的底层const。

const char *pc;
char *p = const_csat<char*>(pc);//正确:但是通过p写值是未定义的行为

对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。

只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:

const char* cp;
char *q = static_cast<char*>(cp);//错误:static_cast不能转换掉const性质
static_cast<string>(cp);//正确:字符串面值转换成string类型
const_cast<string>(cp);//错误:const_cast只改变常量属性

const_cast常常用于有函数重载的上下文中。

III). reinterpret_cast

 reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。eg:

int *ip;
char *pc = reinterpret_cast<char*>(ip);

 我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。eg:

string str(pc);

可能导致异常的运行时行为。

posted @ 2016-08-16 11:11  vsxw  阅读(322)  评论(0编辑  收藏  举报