C++ Primer 学习笔记——第四章
第四章 表达式
前言
本章主要介绍:语言本身定义、并用于内置类型运算对象的运算符。简单介绍:几种标准库定义的运算符。
表达式本身由一个或多个运算对象组成,其目的是得到一个结果。
表达式的结构:value operator n*(operation object)
通过运算符将一个或多个运算对象结合起来便组成了表达式!
4.1 基础
基本概念
一元运算符(unary operator)、二元运算符(binary operator)和三元运算符(ternary operator)
解释:
- 一元运算符:作用于一个运算对象的运算符,例如:取地址符(&)和解引用符(*)
- 二元运算符:作用于两个运算对象的运算符,例如:相等运算符(==)和乘法运算符(*)
- 三元运算符:作用于三个运算对象的运算符,格式为:condition ? expression1 : expression2
补充
函数调用也是一种特殊的运算符,只不过它对运算对象数量没有限制。
对于能够充当多种元运算符的运算符,根据上下文判断其属于哪种元运算符,例如:*。
重载运算符
在这里简单介绍一下重载运算符,重载运算符其实就是对已存在的运算符赋予另一层含义,例如:IO库中的“<<”和“>>”运算符。
重载运算符会改变运算对象的类型和返回值的类型,但是无法改变运算对象的个数、运算符的优先级和结合律。
左值和右值
具体查看代码
优先级和结合律
关键点:
- 优先级和结合律决定了运算对象组合的方式
- 括号无视该规则
- 表达式依赖于子表达式的结合方式
- 高优先级的运算对象比低优先级的运算对象更为紧密的组合在一起
- 算术运算对象满足左结合律
示例:
int a=1+2*4/2+6; /* 11 */
int b=(1+2)*4/2+6; /* 12 */
求值顺序
对于函数的调用来说,求值顺序的不同很有可能会产生不同的结果,甚至是错误!
例如:
int test_number = 1;
int test_evaluationOrder_test_1() { return test_number++; }
int test_evaluationOrder_test_2() { return test_number += 3; }
/**
* 求值顺序的探讨
*/
void test_evaluationOrder() {
int i = 0;
/**
* <<运算符并未明确规定何时以及如何对运算对象进行求值,没有明确的执行顺序,那么该表达式就是错误的。
* 因为您无法确定是i先执行还是++i先执行,得到的答案不论如何都是错误的
* */
std::cout << i << " " << ++i << "\n";
/**
* 同理:这下面一句也是错误的。
* 你无法判断是test_1()先执行还是test_2()先执行。
*/
int number = test_evaluationOrder_test_1() * test_evaluationOrder_test_2();
std::cout << number << "\n";
}
事实上,除了以下四种运算符明确规定运算对象的求值顺序,其他运算符并没有明确说明:
- 逻辑与(&&)运算符
- 逻辑或(||)运算符
- 条件(?:)运算符
- 逗号(,)运算符
这就导致一个问题:这些函数的调用顺序没有明确规定,那么如果某几个函数会影响到同一个对象,那么必然导致结果的不确定性。
建议
对于复合表达式而言,有两条经验之谈:
拿不准最好用括号括起来。
如果已经改变某个对象的值,那么表达式中就不能再调用能够影响该值的函数。
4.2 算术运算符
算术运算符分别有:
- + - ,一元正号(负号)
- * / % ,乘法、除法、取余
- + -,加号、减号
算术运算有时候也会产生未定义的结果,例如除数是0、数据溢出等。
在取余这里,C++ 11定义有:
- m%(-n)=m%n
- (-m)%n=-(m%n)
具体示例:
21%6=3; 21/6=3;
21%7=0; 21/7=3;
-21%-8=-5; -21/-8=2;
21%-5=1; 21/-5=-4;
4.3 逻辑和关系运算符
关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。二者返回值皆为布尔类型。
其含有:
- !,逻辑非
- < <= > >=,小于、小于等于、大于、大于等于
- == !=,等于、不等于
- &&,逻辑与
- ||,逻辑或
注意
在相等性测试和布尔字面值上,如果想要测试一个算术对象或指针对象的真值,最好的方式是用if语句:
if(value)
,在这种条件下,编译器会自动将算术对象转换为布尔值并进行判断。注意:不要使用
if(value==true)
的方式进行判断,原因:①、该写法不直观;②、如果value不是布尔值则失去比较意义:如果value不是布尔值,那么首先将true转换为value的类型,如果value不是布尔值,则转换为if(value==1)
,这样的比较是没有意义的。综述,如果进行比较运算,如果比较对象不是布尔类型,请不要使用布尔字面值作为运算对象参与比较。
4.4 赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。
赋值运算满足右结合律,例如:
int rval,lval;
rval=lval=0; /* rval和lval的值均为0 */
lval作为靠左赋值运算符的右侧运算对象,赋值运算返回其左侧运算对象,所以靠右的赋值运算的结果被赋值给rval。
4.5 递增和递减运算符
递增和递减运算符有前置和后置两种方式,其原理:
前置:将对象本身加/减1后作为左值返回;
后置:将对象原始值的副本作为右值返回,后再进行对象本身的加/减1;
建议
除非必要,在使用递增和递减运算符时一律使用前置方式,使用后置方式会导致不必要的资源浪费,同时前置方式更符合编程初衷。
4.6 成员访问运算符
成员访问运算符的作用就是访问成员(好像是一句废话😂),成员访问运算符有两种:点运算符(.)和箭头运算符(->)。二者关系:pointer->member等价于(*pointer).member。
对左值和右值的讨论:
箭头运算符作用于指针类型的运算对象,结果为左值;点运算符取决于作用的运算对象的左值还是右值,依据运算对象的左值右值返回结果。
注意
虽然我们在前面我们讨论了箭头运算符和点运算符的关系,但是我们需要注意到点运算符的优先级高于箭头运算符,所以在进行等价转换时一定要记得对指针添加括号:“(*pointer).member”,否则语句无法通过编译。
4.7 条件运算符
条件运算符可以这样理解:将if-else逻辑嵌入到单个表达式中。
其结构为:Judgment_Expression ? result1 : result2;
首先执行语句(“Judgment_Expression”),若结果为真,执行result1并返回结果;否则,执行result2并返回结果。
若result1和result2都是左值或能转换为一种左值类型,其运算结果为左值,否则为右值。
条件运算符满足右结合律。
由于条件运算符的优先值非常低,所以在使用时请加上括号!
4.8 位运算
在这里引用本书对位运算做出的解释:
位运算作用于整数类型的运算对象,并把与运算对象看成是二进制位的集合。位运算符提供检查和设置二进制的功能。同时位运算符也能用于bitset类型对象。
一般来说:如果位运算的运算对象为“小整数”将自动提升为整数类型。位运算的对象可以是带符号的,也可以是不带符号的。但是如果处理带符号且为负数的运算对象,那么其处理操作依赖于机器,所以
强烈建议位运算符用于处理无符号类型对象
位运算符有:
运算符 | 功能 |
---|---|
~ | 位求反 |
<< | 左移 |
>> | 右移 |
& | 位与 |
^ | 位异或 |
| | 位或 |
首先介绍左移右移位运算符:
格式为:expr1 << expr2 或者 expr1 >> expr2
左侧运算对象作为被位移对象,在完成位移后以其对象拷贝作为求值结果。右侧对象作为位移操作要求,其对象值必须小于左侧对象值,且不能为负数。
无论是左移还是右移,移出的位都会被舍弃掉。
左移运算符(<<)是在右侧插入0,右移运算符(>>)根据是否为无符号数判断,若为无符号数,在左侧插入0;若为有符号数,在左侧插入符号位的副本或值为0的二进制位(其根据具体环境判断)。
位求反运算符:
将运算对象的每一位都取反。
位与、位非、位或运算符:
三者都是根据两个运算对象的对应位进行比较,最终依照比较内容返回结果。
- 未与:若二者都为1,则为1。
- 位或:若二者至少一个为1,则为1。
- 位异或:若二者仅有一个为1,则为1。
移位运算符满足左结合律。
4.9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数。其返回值为size_t类型(一种机器相关的无符号类型)的常量表达式。
sizeof本身满足右结合律。
其有两种形式:
sizeof (type)
sizeof type
第二种形式返回的是表达式结果类型的大小。
这里引用书中的Sales_data类型:
Sales_data data,*p;
sizeof(Sales_data); /* 存储Sales_data类型的对象所占的空间大小 */
sizeof data; /* data的类型大小,等价于上面的表达式 */
sizeof p; /* 指针所占的空间大小 */
sizeof *p; /* p多指向的对象类型的空间大小,等价于第二个表达式 */
sizeof data.revenue; /* Sales_data的revenue成员对应类型的大小 */
有关sizeof的部分还有许多特点,详细可见书P139页。
4.10 逗号运算符
逗号运算符(comma operator)含有两个运算对象,按照从左往右的顺序依次求值。
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃。真正的结果是右侧表达式的值,如果右侧运算对象为左值,结果也为左值。
4.11 类型转换
隐式转换(implicit conversion),由编译器自动执行,无需程序员介入或了解的类型转换,在下面的情况中,编译器将会进行该转换方式:
- 在大多数情况下,比int类型小的整数值首先提升为较大的整数类型。
- 在条件中,非布尔类型转换为布尔类型
- 初始化对象中,初始值转换为变量的类型;在赋值语句中,右侧运算对象转换城左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时会发生类型转换。
算术转换
算术转换规则为:将运算符的运算对象转换成最宽的类型。例如:运算对象类型有:int、double,那么将转换为double。
转换顺序为:首先进行整型提升、之后对符号位进行判断(若某个运算对象的类型是无符号类型,则转换结果依赖于机器中各个整数类型的相对大小)。
显示转换
显示转换又称为强制类型转换(cast),其本身使用是十分危险的。
强制类型转换的格式:
cast-name <type> (expression);
/* cast-name表示转换方式,type表示转换的目标类型,expression表示要转换的值。如果type为引用类型,其结果为左值。 */
,其中cast-name有四种:static_cast,dynamic_cast,const_cast和reinterpret_cast。
static_cast
对任何具有明确定义的类型转换(不包含底层const),均可使用该转换方式。
例如:
int j=0;
double i=static_cast<double>(j);
在之前如果将较大的运算对象转换为较小的运算对象,编译器将会报错或警告,但是执行显式类型转换,警告信息将会被关闭(编译器默认程序员已经知道这样做的坏处结果)。
const_cast
const_cast只能改变运算对象的底层const。
如果对象本身不是常量类型,使用强制类型转换获得写权限是合法的,但是如果对象为常量,使用const_cast执行写操作就会产生未定义的后果。
const char *pc;
char *p=const_cast<char *>(pc); /* 正确,但是通过p写值是未定义的 */
只有const_cast能够改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。
reinterpret_cast
reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。
示例:
int *ip;
char *cp=reinterpret_cast<char *>(ip);
/* 在这里,本来cp存放的是char类型的指针,经过强制类型转换,其存储的是int类型指针 */
警告
使用reinterpret_cast是十分危险的事情(实际上显示转换本身就是危险的事情)。因为该显示转换改变的是对象本身的类型,编译是无法给出任何警告和错误提示的!,如示例,在编译看来cp本身存储的应当是char类型指针,实际上存储的是int类型指针。
最后,做出警告!
应当避免使用强制类型转换,因为这样做会干扰正常的类型检查!
关于旧式类型转换的讨论:
type (expression) /* 函数形式的强制类型转换 */
(type) expression /* C语言风格的强制类型转换 */
在笔者看来,旧式强制类型转换比现今使用的强制类型转换更加难以追踪!
当我们使用旧式强制类型转换时,编译器会根据合法性进行判断:
如果,该处的强制类型转换能够使用const_cast和static_cast代替,那么其强制类型转换的行为与对应的现今强制类型转换一致;如果不合法,那么该强制类型转换将会执行reinterpret_cast类似功能,同时其效果与reinterpret_cast一致。
旧式的强制类型转换更为模糊,更加难以查找bug和跟踪对象。