4C++ Primer(第5版)

表达式

运算与求值

运算符类型转换求值顺序

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第4章:表达式(Expressions)

一、导读

1.1 本章学习目标

第4章讲解C++的表达式和运算符。正确理解表达式的求值规则是避免bug的关键。本章核心目标:

  • 掌握运算符优先级和结合性:复杂表达式如何求值?
  • 理解逻辑运算符的短路求值:&& 和 || 的特殊行为
  • 掌握递增递减运算符:前置和后置的区别
  • 理解类型转换规则:隐式转换和显式转换
  • 避免未定义行为:哪些写法是危险的?
  • 学会使用C++风格的类型转换:static_cast、const_cast等

1.2 为什么本章如此重要

表达式的求值规则看似简单,实则暗藏陷阱。运算符优先级错误、未定义行为、隐式类型转换导致的意外结果——这些都是C++程序中最常见的bug来源。理解这些规则,才能写出正确的代码。

1.3 核心问题引导

i = ++i + i++ 的结果是什么?为什么这种写法是未定义行为?

为什么 && 和 || 被称为"短路"运算符?有什么实际应用?

static_cast 和 reinterpret_cast 有什么区别?

为什么C++不推荐C风格的类型转换?

成员访问运算符 . 和 -> 有什么区别?


二、核心概念详解

4.1 运算符优先级和结合性

cpp
#include <iostream>

int main() {
    // 优先级示例
    int a = 2 + 3 * 4;     // 14(乘法优先于加法)
    int b = (2 + 3) * 4;   // 20(括号改变优先级)
    
    // 结合性示例
    int c = 10 - 3 - 2;    // 5(左结合:(10-3)-2)
    
    // 赋值运算符是右结合
    int x, y, z;
    x = y = z = 42;        // 从右到左赋值
    
    // 常见优先级陷阱
    int flags = 0x03;
    if (flags & 0x01 == 1)  // 错误!==优先于&
        std::cout << "bit 0 set" << std::endl;
    
    if ((flags & 0x01) == 1)  // 正确
        std::cout << "bit 0 set" << std::endl;
    
    return 0;
}

4.2 逻辑运算符与短路求值

cpp
#include <iostream>

int main() {
    // 短路求值:&&和||不一定会计算右侧表达式
    int x = 0;
    
    // && 短路:左侧为false时不计算右侧
    if (x != 0 && 10 / x > 1) {
        // 安全:x!=0为false时,不会执行10/x
    }
    
    // || 短路:左侧为true时不计算右侧
    int* ptr = nullptr;
    if (ptr == nullptr || *ptr > 0) {
        // 安全:ptr为空时不执行*ptr
    }
    
    // 逗号运算符:从左到右求值,返回最右侧的值
    int i = (1, 2, 3);  // i = 3
    std::cout << i << std::endl;
    
    return 0;
}

4.3 递增递减运算符

cpp
#include <iostream>

int main() {
    int i = 0;
    
    // 前置递增:先递增,再返回
    std::cout << ++i << std::endl;  // 输出1,i变为1
    
    // 后置递增:先返回,再递增
    std::cout << i++ << std::endl;  // 输出1,i变为2
    std::cout << i << std::endl;    // 输出2
    
    // 在表达式中混合使用(危险!)
    int a = 5;
    // int b = a++ + ++a;  // 未定义行为!不要这样写
    
    // 推荐:分开写
    int c = a++;  // c = 5, a = 6
    int d = ++a;  // a = 7, d = 7
    
    // 在循环中的使用
    for (int j = 0; j < 10; ++j) {  // 推荐前置递增
        std::cout << j << " ";
    }
    
    return 0;
}

4.4 成员访问运算符

cpp
#include <iostream>
#include <string>

struct Point {
    int x, y;
    void display() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Point p = {3, 4};
    Point* pp = &p;
    
    // 点运算符:通过对象访问
    std::cout << p.x << std::endl;
    p.display();
    
    // 箭头运算符:通过指针访问
    std::cout << pp->x << std::endl;
    pp->display();
    
    // pp->x 等价于 (*pp).x
    std::cout << (*pp).x << std::endl;
    
    return 0;
}

4.5 类型转换

cpp
#include <iostream>

int main() {
    // 隐式类型转换
    int i = 42;
    double d = i;     // int -> double(安全)
    double pi = 3.14;
    int n = pi;       // double -> int(截断,可能丢失精度)
    std::cout << n << std::endl;  // 3
    
    // 有符号和无符号的转换(危险!)
    unsigned int u = 10;
    int s = -1;
    if (s < u) {
        std::cout << "s < u" << std::endl;
    } else {
        std::cout << "s >= u" << std::endl;  // 会执行这里!
        // 因为-1被转换为unsigned int,变成很大的正数
    }
    
    // C++风格的显式转换
    double x = 3.14;
    
    // static_cast:安全的类型转换
    int y = static_cast<int>(x);  // 推荐
    
    // const_cast:去除const属性
    const int ci = 42;
    int& ref = const_cast<int&>(ci);  // 危险
    
    // reinterpret_cast:底层位模式转换
    int* ptr = nullptr;
    long addr = reinterpret_cast<long>(ptr);
    
    // dynamic_cast:安全的向下转型(用于多态)
    // 需要虚函数
    
    // 不推荐C风格转换
    int z = (int)x;   // C风格
    int w = int(x);   // 函数风格
    
    return 0;
}

4.6 未定义行为

cpp
#include <iostream>

int main() {
    // 以下都是未定义行为(UB),结果不可预测
    
    // 1. 有符号整数溢出
    int max_int = 2147483647;
    // int overflow = max_int + 1;  // UB
    
    // 2. 除以零
    // int div_zero = 1 / 0;  // UB
    
    // 3. 数组越界
    int arr[5] = {1, 2, 3, 4, 5};
    // int oob = arr[10];  // UB
    
    // 4. 空指针解引用
    // int* null_ptr = nullptr;
    // int val = *null_ptr;  // UB
    
    // 5. 同一表达式中多次修改同一变量
    int i = 0;
    // int bad = i++ + i++;  // UB
    
    // 6. 使用未初始化的变量
    // int uninit;
    // std::cout << uninit << std::endl;  // UB
    
    return 0;
}

三、实战案例

4.1 位运算应用

cpp
#include <iostream>

int main() {
    // 位运算常用于标志位操作
    enum Flags {
        READ = 1 << 0,     // 0x01
        WRITE = 1 << 1,    // 0x02
        EXECUTE = 1 << 2   // 0x04
    };
    
    int perms = READ | WRITE;  // 设置读和写权限
    
    // 检查标志
    if (perms & READ)
        std::cout << "Has read permission" << std::endl;
    
    // 设置标志
    perms |= EXECUTE;
    
    // 清除标志
    perms &= ~WRITE;
    
    // 切换标志
    perms ^= READ;  // 如果已设置则清除,否则设置
    
    std::cout << "Permissions: " << perms << std::endl;
    
    // 位运算技巧
    int x = 42;
    std::cout << "x & (x-1): " << (x & (x-1)) << std::endl;  // 清除最低位的1
    std::cout << "x & (-x): " << (x & (-x)) << std::endl;    // 提取最低位的1
    
    return 0;
}

4.2 条件运算符

cpp
#include <iostream>
#include <algorithm>

int main() {
    int a = 10, b = 20;
    
    // 三元运算符
    int max_val = (a > b) ? a : b;
    std::cout << "Max: " << max_val << std::endl;
    
    // 嵌套三元运算符(不推荐,可读性差)
    int score = 85;
    char grade = (score >= 90) ? 'A' :
                 (score >= 80) ? 'B' :
                 (score >= 70) ? 'C' : 'D';
    std::cout << "Grade: " << grade << std::endl;
    
    // 使用std::max和std::min更清晰
    int max2 = std::max(a, b);
    int min2 = std::min(a, b);
    
    return 0;
}

四、本章小结

第4章讲解了C++表达式的核心规则:

运算符优先级:理解优先级和结合性,使用括号明确意图。

短路求值:&& 和 || 不一定会计算右侧表达式,可用于安全检查。

递增递减:前置和后置有区别,避免在表达式中混合使用。

类型转换:优先使用C++风格的显式转换(static_cast等),避免隐式转换陷阱。

未定义行为:溢出、除零、越界、空指针等都会导致UB,必须避免。


五、思考与练习

思考题

为什么 sizeof(char) 总是1?sizeof('a') 在C和C++中结果不同吗?

短路求值在什么场景下非常有用?

为什么同一表达式中多次修改同一变量是未定义行为?

static_cast 和 dynamic_cast 有什么区别?各自适用什么场景?

编程练习

编写程序验证运算符优先级(如 2 + 3 * 4 的结果)。

使用位运算实现一个简单的权限管理系统。

编写程序找出各种类型在当前平台上的大小。


4/12表达式
0%