# 表达式基础

表达式 通常由 运算符运算对象 组成

字面值变量 是最简单的表达式

# 基本概念

一元运算符:作用于一个运算对象的运算符,如取地址符 & 和解引用符 *

二元运算符:作用于两个运算对象的运算符,如相等运算符 == 和乘法运算符 *

三元运算符:作用于三个运算对象,如条件运算符 _ ? _ : _

# 组合运算符和运算对象

表达式的求值结果,依赖于运算符的 优先级结合律 以及 运算对象的求值顺序

  • 优先级:例如, * 优先级高于 +
  • 结合律:通常是从左往右,遇到括号时则是由内到外
  • 求值顺序:例如, f1() + f2() ,对于 f1()f2() 的计算,没有明确的顺序

# 重载运算符

C++ 定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作

当运算符作用于类类型的运算对象时,用户可以自行定义其含义

这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloaded operator)

IO 库的 >><< 运算符以及 string 对象、 vector 对象和迭代器使用的运算符都是重载的运算符

使用重载运算符时,运算对象的类型和返回值的类型都由该运算符定义

运算对象的个数、运算符的优先级和结合律都是无法改变的

# 左值和右值

C++ 的表达式要么是右值(rvalue,读作 “are-value”),要么就是左值(lvalue,读作 “ell-value”),即,非左即右

对于 C 语言:左值可以位于赋值语句的左侧,右值则不能

相比于 C 语言的左值和右值,C++ 会复杂很多:

  • 右值:取不到地址的表达式
  • 左值:能取到地址的表达式
  • 常量对象为代表的左值不能作为赋值语句的左侧运算对象(即,不能修改常量对象的值)
  • 某些表达式的求值结果是对象,但它们是右值(例如,临时的对象 1 + a 无法取地址,故而是右值)

当一个对象被用作右值时,用的是对象的值(内容)

当对象被用作左值时,用的是对象的身份(在内存中的位置)

在需要右值的地方可以用左值来代替,当一个左值被当成右值使用时,实际使用的是它的内容(值)

但是,不能把右值当成左值(也就是位置)使用

常见的运算符:

  • 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也依然是一个左值

  • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值

  • 内置解引用运算符、下标运算符、迭代器解引用运算符、 stringvector 的下标运算符的求值结果都是左值

  • 内置类型和迭代器的递增递减运算符作用于左值运算对象

使用关键字 decltype 时:如果表达式的求值结果是左值, decltype 作用于该表达式(不是变量)将会得到一个引用类型

例如,对于 int *p ,因为解引用运算符(即, *p )生成左值,所以 decltype(*p) 的结果是引用类型,即 int& ;另一方面,因为取地址运算符(即, &p )生成右值,所以 decltype(&p) 的结果是 int** ,即,一个指向整型指针的指针

# 优先级与结合律

复合表达式(compound expression)是指含有两个或多个运算符的表达式

计算复合表达式的值,要根据优先级与结合律决定运算符和运算对象的组合方式

高优先级运算符的运算对象 比 低优先级运算符的运算对象 更为紧密地组合在一起

  • 例如,乘法和除法的优先级相同且都高于加法的优先级,因此,乘法和除法的运算对象会首先组合在一起,然后才能轮到加法和减法的运算对象

如果优先级相同,则其组合规则由结合律确定

  • 算术运算符满足左结合律,即,如果运算符的优先级相同,将按照从左向右的顺序组合运算对象

# 括号无视优先级与结合律

括号无视普通的组合规则:在表达式中,括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合

// parentheses result in alternative groupings
cout << (6 + 3) *  (4 / 2 + 2) << endl;    // prints 36
cout << ((6 + 3) *  4) / 2 + 2 << endl;    // prints 20
cout << 6 + 3 * 4  / (2 + 2) << endl;      // prints 9

# 优先级与结合律有何影响

优先级会影响程序的正确性

int ia[] = {0,2,4,6,8}; // array with five elements of type int
int last = *(ia + 4);   // initializes last to 8, the value of ia [4]
last = *ia + 4;          // last = 4, equivalent to ia [0] + 4

结合律对表达式产生影响的一个典型示例是输入输出运算:IO 相关的运算符满足左结合律,因此,可以把几个 IO 运算组合在一条表达式当中

cin >> v1 >> v2; // read into v1 and then into v2

# 求值顺序

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值,并且,在大多数情况下,不会明确指定求值的顺序

例如,对于下列表达式,我们知道 f1 和 f2 一定会在执行乘法之前被调用,但是我们无法知道到底是先调用 f1 还是先调用 f2

int i = f1() * f2();

对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为

例如, << 运算符没有明确规定何时以及如何对运算对象求值,因此,下面的输出表达式是未定义的

int i = 0;
cout << i << " " << ++i << endl; // undefined

特别地,有 4 种运算符明确规定了运算对象的求值顺序:

  • 逻辑与( && )运算符:先求左侧运算对象的值,只有当左侧运算对象的值为真时,才继续求右侧运算对象的值
  • 逻辑或( || )运算符
  • 条件( ? : )运算符
  • 逗号( , )运算符

# 求值顺序、优先级、结合律

运算对象的求值顺序与优先级和结合律无关

例如,对于 f () + g () * h () + j () 这一表达式而言

  • 优先级规定:g () 的返回值和 h () 的返回值相乘
  • 结合律规定:f () 的返回值先与 g () 和 h () 的乘积相加,所得结果再与 j () 的返回值相加
  • 这些函数的调用顺序并没有明确规定

如果 f 、g 、h 和 j 是无关函数,并且,不会改变同一对象的状态,也不执行 IO 任务,那么函数的调用顺序不受限制;反之,如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为

建议:

  • 在不确定优先级和结合律的时候,最好加上括号来限定表达式的组合关系
  • 如果改变了某个运算对象的值,在表达式的其他地方不要再使用该运算对象

特别地,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时,第二条规则无效

例如,在表达式 *++iter 中,递增运算符改变 iter 的值,iter(已经改变)的值又是解引用运算符的运算对象。此时(或类似的情况下),求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算

显然,这是一种很常见的用法,不会造成什么问题

# 算术运算符

如下所示,在算术运算符中,优先级最高的是正号和负号,其次是乘法和除法,最低是加法和减法

优先级高的运算符比优先级低的运算符组合得更紧密

以上所有算术运算符都满足 左结合律 ,即,当优先级相同时,按照从左向右的顺序组合运算对象

除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型

算术运算符的运算对象和求值结果都是右值

一元正号运算符、加法运算符和减法运算符都能作用于指针

  • When applied to a pointer or arithmetic value, unary plus returns a (possibly promoted) copy of the value of its operand.

  • The unary minus operator returns the result of negating a (possibly promoted) copy of the value of its operand.

int i = 1024;
int k = -i; // i is -1024

bool b = true;
bool b2 = -b; // b2 is true!

对大多数运算符而言,布尔类型的运算对象将被提升为 int 类型

如上所示,布尔变量 b 的值为真,参与运算时将被提升成整数值 1 ,对它求负后的结果是 -1

算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是 0 的情况;另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出

除法运算符 / 作用于两个整数时,所得结果仍为整数(向 0 取整,即直接切除小数部分)

int ival1 = 21/6;  // ival1 is 3; result is truncated; remainder is discarded
int ival2 = 21/7;  // ival2 is 3; no remainder; result is an integral value

在除法运算中,如果两个运算对象的符号相同则商为正(如果不为 0 的话),否则商为负

运算符 % 俗称 “取余” 或 “取模” 运算符,计算两个整数相除所得的余数,其中,参与取余运算的运算对象必须是整数类型

int ival = 42;
double dval = 3.14;
ival % 12;   // ok: result is 6
ival % dval; // error: floating-point operand

根据取余运算的定义,如果 m 和 n 是整数且 n 非 0 ,则表达式 (m / n) * n + m % n 的求值结果与 m 相等,隐含的意思是,如果 m % n 不等于 0 ,则它的符号和 m 相同

 21 % 6;   /*  result is 3   */
 21 / 6;   /*  result is 3   */
 21 % 7;   /*  result is 0   */
 21 / 7;   /*  result is 3   */
-21 % -8;  /*  result is -5  */
-21 / -8;  /*  result is 2   */
 21 % -5;  /*  result is 1   */
 21 / -5;  /*  result is -4  */

C++ 语言的早期版本允许 m % n 的符号匹配 n 的符号,而且 m /n 的结果向负无穷一侧取整,这一方式在新标准中已经被禁止使用了。除了 - m 导致溢出的特殊情况,其他时候 (- m) /n 和 m / (- n) 都等于 - (m /n) ,(- m) % n 和 m % (- n) 都等于 - (m % n)

# 逻辑和关系运算符

关系运算符作用于算术类型或指针类型

逻辑运算符作用于任意能转换成布尔值的类型

逻辑运算符和关系运算符的返回值都是布尔类型,并且,运算对象和求值结果都是右值

# 逻辑运算符

# 逻辑与、逻辑或

逻辑与运算符( && ):当且仅当两个运算对象都为真时,结果为真

逻辑或运算符( || ):只要两个运算对象中的任意一个为真,结果就为真

逻辑与运算符、逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值

  • 对于逻辑与运算符,当且仅当左侧运算对象为真时才对右侧运算对象求值
  • 对于逻辑或运算符,当且仅当左侧运算对象为假时才对右侧运算对象求值

这种策略称为短路求值(short-circuit evaluation

# 逻辑非

逻辑非运算符( ! ):将运算对象的值取反后返回

# 关系运算符

关系运算符:比较运算对象的大小关系并返回布尔值

关系运算符都满足左结合律

由于关系运算符的求值结果是布尔值,将几个关系运算符连写在一起则会产生意想不到的结果:

// oops! this condition compares k to the bool result of i < j
if (i < j < k) // true if k is greater than 1!

在以上 if 语句的条件中,首先把 i、j 和第一个 < 运算符组合在一起,然后将返回的布尔值作为第二个 < 运算符的左侧运算对象

正确的用法应该是

// ok: condition is true if i is smaller than j and j is smaller than k
if (i < j && j < k) { /* ...  */ }

# 相等性测试与布尔字面值

如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为 if 语句的条件:

if (val)  { /*  ...  */ } // true if val is any nonzero value
if (!val) { /*  ...  */ } // true if val is zero

在上面的两个条件中,编译器都将 val 转换成布尔值。如果 val 非 0 则第一个条件为真,如果 val 值为 0 则第二个条件为真

有时会试图将上面的真值测试写成如下形式:

if (val == true) { /* ...   */ } // true only if val is equal to 1!

但是这种写法存在两个问题:首先,与之前的代码相比,这种写法较长而且不太直接;更重要的一点是,如果 val 不是布尔值,这样的比较就失去了原来的意义

如果 val 不是布尔值,那么在比较之前会首先把 true 转换成 val 的类型,即,如果 val 不是布尔值,代码可以改写成如下形式:

if (val == 1) { /* ... */ }

进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象

当布尔值转换成其他算术类型时,false 会转换成 0 ,而 true 会转换成 1

# 赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值

int i = 0, j = 0, k = 0; // initializations, not assignment
const int ci = i;        // initialization, not assignment
 
1024 = k;      // error: literals are rvalues
i + j = k;     // error: arithmetic expressions are rvalues
ci = k;        // error: ci is a const (nonmodifiable) lvalue

赋值运算的结果是它的左侧运算对象,并且,结果的类型就是左侧运算对象的类型

如果赋值运算符的左右两个运算对象的类型不同,则右侧运算对象将转换成左侧运算对象的类型

int k = 0;
k = 3.14159;    //  result: type int, value 3

C++ 11 新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象

  • 如果左侧运算对象是内置类型,初始值列表最多只能包含一个值。而且,即使进行类型转换的话,该值所占空间不应该大于目标类型的空间

  • 对于类类型而言,赋值运算的细节由类本身决定。例如,vector 模板重载了赋值运算符,并且允许接收初始值列表(当赋值发生时,用右侧运算对象的元素替换左侧运算对象的元素)

int k = 0;
k = {3.14};                 // error: narrowing conversion

vector<int> vi;             // initially empty
vi = {0,1,2,3,4,5,6,7,8,9}; // vi now has ten elements, values 0 through 9

无论左侧运算对象的类型是什么,初始值列表都可以为空。此时,编译器创建一个值初始化的临时量,并将其赋给左侧运算对象

# 赋值运算满足右结合律

与其他二元运算符不同的是,赋值运算符满足右结合律

int ival, jval;
ival = jval = 0; // ok: each assigned 0

在上例中,靠右赋值运算 jval = 0 作为靠左赋值运算符的右侧运算对象,因为赋值运算返回的是其左侧运算对象,靠右赋值运算的结果(即 jval )将会被赋给 ival

对于多重赋值语句中的每一个对象,它的类型要么与右边对象的类型相同、要么可由右边对象的类型转换得到

int ival, *pval; // ival is an int; pval is a pointer to int
ival = pval = 0; // error: cannot assign the value of a pointer to an int

string s1, s2;
s1 = s2 = "OK";  // string literal "OK" converted to string

上例中,ival 和 pval 的类型不同,而且 pval 的类型( int* )无法转换成 ival 的类型( int ),因此,尽管 0 能赋给任何对象,但第一条赋值语句依然是非法的。与之相反,第二条赋值语句是合法的,这是因为字符串字面值可以转换成 string 对象并赋给 s2 ,而 s2 和 s1 的类型相同,所以 s2 的值可以继续赋给 s1

# 赋值运算优先级较低

赋值运算的优先级相对较低,通常需要给赋值部分加上括号使其符合我们的原意

例如,以下代码

// a verbose and therefore more error-prone way to write this loop
int i = get_value();  // get the first value
while (i != 42) {
    // do something ...
    i = get_value();  // get remaining values
}

可改写为

// a better way to write our loop---what the condition does is now clearer
int i;
while ((i = get_value()) != 42) {
    // do something ...
}

与第一个版本相比,第二个版本的 while 条件更容易表达我们的真实意图:不断循环读取数据直至遇到 42 为止。其处理过程为,首先将 get_value 函数的返回值赋给 i ,然后比较 i 和 42 是否相等

注意,如果不加括号的话,含义会有很大变化:比较运算符 != 会将 get_value 函数的返回值与 42 进行比较

赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号

# 切勿混淆相等运算符和赋值运算符

C++ 语言允许用赋值运算作为条件

例如以下语句,在条件部分中,首先把 j 的值赋给 i ,然后检查 i 是否为真。如果 j 不为 0 ,条件将为真

if (i = j)

然而,下列代码

if (i == j)

则是在判断 i 与 j 是否相等

# 复合赋值运算符

我们经常需要对某个对象施以某种运算,然后再把计算的结果赋给该对象,例如:

int sum = 0;
// sum values from 1 through 10 inclusive
for (int val = 1; val <= 10; ++val)
    sum += val; //  equivalent to sum = sum + val

复合赋值操作常常应用于算术运算符或者位运算符

+=   -=    *=   /=   %=         // 算术运算符

<<=  >>=   &=   ^=   |=         // 位运算符

其中,任意一种复合运算符都等价于

a = a operator b;

唯一区别在于左侧运算对象的求值次数:

  • 使用复合运算符只求值一次
  • 使用普通的运算符则求值两次
    • 一次是作为右边子表达式的一部分求值
    • 另一次是作为赋值运算的左侧运算对象求值

当然,这种区别除了对程序性能有些许影响外,几乎可以忽略不计

# 递增和递减运算符

递增运算符( ++ )和递减运算符( -- )分别对运算对象执行加 1 和减 1 操作

递增运算符和递减运算符可应用于迭代器

很多迭代器本身不支持算术运算,但可以通过递增运算符和递减运算符移动迭代器

递增和递减运算符有两种形式:

  • 前置版本(prefix):运算符首先将运算对象加 1(或减 1),然后将改变后的对象作为求值结果
  • 后置版本(postfix):将运算对象加 1(或减 1),但是求值结果是运算对象改变之前那个值的副本
int i = 0, j;
j = ++i; // j = 1, i = 1
j = i++; // j = 1, i = 2

这两种运算符必须作用于左值运算对象

  • 前置版本将对象本身作为左值返回
  • 后置版本将对象原始值的副本作为右值返回

建议:尽可能使用前置版本的递增和递减运算符

  • 前置版本的递增运算符把值加 1 后直接返回,避免了不必要的工作
  • 后置版本需要将原始值存储下来,以便于返回这个未修改的内容

如果我们不需要递增(递减)运算符修改前的值,那么后置版本的操作就是一种浪费

# 解引用和递增运算符的结合

如果在一个表达式中,我们既希望将变量加 1(或减 1)、又希望使用它原来的值,可以使用递增和递减运算符的后置版本

auto pbeg = v.begin();
// 输出元素,直到到达尾后或者遇到第一个负数
while (pbeg != v.end() && *beg >= 0)
    cout << *pbeg++ << endl; // 输出当前值,并将 pbeg 向前移动

由于 后置递增运算符的优先级高于解引用运算符 ,在上例中,*pbeg++ 等价于 *(pbeg++):pbeg++ 把 pbeg 的值加 1,然后返回 pbeg 初始值的副本作为其求值结果,即,解引用运算符的运算对象是 pbeg 未增加时的值

形如 *pbeg++ 的表达式,可能一开始不太容易理解,但其实这是一种被广泛使用的、有效的写法

例如, cout << *iter++ << endl; 要比下面的等价语句更简洁、也更少出错

cout << *iter << endl;
++iter;

# 运算对象可按任意顺序求值

大多数运算符都没有规定运算对象的求值顺序,这在一般情况下不会有什么影响。但是,如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了

递增运算符和递减运算符会改变运算对象的值,因此,需要提防这两个运算符在复合表达式中的使用

例如,以下程序使用 for 循环将输入的第一个单词改成大写形式,其中,解引用 it 和递增 it 两项任务是分开进行的

for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
    *it = toupper(*it); // capitalize the current character

如果将解引用 it 和递增 it 结合到一块,即,如下所示,将产生未定义的行为。这是因为:赋值运算符左右两端的运算对象都用到了 beg ,然而,右侧的运算对象改变了 beg 的值

while (beg != s.end() && !isspace(*beg))
    *beg = toupper(*beg++);   // 错误:该赋值语句未定义

# 成员访问运算符

点运算符( . )和箭头运算符( > )可共同用于访问成员,其中,点运算符获取类对象的一个成员,箭头运算符与点运算符有关

表达式 ptr->mem 等价于 (*ptr).mem

string s1 = "a string", *p = &s1;
auto n = s1.size(); // run the size member of the string s1
n = (*p).size();    // run size on the object to which p points
n = p->size();      // equivalent to (*p).size()

其中,因为 解引用运算符的优先级低于点运算符 ,执行解引用运算的子表达式两端必须加上括号

如果不加括号,即:

// run the size member of p, then dereference the result!
*p.size();    // error: p is a pointer and has no member named size

表达式试图访问对象 p 的 size 成员,然而,p 本身是一个指针且不包含任何成员,所以,上述语句无法通过编译

箭头运算符作用于一个指针类型的运算对象,结果是一个左值

点运算符:

  • 如果成员所属的对象是左值,那么结果是左值
  • 如果成员所属的对象是右值,那么结果是右值

# 条件运算符

条件运算符( ? : )可以把简单的 if-else 逻辑嵌入到单个表达式当中

条件运算符按照如下形式使用:

cond  ? expr1  : expr2;

其中,cond 是判断条件的表达式,而 expr1 和 expr2 是两个类型相同(或者,可以转换为某个公共类型)的表达式

条件运算符的执行过程是:首先求 cond 的值,如果条件为真,则对 expr1 求值并返回该值,否则,对 expr2 求值并返回该值

例如,可以使用条件运算符来判断成绩是否合格:

string finalgrade = (grade < 60) ? "fail" : "pass";

当条件运算符的两个表达式都是左值(或者,能转换成同一种左值类型)时,运算的结果是左值;否则,运算的结果是右值

# 嵌套条件运算符

允许在条件运算符的内部嵌套另外一个条件运算符

即,条件表达式可以作为另外一个条件运算符的 cond 或 expr

例如,可以使用嵌套的条件运算符将成绩分成三档:优秀(high pass)、合格(pass)和不合格(fail):

finalgrade = (grade > 90) ? "high pass"
                          : (grade < 60) ? "fail" : "pass";

上述语句首先检查成绩是否在 90 分以上,如果是,执行符号 ? 后面的表达式,得到 "high pass" ;如果否,执行符号 : 后面的分支,即,继续检查成绩是否在 60 分以下,如果是,得到 "fail" ;否则,得到 "pass"

条件运算符满足右结合律,即,运算对象(一般)按照从右向左的顺序组合

因此,在上面的代码中,靠右边的条件运算(比较成绩是否小于 60)构成了靠左边的条件运算的 : 分支

随着条件运算嵌套层数的增加,代码的可读性急剧下降,因此,条件运算的嵌套最好别超过两到三层

# 在输出表达式中使用条件运算符

条件运算符的优先级非常低,因此,当一条长表达式中嵌套了条件运算表达式时,通常需要在它两端加上括号

例如:有时需要根据条件值输出两个对象中的一个,如果写这条语句时没把括号写全就有可能产生意想不到的结果:

cout << ((grade < 60) ? "fail" : "pass"); // 输出 pass 或者 fail
cout << (grade < 60) ? "fail" : "pass";   // 输出 1 或者 0
cout << grade < 60 ? "fail" : "pass"; //  错误:试图比较 cout 和 60

在第二个条件运算表达式中,grade 和 60 的比较结果是 << 运算符的运算对象,因此,如果 grade < 60 为真,输出 1,否则,输出 0。 << 运算符的返回值是 cout ,接下来, cout 将作为条件运算符的条件。也就是说,第二条表达式等价于

cout << (grade < 60);    // 输出 1 或者 0
cout ?  "fail" : "pass"; // 根据 cout 的值(true 或者 false)产生对应的字面值

在第三个条件运算语句中,小于运算符的优先级低于移位运算符 ,所以会先输出 grade,然后比较 cout 和 60 ,即,等价于:

cout << grade;
cout < 60 ? "fail" : "pass";

因此,第三个条件运算语句是错误的

# 位运算符

位运算符作用于整数类型的运算对象,并将运算对象视为二进制位的集合

一种名为 bitset 的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用于 bitset 类型

位运算符提供检查和设置二进制位的功能

运算符功能用法
~位求反~ expr
<<左移expr1 << expr2
>>右移expr1 >> expr2
&位与expr1 & expr2
^位异或expr1 ^ expr2
|位或expr1 | expr2

一般来说,如果运算对象是 “小整型”,则它的值会被自动提升成较大的整数类型

运算对象可以是带符号的,也可以是无符号的

在执行位运算时,如果运算对象是带符号的且它的值为负,如何处理运算对象的 “符号位” 将依赖于机器。并且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为

建议 仅将位运算符用于处理无符号类型

# 位求反运算符

位求反运算符(~)将运算对象逐位求反(即,将 1 置为 0 、将 0 置为 1 )后生成一个新值

特别地,针对 char 类型对象执行位求反运算时,首先会将 char 类型的运算对象提升成 int 类型(提升时,运算对象原来的位保持不变,往高位添加 0 即可),随后将提升后的值逐位求反

例如:

# 移位运算符

移位运算符包括左移和右移

移位运算符的内置含义:对运算对象执行基于二进制位的移动操作

  • 针对左侧运算对象的内容,移动右侧运算对象所要求的位数
  • 将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果

其中,右侧的运算对象一定不能为负,并且,其值必须严格小于结果的位数(否则,会产生未定义的结果)

左移运算符(<<)在右侧插入值为 0 的二进制位

右移运算符(>>)的行为依赖于其左侧运算对象的类型:

  • 如果左侧运算对象是无符号类型,在左侧插入值为 0 的二进制位
  • 如果左侧运算对象是带符号类型,在左侧插入符号位的副本或值为 0 的二进制位(由具体环境确定)

无论是左移还是右移,移出边界的位都会被舍弃掉

# 移位运算符满足左结合律

重载运算符的优先级和结合律都与它的内置版本一样

即,IO 运算符的优先级和结合律与移位运算符一样

由于移位运算符满足左结合律, cout << "hi" << " there" << endl; 等价于 ( (cout << "hi") << " there" ) << endl;

移位运算符的优先级比算术运算符低,但比关系运算符、赋值运算符和条件运算符高。因此,在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求

例如:

cout << 42 + 10;   // 正确:+ 的优先级更高,输出 52
cout << (10 < 42); // 正确:括号的优先级高,输出 1
cout << 10 < 42;   // 错误:试图比较 cout 和 42 (等价于 (cout << 10) < 42;)

# 位与、位或、位异或运算符

位与运算:如果两个运算对象的对应位置都是 1,则该位的运算结果为 1,否则为 0

位或运算:如果两个运算对象的对应位置至少有一个为 1,则该位的运算结果为 1,否则为 0

位异或运算:位或运算:如果两个运算对象的对应位置有且只有一个为 1,则该位的运算结果为 1,否则为 0

一种常见的错误是混淆位运算符和逻辑运算符:位与(&)和逻辑与(&&)、位或(|)和逻辑或(||)、位求反(~)和逻辑非(!)

# sizeof 运算符

sizeof 运算符返回一条表达式或一个类型名字所占的字节数

sizeof 运算符满足右结合律,其所得的值是一个 size_t 类型的常量表达式

sizeof 运算符的运算对象有两种形式:

sizeof (type)

sizeof expr

在第二种形式中,sizeof 返回的是表达式结果类型的大小

需注意的是,sizeof 并不实际计算其运算对象的值

Sales_data data, *p;
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; // 另一种获取 revenue 大小的方式

对于 sizeof *p

  • sizeof 满足右结合律,并且 sizeof 的优先级与 * 运算符一样,所以,表达式按照从右向左的顺序组合,即,等价于 sizeof(*p)
  • sizeof 不会实际求运算对象的值,即便 p 是一个无效(即未初始化)指针也不会有什么影响

可以在 sizeof 的运算对象中解引用一个无效指针,因为指针实际上并没有被真正使用。sizeof 不需要真的解引用指针也能知道它所指对象的类型

C++ 11 允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但执行 sizeof 运算时无须提供一个具体的对象,因为 sizeof 不需要真正获取该成员

sizeof 运算符的结果 部分地 依赖于其作用的类型:

  • 对 char 或者类型为 char 的表达式执行 sizeof 运算,结果为 1
  • 对引用类型执行 sizeof 运算,结果为被引用对象所占空间的大小
  • 对指针执行 sizeof 运算,结果为指针本身所占空间的大小
  • 对解引用指针执行 sizeof 运算,结果为指针指向的对象所占空间的大小,指针不需有效
  • 对数组执行 sizeof 运算,结果为整个数组所占空间的大小,等价于 对数组中所有的元素各执行一次 sizeof 运算并将所得结果求和(注意, sizeof 运算不会把数组转换成指针来处理)
  • 对 string 对象或 vector 对象执行 sizeof 运算,结果为该类型固定部分的大小,不会计算对象中的元素占用了多少空间

因为执行 sizeof 运算能得到整个数组的大小,可以用整个数组的大小除以单个元素的大小,得到数组中的元素个数

// sizeof(ia) / sizeof(*ia) 返回 ia 数组中的元素个数
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz]; // sizeof 返回一个常量表达式

因为 sizeof 的返回值是一个常量表达式,所以我们可以用 sizeof 的结果声明数组的维度

# 逗号运算符

逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值

逗号运算符规定了运算对象求值的顺序:

  • 首先对左侧的表达式求值,然后将求值结果丢弃掉
  • 逗号运算符真正的结果是右侧表达式的值

如果右侧运算对象是左值,那么最终的求值结果也是左值

逗号运算符经常被用在 for 循环中,例如:

vector<int>::size_type cnt = ivec.size();
for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)
    ivec[ix] = cnt;

这个循环在 for 语句的表达式中递增 ix、递减 cnt,每次循环迭代时 ix 和 cnt 都会相应改变

# 类型转换

如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代

int ival = 3.541 + 3; // 初始化 ival 为 6

在上面的例子中,3 转换成 double 类型,然后执行浮点数加法,所得结果的类型是 double,由于被初始化的对象 ival 为 int 型,需要将加法运算得到的 double 类型的结果转换成 int 类型的值

隐式转换(implicit conversion):自动执行的类型转换,无须程序员的介入

在下面这些情况下,编译器会自动地转换运算对象的类型:

  • 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型
  • 在条件中,非布尔值转换成布尔类型
  • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
  • 函数调用时也会发生类型转换

# 算术转换

算术转换(arithmetic conversion):把一种算术类型转换成另外一种算术类型

算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型,例如:

  • 如果一个运算对象的类型是 long double,那么不论另外一个运算对象的类型是什么都会转换成 long double
  • 还有一种更普遍的情况:当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型

# 整型提升

整型提升(integral promotion)负责把小整数类型转换成较大的整数类型

对于 bool、char、signed char、unsigned char、short 和 unsigned short 等类型,只要它们所有可能的值都能存进 int ,它们就会提升成 int 类型;否则,提升成 unsigned int 类型

较大的 char 类型(wchar_t、char16_t、char32_t)提升成 int、unsigned int、long、unsigned long、long long 和 unsigned long long 中能容纳原类型所有可能值的、最小的一种类型

# 无符号类型的运算对象

如果某个运算对象的类型是无符号类型,转换的结果将依赖于机器中各个整数类型的相对大小。具体而言,首先执行整型提升,然后进一步根据运算对象的类型做进一步转换

  • 如果结果的类型匹配,无须进行进一步的转换
  • 如果两个(提升后的)运算对象都是带符号类型或者都是无符号类型,则小类型的运算对象转换成较大的类型
  • 如果一个运算对象是无符号类型、另外一个运算对象是带符号类型
    • 如果无符号类型不小于带符号类型:将带符号的运算对象转换成无符号的
    • 如果带符号类型大于无符号类型,此时转换的结果依赖于机器
      • 如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型
      • 如果不能,那么带符号类型的运算对象转换成无符号类型

要想理解算术转换,办法之一就是研究大量的例子:

bool      flag;         char           cval;
short     sval;         unsigned short usval;
int       ival;         unsigned int   uival;
long      lval;         unsigned long  ulval;
float     fval;         double         dval;
3.14159L + 'a'; //  'a' 提升成 int,然后 int 值转换成 long double
dval + ival;    //  ival 转换成 double
dval + fval;    //  fval 转换成 double
ival = dval;    //  dval 转换成 int
flag = dval;    //  如果 dval 为 0,则 flag 为 false,否则 flag 为 true
cval + fval;    //  cval 提升成 int,然后 int 值转换成 float
sval + cval;    //  sval 和 cval 都提升成 int
cval + lval;    //  cval 转换成 long
ival + ulval;   //  ival 转换成 unsigned long
usval + ival;   //  根据 unsigned short 和 int 所占空间的大小进行提升
uival + lval;   //  根据 unsigned int 和 long 所占空间的大小进行转换

# 其他隐式类型转换

除了算术转换之外还有几种隐式类型转换,包括如下几种

# 数组转换成指针

在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针

int ia[10];
int* ip = ia;

当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeof 及 typeid 等运算符的运算对象时,上述转换不会发生

如果用一个引用来初始化数组,上述转换也不会发生

# 指针的转换

C++ 规定了几种其他的指针转换方式

  • 常量整数值 0 或者字面值 nullptr 能转换成任意指针类型
  • 指向任意非常量的指针能转换成 void*
  • 指向任意对象的指针能转换成 const void*

此外,在有继承关系的类型间还有另外一种指针转换的方式

# 转换成布尔类型

存在一种从算术类型或指针类型向布尔类型自动转换的机制:如果指针或算术类型的值为 0 ,转换结果是 false ;否则转换结果是 true

# 转换成常量

允许将指向非常量类型的指针转换成指向相应的常量类型的指针

允许将绑定非常量类型的引用转换成绑定相应的常量类型的引用

也就是说,如果 T 是一种类型,我们就能将指向 T 的指针或引用分别转换成指向 const T 的指针或引用

int i;
const int &j = i;
const int *p = &i;

相反的转换并不存在,因为它试图删除掉底层 const

int &r = j, *q = p; // 错误:不允许 const 转换成非常量

# 类类型定义的转换

类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换

我们之前已经使用过类类型转换:一处是在需要标准库 string 类型的地方使用 C 风格字符串;另一处是在条件部分读入 istream

string s, t = "a value";  // 字符串字面值转换成 string 类型
while (cin >> s)          // while 的条件部分把 cin 转换成 bool 值

# 显式转换

有时我们希望显式地将对象强制转换成另外一种类型,即,强制类型转换(cast)

例如,如果想在下面的代码中执行浮点数除法,就需要使用某种方法将 i 和(或)j 显式地转换成 double

int i, j;
double slope = i / j;

虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的

# 命名的强制类型转换

一个命名的强制类型转换具有如下形式:

cast-name<type>(expression);

其中,type 是转换的目标类型,expression 是要转换的值,cast-name 是 static_castdynamic_castconst_castreinterpret_cast 中的一种

如果 type 是引用类型,则结果是左值

cast-name 指定了执行的是哪种转换(dynamic_cast 支持运行时类型识别)

# static_cast

任何具有明确定义的类型转换,只要不包含底层 const ,都可以使用 static_cast

例如,将一个运算对象强制转换成 double 类型以便执行浮点数除法:

// 进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;

当需要把一个较大的算术类型赋值给较小的类型时,static_cast 非常有用

  • 一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息
  • 但是当我们执行了显式的类型转换后,警告信息就会被关闭了

static_cast 对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用 static_cast 找回存在于 void* 指针中的值

void* p = &d;   // 任何非常量对象的地址都能存入 void*
double *dp = static_cast<double*>(p); // 将 void* 转换回初始的指针类型

当我们把指针存放在 void* 中,并且使用 static_cast 将其强制转换回原来的类型时,应该确保指针的值保持不变(即,强制转换的结果将与原始的地址值相等),因此,必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果

# const_cast

const_cast 只能改变运算对象的底层 const:

const char *pc;
char *p = const_cast<char*>(pc); // 正确,但通过 p 写值是未定义的行为(因为 pc 所指对象本身是常量)

对于将常量对象转换成非常量对象的行为,我们一般称其为 “去掉 const 性质(cast away the const)”

一旦我们去掉了某个对象的 const 性质,编译器就不回再阻止对该对象的写操作

  • 如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为
  • 如果对象是一个常量,再使用 const_cast 执行写操作,会产生未定义的后果

只有 const_cast 能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误

不能用 const_cast 改变表达式的类型:

const char *cp;
char *q = static_cast<char*>(cp); // 错误: static_cast 不能去掉 const 性质
static_cast<string>(cp); // 正确:字符串字面值转换成 string 类型
const_cast<string>(cp);  // 错误:const_cast 只改变常量属性

const_cast 常常用于有函数重载的上下文中

# reinterpret_cast

reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释

例如,假设有如下的转换

int *ip;
char *pc = reinterpret_cast<char*>(ip); // 效果类似 C 风格的强制转换

我们必须牢记 pc 所指的真实对象是一个 int 而非字符,如果把 pc 当成普通的字符指针使用就可能在运行时发生错误

reinterpret_cast 本质上依赖于机器。要想安全地使用 reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解

建议:尽量避免强制类型转换,尤其是 reinterpret_cast

# 旧式的强制类型转换

在早期版本的 C++ 语言中,显式地进行强制类型转换包含两种形式

type (expr); // 函数形式的强制类型转换
(type) expr; // C 语言风格的强制类型转换

当我们在某处执行旧式的强制类型转换时,如果换成 const_cast 和 static_cast 也合法,则其行为与对应的命名转换一致

如果替换后不合法,则旧式强制类型转换执行与 reinterpret_cast 类似的功能

int *ip;
char *pc = (char*) ip; // ip 是指向 int 的指针

# 运算符优先级表

从上到下,不同表格的优先级按从高到低排列,其中,同一个表格中的多个运算符具有相同的优先级

结合律运算符功能用法
::全局作用域::name
::类作用域class::name
::命名空间作用域namespace::name
结合律运算符功能用法
:-::-::-::-:
.成员选择object.member
->成员选择pointer->member
[]下标expr1[expr2]
()函数调用name(expr_list)
()类型构造type(expr_list)
结合律运算符功能用法
:-::-::-::-:
++后置递增运算lvalue++
--后置递减运算lvalue--
typeid类型 IDtypeid(type)
typeid运行时类型 IDtypeid(expr)
explicit cast类型转换cast_name<type>(expr)
结合律运算符功能用法
:-::-::-::-:
++前置递增运算++lvalue
--前置递减运算--lvalue
~位求反~expr
!逻辑非!expr
-一元负号-expr
+一元正号+expr
*解引用*expr
&取地址&expr
()类型转换(type) expr
sizeof对象的大小sizeof expr
sizeof类型的大小sizeof(type)
sizeof...参数包的大小sizeof...(name)
new创建对象new type
new[]创建数组new type[size]
delete释放对象delete expr
delete[]释放数组delete[] expr
noexcept能否抛出异常noexcept (expr)
结合律运算符功能用法
:-::-::-::-:
->*指向成员选择的指针ptr->*ptr_to_member
.*指向成员选择的指针obj.*ptr_to_member
结合律运算符功能用法
:-::-::-::-:
*乘法expr1 * epxr2
/除法expr1 / epxr2
%取模(取余)expr1 % epxr2
结合律运算符功能用法
:-::-::-::-:
+加法expr1 + epxr2
-减法expr1 - expr2
结合律运算符功能用法
:-::-::-::-:
<<向左移位expr1 << expr2
>>向右移位expr1 >> expr2
结合律运算符功能用法
:-::-::-::-:
<小于expr1 < expr2
<=小于等于expr1 <= expr2
>大于expr1 > expr2
>=大于等于expr1 >= expr2
结合律运算符功能用法
:-::-::-::-:
==相等expr1 == expr2
!=不相等expr1 != expr2
结合律运算符功能用法
:-::-::-::-:
&位与expr1 & expr2
结合律运算符功能用法
:-::-::-::-:
^位异或expr1 ^ expr2
结合律运算符功能用法
:-::-::-::-:
|位或expr1 | expr2
结合律运算符功能用法
:-::-::-::-:
&&逻辑与expr1 && expr2
结合律运算符功能用法
:-::-::-::-:
||逻辑或expr1 || expr2
结合律运算符功能用法
:-::-::-::-:
? :条件expr1 ? expr2 : epxr3
结合律运算符功能用法
:-::-::-::-:
=赋值lvalue = expr
*=,/=, %=, +=, -=, <<=, >>=, &=, |=, ^=复合赋值lvalue += expr 等
结合律运算符功能用法
:-::-::-::-:
throw抛出异常throw expr
结合律运算符功能用法
:-::-::-::-:
,逗号expr1, expr2

# 术语表

算术转换(arithmetic conversion):从一种算术类型转换成另一种算术类型。在二元运算符的上下文中,为了保留精度,算术转换通常把较小的类型转换成较大的类型(例如整型转换成浮点型)

结合律(associativity):规定具有相同优先级的运算符如何组合在一起。结合律分为左结合律(运算符从左向右组合)和右结合律(运算符从右向左组合)

二元运算符(binary operator):有两个运算对象参与运算的运算符

强制类型转换(cast):一种显式的类型转换

复合表达式(compound expression):含有多于一个运算符的表达式

const_cast :一种涉及 const 的强制类型转换。将底层 const 对象转换成对应的非常量类型,或者执行相反的转换

转换(conversion):一种类型的值改变成另一种类型的值的过程

dynamic_cast :和继承及运行时类型识别一起使用

表达式(expression):C++ 程序中最低级别的计算。表达式将运算符作用于一个或多个运算对象,每个表达式都有对应的求值结果。表达式本身也可以作为运算对象,这时就得到了对多个运算符求值的复合表达式

隐式转换(implicit conversion):由编译器自动执行的类型转换。假如表达式需要某种特定的类型而运算对象是另外一种类型,此时只要规则允许,编译器就会自动地将运算对象转换成所需的类型

整型提升(integral promotion):把一种较小的整数类型转换成与之最接近的较大整数类型的过程。不论是否真的需要,小整数类型(即 short 、char 等)总是会得到提升

左值(lvalue):是指那些求值结果为对象或函数的表达式。一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象

运算对象(operand):表达式在某些值上执行运算,这些值就是运算对象。一个运算符有一个或多个相关的运算对象

运算符(operator):决定表达式所做操作的符号

  • C++ 语言定义了一套运算符并说明了这些运算符作用于内置类型时的含义
  • C++ 还定义了运算符的优先级和结合律以及每种运算符处理的运算对象数量
  • 可以重载运算符使其能处理类类型

求值顺序(order of evaluation):是某个运算符的运算对象的求值顺序

  • 大多数情况下,编译器可以任意选择运算对象求值的顺序(运算对象一定要在运算符之前得到求值结果)
  • 只有 && 、|| 、条件和逗号四种运算符明确规定了求值顺序

重载运算符(overloaded operator):针对某种运算符重新定义的适用于类类型的版本

优先级(precedence):规定了复合表达式中不同运算符的执行顺序。与低优先级的运算符相比,高优先级的运算符组合得更紧密

reinterpret_cast :把运算对象的内容解释成另外一种类型。这种强制类型转换本质上依赖于机器而且非常危险

结果(result):计算表达式得到的值或对象

右值(rvalue):是指一种表达式,其结果是值而非值所在的位置

短路求值(short-circuit evaluation):描述逻辑与运算符和逻辑或运算符的执行过程。如果根据运算符的第一个运算对象就能确定整个表达式的结果,求值终止,此时第二个运算对象将不会被求值

sizeof :是一个运算符,返回存储对象所需的字节数,该对象的类型可能是某个给定的类型名字,也可能由表达式的返回结果确定

static_cast :显式地执行某种定义明确的类型转换,常用于替代由编译器隐式执行的类型转换

一元运算符(unary operators):只有一个运算对象参与运算的运算符

, 运算符(, operator):逗号运算符,是一种从左向右求值的二元运算符。逗号运算符的结果是右侧运算对象的值,当且仅当右侧运算对象是左值时逗号运算符的结果是左值

? : 运算符(? : operator):条件运算符

  • cond ? expr1 : expr2 提供 if-then-else 逻辑的表达式:如果条件 cond 为真,对 expr1 求值;否则对 expr2 求值
  • expr1 和 expr2 的类型应该相同或者能转换成同一种类型
  • expr1 和 expr2 中只有一个会被求值

&& 运算符(&&operator):逻辑与运算符,如果两个运算对象都是真,结果才为真。只有当左侧运算对象为真时才会检查右侧运算对象

& 运算符(&operator):位与运算符,由两个运算对象生成一个新的整型值。如果两个运算对象对应的位都是 1 ,所得结果中该位为 1 ;否则所得结果中该位为 0

^ 运算符(^ operator):位异或运算符,由两个运算对象生成一个新的整型值。如果两个运算对象对应的位有且只有一个是 1 ,所得结果中该位为 1 ;否则所得结果中该位为 0

|| 运算符(|| operator):逻辑或运算符,任何一个运算对象是真,结果就为真。只有当左侧运算对象为假时才会检查右侧运算对象

| 运算符(| operator):位或运算符,由两个运算对象生成一个新的整型值。如果两个运算对象对应的位至少有一个是 1 ,所得结果中该位为 1 ;否则所得结果中该位为 0

++ 运算符(++ operator):递增运算符。包括两种形式:前置版本和后置版本

  • 前置递增运算符得到一个左值,它给运算符加 1 并得到运算对象改变后的值
  • 后置递增运算符得到一个右值,它给运算符加 1 并得到运算对象原始的、未改变的值的副本
  • 注意:即使迭代器没有定义 + 运算符,也会有 ++ 运算符

-- 运算符(-- operator):递减运算符。包括两种形式:前置版本和后置版本

  • 前置递减运算符得到一个左值,它从运算符减 1 并得到运算对象改变后的值
  • 后置递减运算符得到一个右值,它从运算符减 1 并得到运算对象原始的、未改变的值的副本
  • 注意:即使迭代器没有定义 - 运算符,也会有 -- 运算符

<< 运算符( << operator:左移运算符,将左侧运算对象的值的(可能是提升后的)副本向左移位,移动的位数由右侧运算对象确定。右侧运算对象必须大于等于 0 而且小于结果的位数。左侧运算对象应该是无符号类型,如果它是带符号类型,则一旦移动改变了符号位的值就会产生未定义的结果

>> 运算符(>> operator):右移运算符,除了移动方向相反,其他性质都和左移运算符类似。如果左侧运算对象是带符号类型,那么根据实现的不同新移入的内容也不同,新移入的位可能都是 0 ,也可能都是符号位的副本

~ 运算符(~ operator):位求反运算符,生成一个新的整型值。该值的每一位恰好与(可能是提升后的)运算对象的对应位相反

! 运算符(! operator):逻辑非运算符,将它的运算对象的布尔值取反。如果运算对象是假,则结果为真,如果运算对象是真,则结果为假

参考:C++ Primer 中文版(第 5 版)

阅读次数