《C++Primer》第四章——表达式
第四章:表达式
4.1 基础
- 表达式是由运算符和运算对象共同作用形成的运算结构,在程序执行过程中会通过计算得出特定的结果。最简单的表达式包括单个变量或常量数值以及由运算符连接的复杂组合体。
- 左操作数与右操作数在内存中的行为存在差异:左操作数始终指向其在内存中的具体存储位置(引用),而右操作数则直接引用其实际存储的内容(数据)。当使用decltype指令提取类型信息时,在这种情况下若计算结果为左操作数,则decltype指令将返回该类型的引用形式。
int *p;
//解引用运算符生成左值,所以得到的结果为 int&
decltype(*p);
//取地址运算符生成右值,所以得到的结果为 int**
decltype(&p);
3.复合表达式:指含有两个或多个运算符的表达式,对于含有多个运算符的复杂表达式来说,要理解它的含义首先要理解运算符的优先级 、结合律 以及运算对象的求值顺序
1)左结合律:若运算符优先级相同,则按照从左到右的顺序组合运算对象
2)求值顺序:优先级规定了运算对象的组合方式,但没说明运算对象按照什么顺序求值,在多数情况不会明确指定求值顺序;
对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并昌盛未定义的行为;
有4种运算符明确规定了运算对象的求值顺序:
逻辑与( && )运算符:规定先求左侧运算对象的值,只有当左侧运算符为真时才继续求右侧运算对象的值
逻辑或( || )运算符:对顶先求左侧运算对象的值,只有左侧运算对象为假时擦对右侧运算对象求值
条件( ?: )运算符:先对条件进行判断,为真对表达式1计算,为假对表达式2计算
逗号(,)运算符:按照从左向右的顺序依次求值
3)求值顺序、优先级、结合律:运算对象的求值顺序与优先级和结合律无关,e g:表达式 f() + g() * h() + j()
优先级规定:g() 的返回值和 h() 的返回值相乘
结合律规定:f() 的返回值先与 g() 和 h() 的乘积相加,所得结果再与 j() 的返回值相加
求值顺序:对于这些函数的调用顺序没有明确规定
注:如果f、g、h、j是无关函数,既不会改变同一对象状态,也不执行IO任务,那么函数调用顺序不受限制,反之若其中几个函数影响同一对象则是错误表达式,将产生未定义行为
4.2 算术运算符
一元运算符的优先级最高;接着是乘法与除法;最后是加减。
该算术运算符遵循左结合特性;当多个运算符具有相同优先级时,则按从左到右的顺序进行计算。
所有的算术运算对象及其计算结果均为右值。
在进行表达式计算时(即在求值得前),小整数类型的参与运算的对象会被升级为较大的整数类型;经过所有必要的计算后(即所有参与计算的对象都完成转换后),它们才会统一转换为同一类型。
4.3 逻辑和关系运算符
- 操作数及其计算结果皆为右值;
采用明确的计算次序实现"与"和"或"操作;
所有关系操作符均遵循左结合规则。 - 在执行比较操作时,默认情况下仅允许对非布尔类型的变量进行比较;若需使用布尔常量 true 或 false 作为操作数,则必须确保被比较的对象本身为布尔类型。
if(val) {} //若val是任意非0值,条件为真
if(!val) {} //若val是0,条件为真
if(val == true) {} //只有当 val 为1时为真
4.4 赋值运算符
1.赋值运算符的左侧运算对象必须是一个可修改的左值
int i = 0, j = 0, k = 0; //初始化而非赋值
const int ci = i; //初始化而非赋值
i + j = k; //错误:算术表达式是右值
ci = k; //错误:ci是常量(不可修改)左值
2.结合律:赋值运算符遵循右结合特性
优先级:赋值运算符具有较低的优先等级,在实际应用中,默认情况下赋值操作具有较高的执行效率
4.5 递增和递减运算符
1.前置版本和后置版本:前置版本运算符首先将运算对象加1(或减1),然后将改变之后的对象作为求值结果
后置版本运算符也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本
两种运算符都必须作用于左值运算对象
前置版本将对象本身作为左值返回
后置版本则将对象原始值的副本作为右值返回
注:除非必须,否则不用递增递减运算符的后置版本,因为前置版本的递增运算符避免了不必要的作用,它将值加1后直接返回改变了的运算对象,而后置版本需将原始值存储下来以便于返回这个未修改的内容,若我们不需要修改前的值则是一种浪费,因此应养成使用前置版本的习惯
2.运算对象可按任意顺序求值:大多数运算符没规定运算对象的求值顺序,一般没什么影响,但若一条子表达式改变了某个运算对象的值,另一条子表达式又要用该值的话,求值顺序就很关键了
*beg = toupper(*beg++); //错误:该赋值语句未定义
//编译器可能按照下面的任意一种思路处理
*beg = toupper(*beg); //如果先求左侧的值
*(beg + 1) = toupper(*beg); //如果先求右侧的值
4.6 成员访问运算符
在编程逻辑中,花括号展开操作符的运算顺序上较低,在进行花括号展开操作时应确保相关子表达式的前后部分正确连接,在该子表达式前后应添加括号以避免歧义的发生。这样的处理方式能够保证程序语义的有效传达,并使代码运行结果保持一致
(*p).size(); //正确
*p.size(); //错误,p是一个指针
4.7 条件运算符
- 当两个操作数均为左值或可转换为相同类型的左值时(即它们具有相同的左值类型),则该操作的结果也将被视为左值;否则结果将被视为右值
- 考虑到条件运算符的优先级较低,在一条包含嵌套使用了条件运算符的长表达式时(即该表达式的长度较长且包含多个嵌套的操作),通常需要在其两端加上括号符号以明确其执行顺序
cout << ((grade < 60) ? "fail" : "pass"); //正确,输出 fail 或者 pass
cout << grade < 60 ? "fail" : "pass"; //错误,因为试图比较 cout 和 60
4.8 位运算符
- 位操作器作用于整数值类型的参与操作元,并将其视为二进制数据集合。
当操作元为"小型整数值"时, 其数值会自动升级为更大容量的整数值类型, 比方说字符型变量会隐式转换为整数值类型。
符号位的具体处理方式尚未有明确统一的规定, 因此建议仅在无符号数据类型上使用位操作器。 - 移位算子在优先级上介于算术和加法减法之间, 其优先级高于关系、赋值以及条件逻辑算子。
结合性遵循左结合原则,
4.9 sizeof 运算符
该运算符返回的是表达式结果类型或类型名称占用的具体字节数量。
size (type)
size expr // 该指令返回的是表达式的大小信息
满足右结合规则。
所得值是一个 size_t 类型的常量表达式。
当应用于引用类型时,该运算返回被引用对象占用的空间信息。
解引用操作后进行 sizeof 运算得到的对象占用空间情况与指针的有效性无关。
对数组执行 sizeof 运算得到整个数组占用的空间较小,并等价于逐个元素计算后再求总和。
string 和 vector 对象进行 sizeof 运算仅能获取它们固定内存块的信息
Sales_data data, *p;
sizeof(Sales_data); //存储Sales_data类型的对象所占空间大小
sizeof data; //data的类型大小,即sizeof(Sales_data)
sizeof p; //指针所占的空间大小
//sizeof 和 * 优先级相同,而sizeof满足右结合律,所以等价于 sizeof(*p)
//由于 sizeof 不会实际求运算对象的值,所以即使p是无效(未初始化)的指针,依旧是安全的行为,因为指针没有实际的使用
sizeof *p;
sizeof data.revenue; //Sales_data 的 revenue 成员对应类型大小
sizeof Sales_data::revenue; //另一种获得 revenue 大小的方式
4.10 逗号运算符
- 涉及两个操作数,并明确了这些操作数的计算顺序,在整个过程中始终遵循从左到右的原则进行计算。
- 对于逗号运算符而言,在执行时会先计算左边表达式的取值,并舍弃该计算结果;最终的结果则由右边表达式的计算得出。若右边的操作数是左边类型的变量(即左值),则最终的结果也将保持为该变量类型。
int j = 10;
int i = (j++, j + 100, 999 + j); //最终结果为最右侧表达式的值,即1010
4.11 类型转换
1.C++语言不会直接将两个不同类型的值相加,而是根据类型转换规则设法将运算对象的类型统一后再求值,该类型转换是自动执行的,无需程序员介入,被称为隐式转换
2.在下述情况,编译器会自动地转换运算对象的类型
1)在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型
2)在条件中,非布尔值转换成布尔类型
3)初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
4)如果算术运算或关系运算 的运算对象有多种类型,需转换成同一类型
5)在第六章,函数调用也会发生类型转换
3.无符号类型的运算对象:若某个运算对象的类型是无符号类型,那么转换结果要依赖于机器中各个整数类型的相对大小
1)首先执行整型提升
2)若一个运算对象是无符号类型、另一个运算对象是带符号类型,而其中的无符号类型不小于带符号类型,则带符号类型转为无符号,但如果 int 类型恰好为负值,会带来副作用
若带符号类型大于无符号类型,此时转换结果依赖于机器,若无符号类型的所有值都能存于带符号类型中,则无符号类型的运算对象转换成带符号类型,如果不能,那么带符号类型转换为无符号类型
4.其他隐式类型转换:
1)数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针,但当数组被用作 decltype 关键字的参数,或者作为取地址符( &)、sizeof 及 typeid 等运算符的运算对象时,上述转换不会发生,同时,若用一个引用来初始化数组也不会发生
int ia[10]; //含有10个整数的数组
int *ip = ia; //ia转换成指向数组首元素的指针
2)指针对换:整型常数0或字面值nullptr可换至任何指针类型;非恒定变量地址可转为void*;对象地址可转为const void*
3)转布尔:算术或引用型变体有隐式至布尔型的操作
4)转常量:可将非恒定对象地址变更为对应目标类型的恒定地址引用;反向操作不可行因会删失const特性
int i;
const int &j = i; //非常量转换成const int的引用
const int *p = &i; //非常量的地址转换成const的地址
int &r = j, *q = p; //错误:不允许const转换成非常量
5)类类型定义的转换:类类型可定义由编译器自动处理的转换操作, 但编译器仅能对单个类类型进行相应的转换操作, 如果在同一时间提交多个不同的转换请求则会被系统拒绝。
string s, t = "a value"; //字符串字面值转换成string类型
//条件部分本来需要以一个布尔值,但是这里实际检查的是istream类型的值
//但IO库中定义了从istream到布尔值的转换,从而将cin自动转换成布尔值
//布尔值到底是什么由输入流状态决定,最后一次读入成功则布尔值为true,若读入不成功则布尔值为false
while(cin >> s) //while的条件部分把cin转换成布尔值
- 显示转换
int i, j;
//进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;
static_cast 对于编译器无法自动执行的类型转换也很有用
void *p = &d; //正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型,必须确保转换后所得类型就是指针所指类型,类型一旦不符将产生未定义后果
double *dp = static_cast<double*>(p);
2)const_cast :仅能作用于底层 const 对象,在这种情况下才能修改其常量特性;与之不同的是,在非const_cast的情况下修改常量属性都会导致编译错误;同样地,在无法通过这种方式更改类型时也应避免使用它。const_cast 常用于函数重载的情境中(参见第 208 页)
将一个常量对象转换为非恒定对象的行为被称为"取消 const 特性";一旦取消某个对象的 const 特性后,默认情况下编译器将不再支持对该对象进行写操作;如果该对象本身不是恒定对象,则通过强制类型转换获得写权是合法操作;然而如果该对象是恒定对象,则再尝试通过 const_cast 进行写操作将会导致未定义的结果。
const char *pc;
char *p = const_cast<char*>(pc);
const char *cp;
//错误:static_cast 不能转换掉const性质
char *q = static_cast<char*>(cp);
static_cast<string>(cp); //正确:字符串字面值转换成string类型
const_cast<string>(cp); //错误:const_cast只能改变常量属性
3)reinterpret_cast :reinterpret_cast 一般会将运算对象的位模式重新解读为低位层次上的信息。采用 reinterpret_cast 是非常危险的做法,并且只有当开发者深入理解其设计原理以及编译器实现的具体机制时才能安全地应用它。我的理解是,在这种情况下(处理不同类别的类型间的转换),会生成一个新的值,并且该值与原始参数具有相同的二进制位。
int *ip;
//pc所指的真实对象是一个int而非字符,若把pc当成普通字符指针可能会在运行时发生错误
//用一个int的地址初始化pc,由于显示声明这种转换合法,所以编译器不会发出任何警告或错误信息
//仅从语法上看无可指摘,但查找这类问题的原因十分困难
char *pc = reinterpret_cast<char*>(ip);
4)dynamic_cast :实现动态类型转换,在19.2节(第730页)将详细讲解其应用方法
【总结
