4.0 前言
- 表达式 是由一个或多个 运算对象(operand) 组成的。
- 字面值和变量是最简单的表达式。
4.1 基础
4.1.1 基本概念
- C++定义了 一元运算符(unary operator) 和 二元运算符(binary operator) ,除此之外还有一个三元运算符。。作用于一个运算对象的运算符是一元运算符,作用于两个运算对象的运算符是二元运算符。
- 运算符被C++定义了 作用于内置数据类型和复合类型的运算对象所作的操作 ,包括运算对象的类型和返回值的类型。
- 重载运算符(overloaded operator) :用户在设计类时,对已知运算符进行了新的定义,但是 运算符的优先级(precedence)和结合律(association)是无法改变的 。
- C++的表达式分为 右值(rvalue) 和 左值(lvalue) 。其中左值表示的是一个在内存中可以寻址的内存单元,而其它的表达式则为右值——也就是在内存中不能寻址,用汇编语言理解就是寄存器中的值、立即数。
✳✳✳✳✳✳✳✳✳✳✳(重要) - 重要原则 :在需要右值的地方可以使用左值替代,但是不能把右值当作左值来使用。因为在使用左值替代右值时,实际上使用的是左值所存储的数据。
- 使用关键字 decltype 时,如果子表达式是左值,则总表达式返回一个引用类型。
4.1.2 优先级与结合律
- 复合表达式(compound expression) 是指含有两个或多个运算符的表达式。
- 括号无视优先级与结合律。
- 先按优先级决定组合顺序,若优先级相同,则按结合律来决定组合顺序。
4.1.3 求值顺序
- C++在大部分情况下,不会明确指定运算对象按什么顺序求值。 比如
int i = f1() * f2();
,就不知道是哪个函数先调用。 - 对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
比如:
int i = 0;
cout << i << " " << ++i << endl;
编译器可能先求++i的值再求i的值,或者先求i的值,再求++i的值。
- 只有4种运算符明确规定了运算对象的求值顺序。
①逻辑与(
&&
)运算符:先求左侧运算对象的值,只有当左侧为真时才求右侧运算对象的值
②逻辑或(||
)运算符:先求左侧运算对象的值,只有当左侧为真时才继续求右侧运算对象的值
③条件(?:
)运算符:先求左侧运算对象的值,只有当左侧为真时才继续求右侧运算对象的值
④逗号(,
)运算符:先求左侧运算对象的值,只有当左侧为真时才继续求右侧运算对象的值
- 书写复合表达式的经验原则:
- 不确定表达式的组合和求值顺序时,使用括号来达成目的;
- 如果改变了某个运算对象的值,在该表达式的其他地方不要再使用这个运算对象。除非改变运算对象值的这个表达式同时正好是另一个表达式的子表达式,比如
*++iter
4.2 算术运算符
- 除非另作说明,否则算术运算符是可以用于任意算术类型,以及能转换成算术类型的数据类型。
- 算术运算符的运算对象和返回值都是右值。
- 在表达式求值前,小整型(比如说 bool 、 short 之类)的运算对象会被提升为较大的整型(一般是 int ) ,最后所有的运算对象会被转换成同一种算术类型。
- 除了算术类型外,一元运算符和加减运算符还可以作用于指针。
- 布尔值不应该参加算术运算 ,容易导致程序员产生逻辑上的错误,比如:
bool a = true ;
bool b = -a; //但是b还是true,不为false
因为在 -a 这个表达式求值前, a 先被隐式转换提升为整形 int ,值为1,然后取负-1不为0,然后转换为 bool 类型。此时表达式的值为 true 。
- 算术表达式可能会产生未定义的结果。比如结果超出数据类型范围大小的 溢出 ,可能会导致 环绕(wrapped around) 例如:
值为32767的 int 变量+1后值为 -32768
,也有可能会导致别的结果,甚至是系统崩溃。 - 两个整数相除(
/
)的结果还是整数,值是去除结果的小数部分后的整数。但若有一个运算对象为浮点数类型,则结果表达式结果为浮点数类型。 - 取余(
&
)运算符的两个运算对象都必须为整形(或可以隐式转换为整形)。 - 取余(
&
)运算的运算规则: 左边运算对象的符号为负号时,结果为负值。
4.3 逻辑和关系运算符
- 关系运算符作用于算术类型或指针类型,逻辑运算符作用于能转换为布尔值的类型。它们的返回值类型都为布尔类型。
- 对于逻辑和关系运算符,他们的运算对象和返回值都是右值 。
- 逻辑运算符:逻辑与(
&&
),当且仅当两个运算对象都为真时,表达式返回值为真;逻辑非(!
),表达式的返回值为子表达式的返回值取反;逻辑或(||
),当且至少有一个运算对象为真时,表达式返回值为真。 - 逻辑与(
&&
)和逻辑非(||
)运算符都是先求左侧运算对象的值,如果无法确定表达式的返回值( && 的左侧运算对象为真, || 的左侧运算对象为假),再求右侧表达式的值—— 短路求值(short-circuit evaluation) - 关系运算符用于比较运算对象的大小关系来返回布尔值。
- 关系运算符都满足左结合律。
4.4 赋值运算符
- 赋值运算符(
=
)的 左侧运算对象 必须是一个可修改的 左值 。 - 赋值运算符的返回值是它的左侧运算对象,并且是一个左值 ,所以表达式的返回值类型与左侧运算对象相同。
- 赋值运算符满足右结合律,优先级较低。
- C++11 允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。
①如果左侧运算对象是 内置类型 ,那么 初始值列表最多只能包含一个值 , 而且如果有丢失信息的风险,编译器将会报错。
比如:int a = {2.2}; //报错,因为2.2为 double ,转换为 int 会丢失信息
②如果左侧运算对象不为内置类型,则赋值运算的细节由类定义本身决定。 - 复合赋值运算符:
①算术运算符:+=、-=、*=、/=、%=、=、=
②位运算符:<<=、>>=、&=、^=、|=
4.5 递增递减运算符
- 递增递减运算符可以用于迭代器。
- 递增递减运算符都必须作用于左值运算对象。
- 前置版本将运算后的对象本身作为左值返回,后置版本将运算前对象的原始值副本作为右值返回。
- 如果没有使用的必要,不推荐使用递增递减运算符的后置版本,因为要保存运算前的对象的副本,内置类型可能没影响不大,但是对于复杂的迭代器则会消耗巨大。
- 优先级:递增递减运算符 > 解引用运算符
*temp++ 等于 *(temp++)
- 由于运算对象的求值顺序的不确定导致的问题:
*beg = toupper(*beg++); //错误,此语句是未定义的
不确定左边的 beg 是 beg 还是 beg+1
4.6 成员访问运算符
- 点(
.
)运算符和箭头(->
)运算符(也称成员运算符和间接成员运算符):成员运算符获取对象的一个成员;间接成员运算符与成员运算符相关,比如ptr->mem 等价于 (*ptr).mem
- ①间接成员(
->
)运算符的运算对象是一个指针类型的运算对象, 返回值是左值 。
②当成员(.
)运算符的运算对象为左值,则返回值为左值;若运算对象为右值,则返回值为右值。
4.7 条件运算符
- 条件(
? :
)运算符 :使用格式如下cond ? expr1 : expr2;
,当条件表达式 cond 为真,则执行表达式 expr1 并返回表达式值,否则执行表达式 expr2 并返回表达式值。 - 其中 expr1 和 expr2 是两个 类型相同或可能转换为某个公共类型的表达式 。
- 若表达式 expr1 和 expr2 都是左值或者可以转换为同一种左值类型时 ,则表达式
cond ? expr1 : expr2;
返回值为左值,否则返回值为右值。 - 条件运算符也可以嵌套使用。例如:
cond ? expr1 : cond ? expr2 : expr3;
由于条件运算符的结合律是自右向左,所以右边的cond ? expr2 : expr3
作为一个子表达式。
4.8 位运算符
- 位运算符作用于整型的运算对象,并把运算对象通过 二进制 的方式理解,并提供检查和设置二进制的功能。
运算符 | 功能 | 用法 |
---|---|---|
~ |
位求反 | ~ expr |
<< |
左移 | expr1 << expr2 |
>> |
右移 | expr1 >> expr2 |
& |
位与 | expr & expr |
^ |
位异或 | expr ^ expr |
| | 位或 | expr | expr |
- 位运算符的运算对象可以是有符号整型,但是位运算符关于符号位如何处理是未定义的,取决于机器环境。 所以建议用于处理无符号整型。
- 位运算符的运算对象如果是小整型,则会自动提升为较大整型。
- 移位运算符 :分为左移(
<<
)和右移(>>
),语法是将左侧运算对象 expr1 按照右侧运算对象 expr2 的要求移动指定位数,然后将左侧运算对象的值的副本作为求值结果。
要点:
① 右侧运算对象的值不能为负值,否则会产生未定义行为;
② 右侧运算对象的值要小于结果的位数,否则会产生未定义行为;
③二进制位或向左或向右移动,移出边界外的就被抛弃;
④左移运算符(<<
),从右边插入值为 0 的二进制位。右移运算符(>>
)的行为则取决于左侧运算对象的类型,如果是无符号类型,从左边插入值为0的二进制位,如果是带符号类型,在左侧插入符号位的副本或者值为 0 的二进制位,取决于系统环境;
⑤移位运算符满足 左结合律 。
- 位求反(
~
)运算符 :将运算对象逐位求反(将二进制上每一个的1转为 0 , 0 转为 1 )后,返回值是运算后新值的副本。 - 位与(
&
)、位或( | )、位异或(^
)运算符 :
①若(
&
)两侧运算对象同一位都是 1 ,则新值中该位为 0 ,否则为 0 ;
②若( | )两侧运算对象同一位至少有一个 1 ,则新值中该位为 1 ,否则为 0 ;
③若(^
)两侧运算对象同一位有且只有一个为 1 ,则新值中该位为 1 ,否则为 0 。
- 移位运算符的优先级都低于算术运算符,高于关系、赋值、条件运算符。
4.9 sizeof 运算符
-
sizeof
运算符返回一条表达式 expr 或一个类型名字 type 所占的字节数。其中类型名字 type 必须需要使用()
括起来。 -
sizeof
运算符符合右结合律。 sizeof
运算符的表达式返回值是一个sizeof_t
类型的常量表达式(const expression)。-
sizeof
运算符与右侧运算对象有两种结合方式:①sizeof (type)
②sizeof expr
;其中第二种结合方式返回的是表达式值的类型的大小。 - 注意:
sizeof
并不实际计算其运算对象的值 ,分为以下几种情况:
Sales_data data, *p; //定义一个 Sales_data 类型的变量和一个改类型的指针
sizeof (Sales_data); //存储 Sales_data 类型的对象所占的大小
sizeof data; // data 的类型大小,也就是 sizeof (Sales_data);
sizeof p; //指针所占的大小
sizeof *p; // p 所指的空间的大小,也就是 sizeof (Sales_data);
sizeof data.revenue; // Sales_data 的 revenue 成员的类型的大小
sizeof Sales_data::revenue; // 另一种获取 Sales_data 的 revenue 成员的类型的大小的方式
- 使用
sizeof *p;
是一种很安全的行为,因为sizeof
并没有实际计算运算对象的大小,所以即使 p 指针未初始化,在sizeof *p;
里解引用 p 也是一种安全的行为。 - C++11 允许我们使用作用域运算符
::
与sizeof
来获取类成员的大小,sizeof Sales_data::revenue;
,这样可以不需要创建一个该类型的新对象。 -
sizeof
运算符的结果部分地依赖于其运算对象的类型:
- char 或者返回值类型为 char 的表达式,结果为 1 ;
- 引用类型,结果是被引用对象所占空间大小;
- 指针类型,结果是指针类型所占的空间大小;
- 解引用指针,结果是指针所指向类型所占空间的大小,指针无需有效;
- 数组名,结果是整个数组所占空间的大小; 注意 :
sizeof
并不会将数组转换成指针来处理。- string 和 vector 类型的对象,结果是该类型固定部分的大小,而不会计算其实际所占的空间大小。
- 由于
sizeof
运算符返回的值是常量表达式, 所以可以用于声明数组的维度。
4.10 逗号运算符
- 逗号(
,
)运算符(comma operator) :有左右两个运算对象,按照从左向右的运算顺序依次求值。与逻辑与、逻辑或和条件运算符一样规定了运算对象的求值顺序。 - 逗号运算符先对左侧运算对象求值,然后丢弃结果。逗号运算符的返回值是右侧表达式的值,如果逗号运算符的右侧运算对象是左值,则返回值也是左值。
4.11 类型转换
- 如果两种类型可以 相互转换(conversion) 那么他们就是关联的。
- 隐式转换(implicit conversion) :根据类型转换规则, 自动地 执行类型转换。
- 在下面这些情况,编译器会自动地(隐式)转换运算对象的类型:
- 在大多表达式中,比 int 小的整型会被隐式转换为较大的整型;
- 在循环语句和 if 语句的条件表达式中,非布尔值会被隐式转换为布尔类型。
- 初始化过程中,初始值隐式转换为变量对应类型;在赋值语句中,赋值运算符的右侧运算对象隐式转换为左侧运算对象的类型;
- 如果算术运算或关系运算的运算对象有多种类型,需要最后转换成同一类型;
- 函数调用时有可能会发生隐式类型转换 。
4.11.1 算术转换
- 算术转换(arithmetic conversion) :将一种算术类型自动转换成另一种算术类型。也是一种隐式转换。
- 算术转换的规则 :其中运算符的运算对象将转换成范围最宽的类型。如果当表达式中既有浮点型又有整型,则将整型转换为浮点型。
- 整型提升(integral promotion) :负责将较小的整型提升为较大的整型。
对于 bool 、 char 、 unsigned char 、 short 、 unsigned short ,若上述类型的所有可能的值都能包含在 int 内(即 int 能表示数的范围包含了前面类型能表示的范围),那么上述类型在进行算术运算是就会转换为 int 类型,否则,它们会转换为 unsigned int 类型。
较大的 char 类型(wchar_t 、 char16_t 、 char32_t)提升成 int 、unsigned int 、 long 、 unsigned long 、 long long 和 unsigned long long 中最小的一种类型,前提是转换后的类型能容纳原类型的范围。
- 无符号类型的运算对象 :如果某个运算对象的类型是无符号类型,则转换的结果则取决于机器中各个整型的相对大小。
- 若两侧运算对象都为带符号类型或无符号类型,则较小的类型转换为较大的类型;
- 若两侧运算对象分别为带符号类型和无符号类型,且 无符号类型不小于带符号类型 ,那么带符号的运算对象转换成无符号的。
- 若两侧运算对象分别为带符号类型和无符号类型,且 无符号类型小于带符号类型 ,此时转换的结果取决于机器。如果该无符号类型的值能都存在该带符号类型中,则无符号类型的运算对象转换成该带符号类型,否则带符号类型的运算对象转换成该无符号类型。
4.11.2 其他的隐式类型转换
- 数组转换成指针 :在大多数表达式中,数组会自动转换成指向数组首元素的指针。
注意 :当数组被用作decltype
关键字的参数,或者作为取地址符(&
)、sizeof
以及typeid
等运算符的运算对象时,上述转变不会发生。同样地,使用一个引用来初始化数组时,例如int (*Parray)[10] = &arr;
,上述转变不会发生。 - 指针的转换 :C++规定了几种其他的指针转换方式,包括常量整数值
0
或者字面量nullptr
能转换成任意指针类型;指向任意非常量的指针能转换成void*
;指向任意对象的指针能转换成const void*
。 - 转换成布尔类型 :存在如果指针或算术类型的值为 0 ,则转换结果为
false
,否则为true
。 - 转换成常量 :允许将指向非常量类型的指针,转换成指向对应的常量类型的指针,对于引用也是。
例如:
int i = 0;
const int &j = i;
const int *p = &i;
相反的转换不存在——因为它试图将底层 const 删除。
- 类类型定义的转换 :类类型能定义由编译器自动执行的转化,不过编译器每次只能执行一种类型的转换。
例如:在需要使用 string 对象地方使用 C 风格字符串;在条件表达式中使用 istream 的 cin 来查看读入是否成功,若读入成功, cin 转换为true
,否则转换为false
。
4.11.3 显式转换
- 强制类型转换(cast) :显式地将对象强制转换为另一种类型。
- 命名的强制类型转换 :一个命名的强制转换具有如下形式
cast_name (expression);
其中 type 是转换的目标类型, expression 是要转换的值。 如果 type 是引用类型,则结果是左值。 cast_name 是 static_cast 、 dynamic_cast 、 const_cast 和 reinterpret_cast 中的一种。其中 dynamic_cast 支持运行时类型识别。
cast_name:
- static_cast :任何具有明确定义的类型转换,只要不包含底层 const ,都可以使用 static_cast 。
①使用 static_cast 相当于告诉编译器我们知道且不在乎转换时的精度损失,编译器将不会提示 warning 。
② static_cast 对于编译器无法自动执行的类型转换有用。例如可以使用 static_cast 找回存储在 void* 指针中的值:void *p = &d; //正确,任何非常量对象的地址都能存储在 void* 指针 double *n = static_cast
(p); 注意,转换后得到的类型必须与指针所指的类型一致。否则会产生未定义行为。
- const_cast :与 static_cast 不同, const_cast 只能改变运算对象的底层 const 。
例如:const char *pc; char *p = const_cast
(pc); //正确,但是通过 p 来进行写操作是未定义行为 对于将常量对象转换成非常量对象的行为,一般称其为 去掉 const 性质(cast away the const) 。如果对象本身不是一个常量,那么使用强制类型转换获得写权限是合法的。但是如果对象是一个常量,使用 const_cast 执行写操作会产生未定义的后果。
- reinterpret_cast :通常为运算对象的位模式提供较低层次上的重新解释。
例如:int *tp; char *cp = reinterpret_cast
(tp); cp 实际上指向的是一个 int 对象,而不是一个 char 对象。如果把 cp 当成 char 对象当作是一个普通的字符指针使用,就可能在运行时发生错误。
由于编译器对于这种强制类型转换是合法的,所以后续使用 cp 时,编译器会将其当作指向 char 对象的指针去使用,不会有任何警示和报错。
- 旧式的强制类型转换
早期的 C++ ,显式地进行强制类型转换包含两种形式:
type (expr); //函数形式的强制类型转换
(type) expr; //C语言风格的强制类型转换
旧式的强制类型转换具有 const_cast 、 static_cast 、 reinterpret_cast 相似的行为。使用旧式强制类型转换时,如果换成 const_cast 、 static_cast 也合法,则其行为与对应的命名转换一致。如果替换后不合法,则此时旧式强制类型转换执行与 reinterpret_cast 类似的功能。
用我的话说,旧式的强制类型转换把三种命名的类型转换结合在了一起,分情况选择。优点是简洁。缺点是安全性较低和阅读性较差。