0%

C++ Primer - 第 4 章 表达式

  1. 字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。

  2. 函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。

  3. 对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级(precedence)、结合律(associativity)以及运算对象的求值顺序(order of evaluation)。

  4. 类型转换中,小整数类型(如 boolcharshort 等)通常会被提升(promoted)成较大的整数类型,主要是 int

  5. 为已存在的运算符赋予另外一层含义,被称之为重载运算符(overloaded operator)。我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。

  6. C++ 的表达式要不然是右值,要不然就是左值:左值可以位于赋值语句的左侧,右值则不能。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)

  7. 在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。

    • 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
    • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
    • 内置解引用运算符、下标运算符、迭代器解引用运算符、stringvector 的下标运算符的求值结果都是左值。
    • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。

  8. 使用关键字 decltype 的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype 作用于该表达式(不是变量)得到一个引用类型。举个例子,假定 p 的类型是 int*,因为解引用运算符生成左值,所以 decltype(*p) 的结果是 int&。另一方面,因为取地址运算符生成右值,所以 decltype(&p) 的结果是 int**,也就是说,结果是一个指向整型指针的指针。

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

    1
    int i= f1() * f2();

    我们知道 f1 和 f2 一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底 f1 在 f2 之前调用还是 f2 在 f1 之前调用。对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。举个简单的例子,<< 运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:

    1
    2
    int i = 0;
    cout << i << " " << ++i << endl; // 未定义的

    因为程序是未定义的,所以我们无法推断它的行为。编译器可能先求 ++i 的值再求 i 的值,此时输出结果是 1 1;也可能先求 i 的值再求 ++i 的值,输出结果是 0 1:甚至编译器还可能做完全不同的操作。因为此表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。vscode 中输出结果为 1 1。有 4 种运算符明确规定了运算对象的求值顺序,第一种是逻辑与(&&)运算符,它规定先求左侧运算对象的值,只有当左侧运算对象的值为真时才继续求右侧运算对象的值。另外三种分别是逻辑或(ll)运算符、条件(?:)运算符和逗号(,)运算符。

  10. 以下两条经验准则对书写复合表达式有益:
    (1) 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求;
    (2) 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。第 2 条规则有一个重要例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。例如,在表达式 *++iter 中,递增运算符改变 iter 的值,iter(已经改变)的值又是解引用运算符的运算对象。此时(或类似的情况下),求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。

  11. 算术运算符的运算对象和求值结果都是右值。在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转换成同一类型。当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本。一元负号运算符对运算对象值取负后,返回其(提升后的)副本:

    1
    2
    bool b = true;
    bool b2 = -b; // b2 是 true

    布尔值不应该参与运算。对大多数运算符来说,布尔类型的运算对象将被提升为 int 类型。如上所示,布尔变量 b 的值为真,参与运算时将被提升成整数值 1,对它求负后的结果是 -1。将 -1 再转换回布尔值并将其作为 b2 的初始值,显然这个初始值不等于 0,转换成布尔值后应该为 1。所以,b2 的值是真!

  12. 假设某个机器的 short 类型占 16 位,则最大的 short 数值是 32767。在这样一台机器上,下面的复合赋值语句将产生溢出:

    1
    2
    3
    short short_value = 32767;  // 如果 short 类型占 16 位,则能表示的最大值是 32767
    short_value += 1; // 该计算导致溢出
    cout << "short value: " << short_value << endl;

    给 short_value 赋值的语句是未定义的,这是因为表示一个带符号数 32768 需要 17 位,但是 short 类型只有 16 位。很多系统在编译和运行时都不报溢出错误,像其他未定义的行为一样,溢出的结果是不可预知的。在我们的系统中,程序的输出结果是(vscode 中调试也是如此):

    1
    short_value: -32768

    该值发生了“环绕(wrapped around”,符号位本来是 0,由于溢出被改成了 1,于是结果变成一个负值。在别的系统中也许会有其他结果,程序的行为可能不同甚至直接崩溃。

  13. 整数相除结果还是整数,也就是说,如果商含有小数部分,直接弃除:

    1
    2
    int ival1 = 21 / 6;  // ival1 是 3,结果进行了删节,余数被抛弃掉了
    int ival2 = 21 / 7; // ival2 是 3,没有余数,结果是整数值

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

  14. C++ 语言的早期版本允许结果为负值的商向上或向下取整,C++11 新标准则规定商一律向 0 取整(即直接切除小数部分)

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

    1
    2
    3
    4
    5
    6
    7
    8
    21 % 6;      /*结果是3*/
    21 % 7; /*结果是0*/
    -21 % -8; /*结果是-5*/
    21 % -5; /*结果是1*/
    21 / 6; /*结果是3*/
    21 / 7; /*结果是3*/
    -21 / -8; /*结果是2*/
    21 /-5; /*结果是-4*/
  16. 下列表达式的结果是什么?

    1
    2
    30 / 3 * 21 % 5;   // 0
    -30 / 3 * 21 % 4; // -2
  17. 逻辑运算符和关系运算符的返回值都是布尔类型,运算对象和求值结果都是右值。

  18. 逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。

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

  19. 声明成引用类型可以避免对元素的拷贝。

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

    1
    if (i < j < k)  // 若k大于1则为真!
  21. 解释在下面的 if 语句中条件部分的判断过程。

    1
    2
    const char *cp = "Hello World";
    if (cp && *cp)

    先判断 cp 是否是空指针,再判断 cp 指向的是否是空字串。

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

  23. 赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。

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

    1
    2
    3
    k = (3.14);                           // 错误:窄化转换
    vector<int> vi; // 初始为空
    vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // vi现在含有10个元素了,值从0到9

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

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

  26. 赋值运算符满足右结合律,这一点与其他二元运算符不太一样:

    1
    2
    int ival, jval;
    ival = jval = 0; // 正确:都被赋值为0

    因为赋值运算符满足右结合律,所以靠右的赋值运算 jval = 0 作为靠左的赋值运算符的右侧运算对象。又因为赋值运算返回的是其左侧运算对象,所以靠右的赋值运算的结果(即 jval)被赋给了 ival。
    对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到:

    1
    2
    int ival, *pval;  // ival的类型是int;pval是指向int的指针
    ival = pval = 0; // 错误:不能把指针的值赋给int

    vscode 下提示错误:不能将 int * 类型的值分配到 int 类型的实体。但是可以强制转换:ival = int(pval = 0);此时,vscode 下提示警告:从 int *int 的指针截断。

  27. 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。任意一种复合运算符都完全等价于:

    1
    a = a op b;

    唯一的区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次。 这两次包括:一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。其实在很多地方,这种区别除了对程序性能有些许影响外几乎可以忽略不计。

  28. 递增运算符和递减运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。

  29. 如果我们想在一条复合表达式中既将变量加 1 或减 1 又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本。使用后置的递增运算符来控制循环输出一个 vector 对象内容直至遇到(但不包括)第一个负值为止:

    1
    2
    3
    4
    auto pbeg = v.begin();
    // 输出元素直至遇到第一个负值为止
    while (pbeg != v.end() && *beg >= 0)
    cout << *pbeg++ << endl; // 输出当前值并将 pbeg 向前移动一个元素

    后置递增运算符的优先级高于解引用运算符,因此 *pbeg++ 等价于 *(pbeg++)。pbeg++ 把 pbeg 的值加 1,然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。

  30. 观察下面的代码片段:

    1
    2
    3
    // 该循环的行为是未定义的!
    while (beg != s.end() && !isspace(*beg))
    *beg = toupper(*beg++); // 错误:该赋值语句未定义

    上述代码将产生未定义的行为。问题在于:赋值运算符左右两端的运算对象都用到了 beg,并且右侧的运算对象还改变了 beg 的值,所以该赋值语句是未定义的。编译器可能按照下面的任意一种思路处理该表达式:

    1
    2
    *beg = toupper(*beg);        // 如果先求左侧的值
    *(beg + 1) = toupper(*beg); // 如果先求右侧的值
  31. 点运算符获取类对象的一个成员,箭头运算符与点运算符有关,表达式 ptr -> mem 等价于 (*ptr).mem解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。箭头运算符作用于一个指针类型的运算对象,结果是一个左值。 点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。

  32. 假设 iter 的类型是 vector<string>::iterator,下述表达式哪些不合法,合法表达式的含义是什么?

    1
    2
    3
    4
    5
    6
    *iter++;          // 正确
    (*iter)++; // 正确
    *iter.empty(); // 错误
    iter->empty(); // 正确
    ++*iter; // 正确
    iter++->empty(); // 正确

    要区分开 *iter++(*iter)++:前者的解引用运算符作用于 iter 初始值的副本,后置递增运算符作用域 iter;后者的解引用运算符作用于 iter 本身,后置递增运算符作用于 iter 解引用后的值。

  33. 条件运算符(?:)允许我们把简单的 if-else 逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用:

    1
    cond ? expr1 : expr2;

    其中 cond 是判断条件的表达式,而 expr1 和 expr2 是两个类型相同或可能转换为某个公共类型的表达式。
    当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。

  34. 允许在条件运算符的内部嵌套另外一个条件运算符。也就是说,条件表达式可以作为另外一个条件运算符的 cond 或 expr。举个例子,使用一对嵌套的条件运算符可以将成绩分成三档:优秀(high pass)、合格(pass)和不合格(fail):

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

    条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合。因此在上面的代码中,靠右边的条件运算(比较成绩是否小于 60)构成了靠左边的条件运算的 : 分支。

  35. 观察下面的输出语句:

    1
    2
    3
    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 作为条件运算符的条件。也就是说,第二条表达式等价于:

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

    因为第三条表达式等价于下面的语句,所以它是错误的:

    1
    2
    cout << grade;  // 小于运算符的优先级低于移位运算符,所以先输出 grade
    cout << 60 ? "fail" : "pass"; // 然后比较 cout 和 60!
  36. 因为运算符的优先级问题,下面这条表达式无法通过编译。请指出问题在哪里,应该如何修改:

    1
    2
    string s = "word";
    string p1 = s + s[s.size() - 1] == 's' ? "" : "s";

    算数运算符优先级高于关系运算符,因此先计算 s + s[s.size() - 1],返回结果是一个 string 对象,无法与字符字面值常量 s 进行相等性比较,程序应改为:

    1
    string p1 = s + (s[s.size() - 1] == 's' ? "" : "s");
  37. 位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。一种名为 bitset 的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用于 bitset 类型。

  38. 一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。

  39. 移位运算符首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了。

  40. 左移运算符(<<)在右侧插入值为 0 的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为 0 的二进制位;如果该运算对象是带符号类型,在左侧插入符号位的副本或值为 0 的二进制位,如何选择要视具体环境而定。

  41. 对于位与运算符(&)来说,如果两个运算对象的对应位置都是 1 则运算结果中该位为 1,否则为 0。对于位或运算符(|)来说,如果两个运算对象的对应位置至少有一个为 1 则运算结果中该位为 1,否则为 0。对于位异或运算符(^)来说,如果两个运算对象的对应位置有且只有一个为 1 则运算结果中该位为 1,否则为 0。

  42. 移位运算符(又叫 IO 运算符)满足左结合律。移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。

  43. sizeof 运算符返回一条表达式或一个类型名字所占的字节数。sizeof 运算符满足右结合律,其所得的值是一个 size_t 类型的常量表达式。运算符的运算对象有两种形式:

    1
    2
    sizeof(type)
    sizeof expr

    在第二种形式中,sizeof 并不实际计算其运算对象的值。

  44. 关于 sizeof *p。首先,因为 sizeof 满足右结合律并且与 * 运算符的优先级一样,所以表达式按照从右向左的顺序组合。也就是说,它等价于 sizeof(*p)。其次,因为 sizeof 不会实际求运算对象的值,所以即使 p 是一个无效(即未初始化)的指针也不会有什么影响。sizeof 的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof 不需要真的解引用指针也能知道它所指对象的类型。C++11 新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是 sizeof 运算符无须我们提供一个县体的对象,因为要想知道类成员的大小无须真的获取该成员

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

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

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

    1
    2
    constexpr size_t sz = sizeof(ia) / sizeof(*ia);
    int arr2[sz]; // 正确:sizeof 返回一个常量表达式

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

  46. 观察下面的例子:

    1
    int ival = 3.541 + 3;  // 编译器可能会警告该运算损失了精度

    算数类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。在上面的例子中,3 转换成 double 类型,然后执行浮点数加法,所得结果的类型是 double。在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。仍以这个例子说明,加法运算得到的 double 类型的结果转换成 int 类型的值,这个值被用来初始化 ival。由 doubleint 转换时忽略掉了小数部分,上面的表达式中,数值 6 被赋给了 ival。

  47. 何时发生隐式类型转换:

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

  48. 算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是 long double,那么不论另外一个运算对象的类型是什么都会转换成 long double。还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。

  49. 整型提升(integral promotion)负责把小整数类型转换成较大的整数类型。对于 boolcharsigned charunsigned charshortunsigned short等 类型来说,只要它们所有可能的值都能存在int里,它们就会提升成 int 类型;否则,提升成 unsigned int 类型。就如我们所熟知的,布尔值 false 提升成 0、true 提升成 1。较大的 char 类型(wchar_tchar16_tchar32_t)提升成 intunsigned intlongunsigned longlong longunsigned long long 中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。

  50. 如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。像往常一样,首先执行整型提升。如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的)运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型。如果一个运算对象是无符号类型,另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是 unsigned intint,则int类型的运算对象转换成 unsigned int 类型。需要注意的是,如果 int 型的值恰好为负值,可能得到意想不到的结果。剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是longunsigned int,并且 intlong 的大小相同,则 long 类型的运算对象转换成unsigned int 类型;如果 long 类型占用的空间比 int 更多,则 unsigned int 类型的运算对象转换成 long 类型。

  51. 其它隐式类型转换:

    • 数组转换成指针。在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeoftypeid 等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。当在表达式中使用函数类型时会发生类似的指针转换;
    • 指针的转换。C++还规定了几种其他的指针转换方式,包括常量整数值 0 或者字面值nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成 void*;指向任意对象的指针能转换成 const void*
    • 转换成布尔类型。存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为 0,转换结果是 false;否则转换结果是 true;
    • 转换成常量。允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果 T 是一种类型,我们就能将指向 T 的指针或引用分别转换成指向const T 的指针或引用

      1
      2
      3
      4
      int i;
      const int &j = i; // 非常量转换成 const int 的引用
      const int *p = &i; // 非常量的地址转换成 const 的地址
      int &r = j, *q = p; // 错误:不允许 const 转换成非常量
    • 类类型定义的转换。有下面的程序片段:

      1
      string s; while (cin >> s)

      IO 库定义了从 istream 向布尔值转换的规则,根据这一规则,cin 自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是 true;相反,如果最后一次读入不成功,转换得到的布尔值是 false

  52. 现代 C++ 中有四种强制类型转换(cast)形式:static_castdynamic_castconst_castreinterpret_castdynamic_cast 支持运行时类型识别。

    • static_cast。任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast。当需要把一个较大的算术类型赋值给较小的类型时,static_cast 非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。static_cast 对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast找回存在于void_ 指针中的值:

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

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

    • const_castconst_cast 只能改变运算对象的底层 const

      1
      2
      const char *pc;
      char *p = const_cast<char*>(pc); // 正确:但是通过 p 值是未定义的行为

      对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉 const 性质(cast awaythe const)”。一旦我们去掉了某个对象的 const 性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用 const_cast 执行写操作就会产生未定义的后果。 只有 const_cast 能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用 const_cast 改变表达式的类型:

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

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

    • reinterpret_castreinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。在工程应用中编写 udp2lcm 程序时曾使用过这种强制类型转换。有如下代码片段:

      1
      2
      int *ip;
      char *pc = reinterpret_cast<char*>(ip);

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

      1
      string str(pc);

      可能导致异常的运行时行为。使用 reinterpret_cast 是非常危险的,用 pc 初始化 str 的例子很好地证明了这一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个 int 的地址初始化 pc 时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。接下来再使用 pc 时就会认定它的值是 char* 类型,编译器没法知道它实际存放的是指向 int 的指针。最终的结果就是,在上面的例子中虽然用 pc 初始化 str 没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将 ip 强制转换成 pc 的语句和用 pc 初始化 string 对象的语句分属不同文件就更是如此。

  53. 当我们在某处执行旧式的强制类型转换时,如果换成 const_caststatic_cast 也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与 reinterpret_cast 类似的功能。

  54. 用命名的强制类型转换改写下列旧式的转换语句:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int i;
    double d;
    const string *ps;
    char *pc;
    void *pv;
    pv = (void*)ps; // (a)
    i = int(*pc); // (b)
    pv = &d; // (c)
    pc = (char*)pv; // (d)

    对于语句(a),需要先使用 const_cast 去掉 ps 的底层 const 性质,然后再使用 static_cast 进行类型转换;其它语句直接使用 static_cast 进行类型转换即可:

    1
    2
    3
    4
    pv = static_cast<void *>(const_cast<string *>(ps));
    i = static_cast<int>(*pc);
    pv = static_cast<void *>(&d);
    pc = static_cast<char *>(pv);
  55. 关键术语表:

    • const_cast 一种涉及 const 的强制类型转换。将底层 const 对象转换成对应的非常量类型,或者执行相反的转换。
    • dynamic_cast 和继承及运行时类型识别一起使用。
    • 整型提升(integral promotion) 把一种较小的整数类型转换成与之最接近的较大整数类型的过程。不论是否真的需要,小整数类型(即 shortchar 等)总是会得到提升。
    • 短路求值(short-circuit evaluation) 是一个专有名词,描述逻辑与运算符和逻辑或运算符的执行过程。如果根据运算符的第一个运算对象就能确定整个表达式的结果,求值终止,此时第二个运算对象将不会被求值。
    • , 运算符(, operator) 逗号运算符,是一种从左向右求值的二元运算符。逗号运算符的结果是右侧运算对象的值,当且仅当右侧运算对象是左值时逗号运算符的结果是左值
    • ++ 运算符(++ operator) 递增运算符。包括两种形式:前置版本和后置版本。前置递增运算符得到一个左值,它给运算符加 1 并得到运算对象改变后的值。后置递增运算符得到一个右值,它给运算符加 1 并得到运算对象原始的、未改变的值的副本。注意:即使迭代器没有定义+运算符,也会有++运算符。
    • - - 运算符(- - operator) 递减运算符。包括两种形式:前置版本和后置版本。前置递减运算符得到一个左值,它从运算符减 1 并得到运算对象改变后的值。后置递减运算符得到一个右值,它从运算符减 1 并得到运算对象原始的、未改变的值的副本。注意:即使迭代器没有定义 - 运算符,也会有 - - 运算符。

Thank you for your donate!