第四章是和表达式有关的知识,表达式是C++的基础设施,本章由三部分组成:
- 表达式概念基础,包括表达式的基本概念,左值和右值的概念,优先级结合律,求值顺序。
- 各种运算符,主要包括算数\关系\逻辑\赋值\递增递减\成员访问\条件\位运算\sizeof\逗号运算符 这10种运算符。
- 类型转换,包括隐式和显式两种转换的规则。
表达式基础
表达式的基本概念(P120,4.1.1)
表达式由一个或者多个运算对象组成,多个对象组成表达式时,对象之间用运算符连接形成复杂表达式。
运算符中,需要两个对象和运算符连接形成表达式的这种运算符叫做二元(双目,二目)运算符。
分析一个表达式,必须先了解运算对象的含义、运算符的优先级(precedence)、结合律(associativity)和运算符的求值顺序(order of evaluation)。
- 对于含有子表达式的复杂表达式,应该按照求值顺序,看看应该先求哪一个子表达式的值。
- 对于不那么复杂的子表达式,应该按照优先级,查看表达式中的每个操作数(对象)应该先跟那一个运算符在一起运算。
- 如果有优先级相同的运算符同时在同一个运算对象左右,应该按照结合律选定结合顺序是从右向左还是从左向右计算表达式的值。
左值和右值(P121,4.1.1)
起源:左值和右值原来是C语言中的概念,特指赋值运算符左右两段的表达式。C语言中,能放在赋值运算符左侧被赋值的对象就是左值,反过来在赋值运算符右侧的对象就是右值。C++中的这两个概念的词义发生了改变。
概述:可以暂时概述一下C++中左值和右值的概念。从性质上来看,当一个对象做右值时,我们使用的是这个对象的内容(值);当一个对象做左值时,我们使用的是它对象的身份(在内存中的位置)。
应用:表达式中有的位置需要的是左值,有的位置需要的是右值。表达式的值本身也有左右的分别。
赋值运算符中左侧操作数和表达式结果都是左值。
取地址符的操作对象是左值,得到的是右值。
解引用、下标运算符的求值结果是左值。
decltype作用于表达式时,如果表达式的结果是一个左值,decltype会返回一个引用类型。
优先级和结合律(P122,4.1.2)
1.优先级 复杂表达式中一个运算对象连接多个不同运算符时,哪个运算符优先级高,就先计算哪个运算符和对象作用后的值。
2.结合律 复杂表达式中一个运算对象连接多个优先级相同的运算符时,根据这一优先级对应的结合律,按从右至左或者从左至右的顺序计算表达式的值。
如3+2*4-7;
这个表达式是一个复杂表达式,因为表达式里*
号优先级比较高,所以先计算2*4
,得到3+8-7
;得到的新表达式更简洁了,只剩下+-两个符号,这两个符号优先级相同,因此查看这个优先级对应的结合律可知这一级别的符号满足左结合性。因此从左向右计算,得到11-7
;进一步得到结果4
。
括号无视优先级和结合律,可以考虑多使用括号。
求值顺序(P123,4.1.3)
一个表达式里如果运算对象都是函数返回的,都需要计算求值才知道对象的状态,函数调用符号优先级一致,中间隔着几个优先级低的其他符号连接操作对象,比如int a=f()+g();
,这时候是函数f()先被调用还是g()先被调用呢?答案是未定义。C++语法没有规定这种情况应该谁先谁后。
就像下面的表达式++i+i++
这个表达式中,优先级最高的表达式++i
和i++
中间隔着优先级低的运算符+
,关于++i
先计算还是i++
先计算,这是未定义的,而因为这个表达式先计算++i
或先计算i++
的结果不同,所以这条表达式是错误的。一个变量如果在同一个表达式里被多次改变,这个表达式的求值顺序又不一定,就会出现二义性。应该避免这样的写法。
目前只有四种运算符明确规定了求值顺序。
- 逻辑运算符
&&
和||
(P126):这两个运算符先计算左边操作数的值。 - 条件运算符
?:
(P137):条件运算符先计算?前的表达式,并求值,之后对视情况对:左右侧的表达式求值。 - 逗号运算符
,
(P140) :这个运算符的求值顺序是从左至右。
处理复合表达式的两点建议:
①拿不准的时候最好用括号来强制让表达式的组合关系复合程序逻辑的要求;
②如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。例外:当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。
关于运算符,左值和右值的归纳
本章各种运算符形成的表达式所返回的值的属性和运算符需要的操作数的属性如下:
- 算数/逻辑/位运算符: 操作对象和结果都是右值
- 赋值运算符:左侧的操作对象必须是可以修改的左值,右侧的操作对象是右值,返回一个左值。
- 递增/递减运算符: 前置版本的++/–返回左值,后置版本的++/–返回右值。操作对象都必须是左值。
- 箭头成员访问运算符: 作用于指针,表达式结果是一个左值。
- 点成员访问运算符: 这个成员所属的对象是左值,结果就是左值;这个成员所属的对象是右值,结果就是右值。
- 条件运算符: 条件运算符的三个表达式都是左值或者都能转化成左值类型时,结果为左值;否则是右值。
算术运算符(P124, 4.2)
除法和取模的结果 (P125,4.2)
两个非浮点型变量/字面值相除,结果还是原来的类型,不会有原来操作数是整数,运算之后结果是小数的情况。
C++11中, 对于除运算符,结果向零取整(直接切掉小数部分,得到的数就是结果)。对于取模运算符,结果的符号和被除数的符号一致。(之前的语法标准里除法的结果可以选择是否向零取整,求模(模就是余数)运算可选符号)
(-m)/n = -(m/n); m/(-n) = -(m/n);
m%(-n) = m%n; (-m)%n = -(m%n);
成员访问运算符(P133, 4.6)
点运算符和箭头运算符都可以获取类对象的一个成员,ptr->mem
等价于 (*ptr).mem
。
解引用运算符的优先级低于点运算符,所以(*ptr).mem
中的括号不能省略,否则出错。
条件运算符(P134, 4.7)
条件运算符的格式:
条件运算符允许嵌套。
位运算符(P136,4.8)
bitset
的标准库类型可以表示任意大小的二进制位集合。
关于符号位没有明确的规定,因此强烈建议仅将位运算符用于处理无符号类型。
左移运算符移动二进制数后会在右侧插入零,右移运算符在处理有符号类型的操作数(尤其是带负号的)时具体行为由环境决定。
运算符 | 功能 |
---|---|
~ | 位求反 |
<< | 左移 |
>> | 右移 |
& | 位与 |
^ | 位异或 |
| | 位或 |
【写博客相关】表格中竖号的打法:
|
,或者中文格式的丨
(输入法输入“shu”查找)
移位运算符(也叫IO运算符)满足左结合律。
sizeof运算符(P139,4.9)
sizeof运算符有两种用法:
第一种是sizeof后面直接加一条表达式语句;第二种形如sizeof (类型名);
第二种形式后会得到该类对象所占空间的大小。
第一种形式中,如果表达式是指针类型,sizeof运算符会返回指针本身的大小。当有一个类名叫data,类中有一个成员叫做student时,可以使用作用域标识符和sizeof联动,使用sizeof(data::student);
就可以计算出student占字节数。
隐式类型转换(P141,4.11)
概述:在C++中,一些类型可以按照一定规则互相转换,很多时候语境中需要使用两个或多个相同的类型才能继续运算。因此这时一种类型的值会被自动转换成另一个类型的值。这个过程就是隐式转换,其中算术隐式转换较为常见。
主要的隐式转换发生的情况:
- 大多数表达式中,比int小的类型会被提升为int型。
- 在条件中,非布尔值要转化成布尔值。
- 在初始化和赋值语句中,赋值符号的右侧对象的类型转换成左侧对象的类型进行运算。
- 算术/关系运算中对象有有多种类型的,转化成同一类型。
- 形参转化为实参的类型(第六章)。
- 数组名会被转换为指针。
- 0,nullptr会转为任何类型的指针。任何类型的指针都可以转化为(const)void *类型。
算数转换时发生隐式转换的补充:
在算术运算符的作用下,不同的操作数要转换成同一个类型才能够进行计算。以i+a;
这个表达式举例,了解算术转换的方式。
- 首先,当i和a的类型占字节比int小,如
char、short
,把他们转换为int型。如果他们原来类型的最大值在当前系统里大于int型最大值,则转化成unsigned int
型。 - 之后,如果i和a的类型相同,结束算数隐式转换,若i和a的类型不同,把占字节少的类型的对象转成占字节多的类型的对象。
- 如果占字节多的带符号类型的最大值小于占字节少的带转换对象的最大值,带符号类型将被转换为无符号类型。
显式转换(P144,4.11.3)
显式转换就是强制类型转换(cast)。
一个命名的强制类型转换具有以下形式:cast-name<要转换成的类型> (被转换的值);
其中,cast-name是四种强制类型转换:static_cast、dynamic_cast、const_cast
和reinterpret_cast
之中的一种。
static_cast
用于常见的强制类型转换。只要两个类型有关联,比如浮点数类型和整数类型,整数类型和布尔值类型,布尔值类型和指针类型,就可以使用static_cast。只是不能转换常量const到变量。const_cast
用于去掉(或者加上)对象的底层const,要转换的类型和转换的类型都必须是指针或者引用类型。常用于将在第六章介绍的函数重载。当然,这个重载只能针对指针或者引用类型。reinterpret_cast
依赖机器,是强行改变一个类型到另外一个不相干的类型。dynamic_cast
支持运行时类型识别,在19章(P730)将会提到。
建议:避免强制类型转换。
运算符优先级列表的规律(P147,4.12)
优先级和结合性是第四章的重要内容,因此第四章之后给出了完整的优先级和结合性的参考表。这里是有一定的规律的。
- 首先优先级最高的运算符都有这样的属性:单独拿出这个运算符左面的操作数和右面的操作数都没有意义。即运算符本身是连接两个名字组合一个概念的连接器。比如优先级最高的运算符::(作用域运算符),优先级比较高的点运算符(成员选择)下标运算符[]。
- 比连接不同名字形成概念的这种运算符稍微低一级别的就是计算对象本身的运算符,比如++,–,类型转换,位求反,逻辑非,解引用,取地址,求类型占的字节数这些运算符大多都是单目元素符,他们的运算目的一般是根据操作数本身的属性进行计算或者改变操作数本身。
- 算术运算符。
- 逻辑运算符。
- 条件运算符。
- 赋值运算符 。
- 复合赋值,抛出异常,逗号运算符等。
术语表(P149)
1.运算对象 operand
2.结果 result
3.一元运算符 unary operator
4.二元运算符 binary operator
5.优先级 precedence
6.结合律 associativity
7.求值顺序 order of evaluation
8.提升 promoted
9.重载运算符 overloaded operator
10.右值 rvalue
11.左值 lvalue
12.复合表达式 compound expression
13.短路求值 short-circuit evaluation
14.高位 high order position
15.逗号运算符 comma operator
16.相互转换 conversion
17.隐式转换 implicit conversion
18.算术转换 arithmetic conversion
19.整型提升 intergral promotion
20.运算对象 operand
语句简介
第五章是和语句有关的知识,语句也是C++的重要组分,本章由三部分组成:
- 语句的概念,包括简单语句和语句作用域的概念。
- 条件/循环/跳转语句,条件语句主要包括if/else语句、switc语句和?:表达式条件语句;循环语句则是for语句和while语句;跳转语句包括continue、break和goto语句。
- try/throw和异常处理,包括异常处理的使用方法。
语句作用域(P155,5.2)
用花括号括起来的块就是作用域的标志。在作用域中定义的对象只在作用域中起作用。块之外是没法访问和控制块内部的变量的。
尤其是在switch语句中,switch的执行过程可能跨过一些标签,当标签里声明并定义了一个对象,这个对象的作用域就延伸到了所有case标签里,如果case 1定义了int a;switch执行了case2,这时这个我们不想执行的语句却产生了自己的作用域,这显然是不行的。因此可以在case标签后使用大括号形成块,这样就不会出现作用域的问题。
goto也一样不能向前(代码的后几行)跳过对象的定义。不允许跨过变量的定义到达变量的作用域内。但是goto语句可以向后(代码的前几行)跳过定义。
不要在程序中使用goto语句,它会使得程序既难理解又难修改。
try语句块和异常处理(P172,5.6)
可以用try\throw和catch联动进行异常处理,形式如下:
throw抛出异常和catch处理异常的头文件都在stdexcept
里定义。抛出异常的语句形如:
异常类型一般只支持赋值,初始化,调用成员函数.what之类的几种操作。
术语表
1.控制流 flow-of-control
2.表达式语句 expression statement
3.空语句 null statement
4.复合语句 compound statement
5.悬垂else dangling else
6.case标签 case label
7.引发(异常) raise
8.catch子句 catch clause
9.异常处理代码 exception handler
10.异常安全(这不是特别安全的意思,而是在异常情况下也能保证程序执行预期的正确行为) exception safe
参考资料
- C++ Primer 中文版 第5版
- C++Primer第5版学习笔记(四、五)
- 用Markdown写Hexo博客时如何转义竖杠 | ?