c++学习笔记之表达式
问题
- 左值与右值的不同
- 左值与右值对 decltype 的影响
- 整数除法的结果是如何舍入的
- 理解运算符的返回值
- 区分递增运算符的前置与后置版本。
- 位运算符的使用要注意什么
- 运算符的结合顺序是怎样的
- sizeof 运算符的返回值是什么
- 常见的隐式类型转换的方式
- 四种显示类型转换是什么
- 常见的如 int 转换为 double 这样的转换用什么
- const_cast 用于什么时候
- 旧式的两种强制类型转换方式
- 需要记住的典型运算符的优先级
回答
- 使用左值使用的是对象的身份(在内存中的位置),使用右值使用的是对象的值(内容)。需要右值的地方可以用左值代替,反之不行。
- 如果 decltype 中的表达式返回的是左值,那么 decltype 得到是一个引用类型。
- 向零舍入
- 采用运算符进行各类运算时,把运算符理解为函数,返回值是运算符函数的返回值,cin>>10 的返回值是运算符 >> 的返回值
- 前置版本直接返回改变后的运算对象,后置版本返回的是运算对象改变前的原始值的副本。对迭代器使用后置递增消耗很大。
- 建议使用位运算符处理无符号类型。C++中使用位运算符处理带符号数的运算结果依赖于机器。
- 如果优先级相同,除了赋值运算符,其他运算符都是从左向右结合。
- sizeof 也是运算符,它返回一条表达式或一个类型所占的字节数。
- 算数运算中小整型(bool,char,short)提升为大整型(int)、整型转换为浮点型,条件里非布尔值转换为布尔值,初始化时初始值转换为变量的类型,赋值时右侧转换为左侧类型,算术运算或关系运算的运算对象被转换为同一种类型。数组和函数转换为指针,指向非常量的指针转换为指向常量的指针,c风格字符串转换为string,cin>>s 返回的的 cin 转换为 bool 值。
- static_cast(expression), const_cast, dynamic_cast, reinterpret_cast(不要用它)。
- 使用 static_cast(num) 来转换。
- const_cast 用于移除或增加对象的底层 const,即用于使不能修改引用对象值的引用和不能修改指向对象值的指针变成可以修改的,或者反过来。
- int(a) 和 (int)a。两种都可以,但是建议使用新式的代替。
- 作用域运算符优先级是最高的,成员访问运算符(. 和 ->)的优先级高于括号,也是非常高的。递增和递减的优先级也很高,是括号之下最高的。
第4章 表达式
4.1 基础
4.1.1 基本概念
表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果。
字面值和变量是最简单的表达式。
把运算符和运算对象结合起来可以生产较复杂的表达式。
一元运算符:作用域一个运算对象的运算符,如取地址符(&)和解引用符(*)
二元运算符:要求两个运算对象的类型相同或可以转换为同一种类型。
小整型(bool, char, short)通常会被提升为大整型,主要是 int。
运算符重载:当运算符作用于类类型的对象时,用户可以自行定义含义,即重载运算符。
例:IO库的>>和<<,string对象、vector对象和迭代器使用的运算符
左值和右值
C++表达式要么是左值,要么是右值
C语言:左值可以位于赋值语句的左侧、右值不能。
C++语言中,要复杂得多
右值:取不到地址的表达式
左值:能取到地址的表达式
常量对象为代表的左值不能作为赋值语句的左侧运算对象
某些表达式的求值结果是对象,但他们是右值
当一个对象被作用右值的时候,用的是对象的值(内存中的内容)
当一个对象被当做左值的时候,用的是对象的身份(内存中的位置)
通常情况:左值可以当成右值,实际使用的是它的内容(值)。不能把右值当成左值(也就是位置)
取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值
1 | 如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。例如,对于int *p : |
运算符对于作用对象是左值还是右值会有要求,比如赋值运算符的左侧运算对象必须是左值。
4.1.2 优先级与结合律
表达式 的计算结果,依赖运算符的优先级(precedence)、结合律(associativity) 以及运算对象的求值顺序(order of evaluation)
左结合律:如果运算符优先级相同,按照从左向右的顺序组合运算对象。大部分二元运算符满足左结合律。
4.1.3 求值顺序
4种运算符明确规定了运算对象的求值顺序:逻辑与(&&)、逻辑或(||)、条件(?:)、逗号(,)
处理复合表达式的两个建议:
- 不确定优先级与结合律时使用括号
- 如果改变了某个运算对象的值,在同一表达式中不要再使用该运算对象。
4.2 算术运算符
算术运算符有 3 组,按优先级从高到低依次是
- +、- :一元正号与一元负号
- *、/、% :乘法、除法、求余
- +、- :加法、减法
注意一元正负号的优先级最高,求余也是一种算术运算。
一元负号运算符对运算对象值取负后,返回其(提升后的)副本
整数除法的结果是向零舍入。
求余运算符的运算对象必须是整数,运算结果始终与被除数符号相同
4.3 逻辑和关系运算符
- 逻辑运算符:!、&&、||。逻辑运算符的作用对象必须是能转换成布尔值的类型
- 关系运算符:<, <=, >, >=, !=, == :大于小于的优先级高于等于和不等于
逻辑运算符与关系运算符的求值结果都是布尔值。
逻辑与和逻辑或都是先求左侧对象的值再求右侧。也就是 && 和 || 两个运算符自带了个 if 的功能。
布尔字面值
使用算术值做条件时直接用,不要与布尔值做比较
1 | if(a);//正确 |
4.4 赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值
C++ 11 允许使用花括号括起来的初始值列表作为右侧运算对象。初始化列表可以为空,此时将进行值初始化。
赋值运算符满足右结合律。 赋值运算符优先级较低。
复合赋值运算符
1 | +=; -=; *=; /=; %=; <<=; >>=; &=; ^=; |=; |
位运算也可以使用赋值运算符。
复合赋值运算符只求值一次,而普通运算符需要两次。(a=a+1 要先求一次 a+1,再将结果赋值给 a)
4.5 递增和递减运算符
1 | int i = 0, j; |
如无必要,不要使用后置版本。
在一条语句中混用解引用和递增运算符
*p++ :p++将 p 的值加一,然后返回 p 的初始值的副本作为求值结果用于解引用。(递增运算符优先级高于解引用)。推荐使用这种写法。
1 | auto pbeg = v.begin(); |
4.6 成员访问运算符
点运算符和箭头运算符都可以用来访问成员。
ptr->mem 等价于 (*ptr)->mem
4.7 条件运算符
cond ? expr1 : expr2
可以使用嵌套条件运算符
1 | finalgrade = (grade>90) ? "high pass" : (grade<60) ? "fail" : "pass"; |
条件运算符优先级非常低,通常都要加括号
4.8 位运算符
位运算符作用于整数类型的对象。
位运算符有六种:位求反、位与、位或、位异或、左移、右移。
1 | ~a; a&b; a|b; a^b; a<<2; a>>2; |
如果运算对象是“小整型”,值会被自动提升为较大的整数类型。
运算对象可以带符号,也可以不带符号。不带符号的运算结果是固定的,带符号的运算结果依赖于机器。
左移操作处理带符号值是一种未定义的行为。
在 C++ 中,建议仅用位运算符来处理无符号类型。
移位运算符
使用移位运算符,移动的位数必须严格小于结果的位数,否则会产生未定义的行为。
<< 运算符在右侧插入 0,左侧移动超出边界的值舍弃掉。
>> 运算符处理无符号数时在左侧插入 0,右侧移动超出边界的值舍弃掉。
>> 运算符处理有符号数时可能在左侧插入 0 也可能插入符号位的副本,由机器决定使用哪种方式。
标准 IO 库所使用的 << 和 >> 都是重载版本。
移位运算符满足左结合律,以下两种等价。
1 | cout << a << b << endl; |
位求反、位与、位或、位异或
这几种运算符处理 char 时,都会把 char 类型的运算对象首先零扩展提升成 int 类型再进行位运算。
在写代码时,考虑到可移植性,选取数据类型应考虑到某一类型的最大字节和最小字节,比如 int 的最小位是 2 字节。
4.9 sizeof运算符
sizeof 是一个运算符。
sizeof 返回一条表达式或一个类型名字所占的字节数,值为 size_t 类型。
1 | sizeof(type) |
对数组执行 sizeof 运算符得到的是整个数组所占空间的大小。不会把数组转换为指针来处理。
但是对指针执行 sizeof 运算符得到的是指针类型的大小,也就是 8。
对 string 或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中的元素占了多少空间。
可以用 sizeof 获得数组中元素的个数:
1 | sizeof(arr)/sizeof(*arr);//返回的是数组 arr 的元素数量 |
4.10 逗号运算符
在 for 循环中可以用逗号分隔两个不同的条件
1 | for(int i=0; i!=n; i++,j++) |
注意不要在判断条件那里使用逗号分隔不同的条件,那样只会返回逗号分隔的最后一个表达式的值。
4.11 类型转换
c++ 不会直接将两个不同类型的值相加,会先通过类型转换把运算对象的类型统一后再求值。
隐式类型转换****的发生场景
- 比 int 类型小的整型值首先提升为较大的整数类型
- 在条件里,把非布尔值转换成布尔值
- 初始化过程中,初始值转换为变量的类型
- 赋值时,右侧运算对象转换成左侧类型
- 算数运算或关系运算的运算对象有多种类型,转换成一种。
4.11.1 算术转换
运算符的运算对象将转换成所有运算对象中最宽的类型。如果表达式中既有整型也有浮点型,一般会把整型转换为浮点型。
1 | 3.14159L + 'a';//先将'a'提升成 int,然后把 int 转换成 long double |
整型提升
整型提升把小整数类型(包括 char、bool等)转换成较大的整数类型。如果 int 可以就转换成 int,否则提升成 unsigned int 类型
无符号类型的运算对象
如果一个是无符号一个带符号。如果无符号类型不小于带符号类型(比如都是 4 字节),则带符号转换为无符号
如果无符号类型小于带符号,转换结果依赖机器。尽量避免。
4.11.2 其他隐式类型转换
数组转换成指针
大多数情况下数组自动转换成指向数组首元素的指针。(decltype关键字参数、取地址符(&)、sizeof、typeid 都不会发生这种转换)
指针的转换
0 或 nullptr 都能转换成任意指针类型。指向非常量的指针能转换成 **void***。指向所有对象的指针都能转换成 **const void***。
转换成常量
指向非常量的指针转换成指向相应常量类型的指针
类类型定义的转换
1 | while(cin>>s);//将 cin 转换为 bool 值 |
4.11.3 显式转换
显示转换即使用强制类型转换。
强制类型转换非常危险,尽量避免使用。如果使用了,应反复思考是否可以用其他方式代替。
1 | castname<type>(expression); |
castname 有四种:static_cast、dynamic_cast、const_cast、reinterpret_cast 。它指定了执行哪种转换。
static_cast
任何类型转换,只要不包含底层 const,都可以用 static_cast
1 | double slope = static_cast<double>(j)/i; //将 j 转换成 double 以便执行浮点数除法 |
当把较大的类型转换为较小的类型时,static_cast 很有用。这时它告诉读者和编译器:我们知道且不在乎精度损失。平时编译器会给警告,显式转换后就不警告了。
const_cast
const_cast 只能改变对象的底层 const。可以去掉或增加 const 性质。
只有 const_cast 能改变表达式的常量属性,其他都不行。
cosnt_cast 常用于有函数重载的上下文中。
1 | string& s; |
reinterpret_cast
它依赖于机器,使用门槛很高,且使用时充满风险,不要用它。
旧式的强制类型转换
1 | int(a);// 函数形式的强制类型转换 |
旧式的强制类型转换本质上采用 const_cast、static_cast 或 reinterpret_cast 的一种。
旧式与新式相比没那么清晰明了,如果出现问题,追踪困难。
4.12 运算符优先级表
1 | :: //作用域 |








