《C++ Primer》 拾遗 第 4 章 表达式
Notes Cpp Primer
Lastmod: 2021-06-01 周二 22:40:27

第 4 章 表达式

表达式 expression 由一个或多个运算对象 operand 组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,运算符 operator 将一个或多个对象组合起来可以生成复杂的表达式。

4.1 基础

4.1.1 基本概念

对于复杂的表达式,需要了解运算符的优先级 precedence、结合律 associativity 及运算对象的求职顺序 order of evaluation

数值计算过程可能会涉及到类型提升 promoted

左值与右值

左值 lvalue 和右值 rvalue 的概念来源于 C 语言,但在 C++ 中不等同于是否可以出现在赋值语句左侧、右侧。

当对象被当做右值时使用的是值,当做左值时使用的是它的身份(地址,内存中的位置)。需要右值的时候可以用一个左值代替。

不同运算符对于参与运算的对象有不同的要求。

赋值运算左侧的运算对象需要是一个左值,得到的结果也是一个左值。

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

内置解引用运算符、下标运算符、迭代器解引用运算符、string 和 vector 下标运算符得到的都是左值。

内置类型和迭代器的递增递减运算符作用于左值,其前置版本(前缀运算符)得到的也是左值。

使用 decltype 的时候,是区分左值右值的。当表达式的求值结果是一个左值,decltype 作用于该表达式将得到引用类型。

4.1.2 优先级与结合律

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

4.1.3 求值顺序

注意求值顺序与优先级和结合律无关。多数算符没有明确规定求值顺序。

四种明确规定了求值顺序的算符是 &&||?:,

4.2 算术运算符

所有算符满足左结合律。

C++11 规定整数商向 0 取整。

对于取余,需要保证,对于非 0 的 n 有,

(m / n) * n + m % n == m
#include <iostream>
using namespace std;

int main() {
	{
		int a = 32;
		int b = 5;
		cout << "a = " << a << ", b = " << b << ", a/b = " << (a / b) << ", a%b = " << (a % b) << '\n';
	}

	{
		int a = 32;
		int b = -5;
		cout << "a = " << a << ", b = " << b << ", a/b = " << (a / b) << ", a%b = " << (a % b) << '\n';
	}

	{
		int a = -32;
		int b = 5;
		cout << "a = " << a << ", b = " << b << ", a/b = " << (a / b) << ", a%b = " << (a % b) << '\n';
	}

	{
		int a = -32;
		int b = -5;
		cout << "a = " << a << ", b = " << b << ", a/b = " << (a / b) << ", a%b = " << (a % b) << '\n';
	}
  return 0;
}
a = 32, b = 5, a/b = 6, a%b = 2
a = 32, b = -5, a/b = -6, a%b = 2
a = -32, b = 5, a/b = -6, a%b = -2
a = -32, b = -5, a/b = 6, a%b = -2

4.3 逻辑和关系运算符

短路求值 short-circuit evaluation

注意当比较运算符一侧的变量不是布尔类型时,不应使用 true 字面值作为比较对象,因为 true 会被自动转换成 1。

#include <iostream>
using namespace std;

int main() {
  int a = 2;
  cout << (a == true) << '\n'; // 0
  cout << (!a) << '\n';        // 0
  return 0;
}

4.4 赋值运算符

赋值运算符返回左值。左右运算对象类型不同时将右侧转化为左侧。

C++11 允许初始值列表作为赋值语句的右侧运算对象。

赋值运算满足右结合律。

注意赋值运算的优先级比关系运算低。

4.5 递增和递减运算符

前置版本返回对象本身的左值,后置版本返回原始值的副本右值。

注意经常会混用解引用和递增算符。此时递增算符的优先级更高。

*p++ // 相当于 *(p++)

注意求值顺序可能是未定义的!这在有递增、递减算符的表达式中尤其重要。

4.6 成员访问运算符

vec.size();
pvec->size();

注意解引用算符的优先级比成员访问算符低,因此

(*pvec).size() // 等价于 pvec->size();
*pvec.size()   // 则表示对于 pvec 对象先调用成员函数 size() 后解引用

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

点运算符,如果所属对象是左值则得到左值,反之得到右值。

4.7 条件运算符

cond ? expr1 : expr2

当 expr1 和 expr2 是左值或是能转换成同一左值类型时,运算结果是左值,否则结果是右值。

条件运算符满足右结合律。

注意条件算符优先及很低,和输出的 << 混用时,需要加括号。

4.8 位运算符

运算对象如果是“小整型”则它的值可能会被自动提升成较大的整数类型。

注意符号位处理没有明确规定,因此尽量使用无符号整数。

如果符号位的值为负,则如何处理符号位依赖于机器。

4.9 sizeof 运算符

sizeof 运算符返回一个表达式或一个类型名字所占的字节数。满足右结合律,其所得到的值是一个 size_t 类型的常量表达式。

sizeof (type)
sizeof expr

注意 sizeof 并不实际计算运算对象的值!

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int arr[10];

int arr2[5][10];

class A {
 private:
  int a;
  long long b;
 public:
  char c;
  void fvoid() { cout << "call fvoid()\n"; }
  long long fll() { cout << "call fll()\n"; return 1LL; };
    // never called in main.
}objA;

vector<int> vec_a = {1, 2, 3, 4, 5};
vector<int> vec_b = {1, 2, 3};

string s_a = "12345";
string s_b = "abc";

int main() {
 
  cout << sizeof(char) << '\n'; // 1

  // cout << sizeof int << '\n'; // ce
  cout << sizeof(int) << '\n'; // 4

  cout << sizeof(long long) << '\n'; // 8

  // cout << sizeof(void) << '\n'; // 1 with warning

  cout << sizeof(char*) << '\n'; // 4
  
  cout << sizeof(long long*) << '\n'; // 4
  
  cout << sizeof(void*) << '\n'; // 4

  // cout << sizeof(int[]) << '\n'; // ce
  cout << sizeof(int[50]) << '\n'; // 200
    
  cout << sizeof arr << '\n'; // 40
  cout << sizeof(arr) << '\n'; // 40
   
  cout << sizeof arr2 << '\n'; // 200

  // cout << sizeof A << '\n'; // ce
  cout << sizeof(A) << '\n'; // 24

  cout << sizeof A() << '\n'; // 24

  cout << sizeof(objA) << '\n'; // 24

  cout << sizeof objA << '\n'; // 24

  // cout << sizeof objA.fvoid() << '\n'; // 1 with warning
  cout << sizeof objA.fll() << '\n'; // 8
  cout << sizeof(objA.fll()) << '\n'; // 8
  cout << sizeof A::fll << '\n'; // 8

  // cout << sizeof A::b << '\n';
  cout << sizeof A::c << '\n'; // 1
  cout << sizeof objA.c << '\n'; // 1

  cout << sizeof vec_a << '\n'; // 12
  cout << sizeof vec_b << '\n'; // 12

  cout << sizeof s_a << '\n'; // 24
  cout << sizeof s_b << '\n'; // 24

  return 0;
}

4.10 逗号运算符

逗号运算符 comma operator 含有两个运算对象,首先对左侧运算对象求值之后对右侧运算对象求值。逗号表达式的结果是右侧表达式的结果,如果右侧运算对象是左值,则逗号表达式的返回值也是左值。

4.11 类型转换

如果两种类型可以相互转换 conversion 则我们说它们是关联的。

隐式转换 implicit conversion

何时发生隐式类型转换:

大多数表达式中比 int 小的整型会被提升。

条件中非布尔值类型会被转换成布尔类型。

初始值转换成变量类型。

算术运算或关系运算对象有多种类型则会转换成同一种类型。

函数调用时也会发生类型转换。

4.11.1 算术转换

arithmetic conversion

整型提升 integral promotion

比 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 等运算符的对象时。

指针转换:0 和 nullptr 能转换成任意类型指针。任意非常量指针可以转换成 void*,任意指针可以转化为 const void*

算术或指针类型转换为 bool 类型。

非常量指针转化为对应的常量指针。

类类型定义的转换。

4.11.3 显式转换

强制类型转换 cast

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

const_cast 可以得到同类型去掉底层 const 的指针。

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

dynamic_cast 支持运行时类型识别。

旧式的强制类型转换
type (expr);
(type) expr;

如果旧式类型转换换成 const_cast 或 static_cast 也合法,则其行为与对应的命名转换一致,否则其拥有 reinterpret_cast 类似的功能。

4.12 运算符优先级表

优先级从上到下从高到低

优先级 结合律(只标右) 运算符 功能 用法
1 :: 全局作用域 ::name
1 :: 类作用域 class::name
1 :: 命名空间作用域 namespace::name
2 . 成员选择 object.member
2 -> 成员选择
2 [] 下标
2 () 函数调用
2 () 类型构造
3 ++ 后递增
3 -- 后递减
3 typeid 类型 ID
3 typeid 运行时类型 ID
3 static_cast
3 dynamic_cast
3 const_cast
3 reinterpret_cast
4 ++ 前递增
4 -- 前递减
4 ~
4 !
4 - 一元负号
4 + 一元正号
4 * 解引用
4 & 取地址
4 () 类型转换
4 sizeof 对象大小
4 sizeof 类型大小
4 sizeof... 参数包大小
4 new
4 new[]
4 delete
4 delete[]
4 noexcept 能否抛出异常
5 ->*
5 .*
6 *
6 /
6 %
7 +
7 -
8 <<
8 >>
9 <
9 <=
9 >
9 >=
10 ==
10 !=
11 &
12 ^
13 ` `
14 &&
15 ` `
16 ?:
17 =
18 *=, /=, *=, +=, -=, <<=, >>=, &=, ` =, ^=`
19 throw
20 ,
Prev: 《C++ Primer》 拾遗 第 3 章 字符串、向量和数组
Next: 《C++ Primer》 拾遗 第 5 章 语句