1Effective C++(第3版)

让自己习惯C++

C++思维

const初始化编译器默认函数拷贝语义

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

第1章:让自己习惯C++(Accustoming Yourself to C++)

一、导读

1.1 本章学习目标

第1章是《Effective C++》的开篇,Scott Meyers 在此帮助读者从 C 的思维模式转换到 C++ 的思维模式。本章涵盖了 C++ 中最基础也最重要的编程习惯:使用 const、初始化、理解编译器默认行为等。

本章核心学习目标:

  • 理解 C++ 的编程范式:C++ 不是"更好的 C",而是多范式语言
  • 掌握 const 的正确使用:const 指针、const 引用、const 成员函数
  • 理解初始化的重要性:为什么应该用初始化列表而非赋值?
  • 掌握编译器默认生成的函数:默认构造、拷贝构造、拷贝赋值、析构
  • 理解 C++ 的"规则"思维:为什么 C++ 有这么多规则?

1.2 为什么本章如此重要

C++ 是一门复杂的语言,但大多数问题源于对基本规则的忽视。Meyers 在开篇就强调:先养成正确的习惯,再学习高级技巧。

1.3 核心问题引导

为什么应该尽量用 const 和 enum 替代 #define?

C++ 的初始化语法有几种?各有什么优劣?

编译器在什么情况下会生成默认函数?如何禁止?


二、核心概念详解

2.1 用 const 和 enum 替代 #define

cpp
// 不好: 使用 #define
#define ASPECT_RATIO 1.653
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// 好: 使用 const
const double AspectRatio = 1.653;

// 好: 使用 enum (编译期常量)
class GameCharacter {
    enum { NumTurns = 5 };  // 编译期常量,不占内存
    int scores[NumTurns];
};

// #define 的问题:
// 1. 调试器看不到符号名(只看到 1.653)
// 2. 宏展开可能导致意外行为
//    MAX(a++, b) → ((a++) > (b) ? (a++) : (b)) → a 被递增两次!
// 3. 没有类型检查

2.2 初始化 vs 赋值

cpp
class PhoneNumber {
    std::string name;
    int age;
    std::string phone;

    // 不好: 赋值(先默认构造,再赋值)
    PhoneNumber(std::string n, int a, std::string p) {
        name = n;    // 赋值
        age = a;     // 赋值
        phone = p;   // 赋值
    }

    // 好: 初始化列表(直接构造)
    PhoneNumber(std::string n, int a, std::string p)
        : name(n), age(a), phone(p) {}  // 初始化

    // 更好: C++11 统一初始化
    PhoneNumber(std::string n, int a, std::string p)
        : name{std::move(n)}, age{a}, phone{std::move(p)} {}
};

// 为什么初始化列表更好?
// 1. const 成员只能初始化,不能赋值
// 2. 引用成员只能初始化,不能赋值
// 3. 没有默认构造函数的类成员只能初始化
// 4. 效率更高(避免默认构造 + 赋值的双重开销)

2.3 了解编译器默认生成的函数

cpp
class Empty {
    // 编译器默认生成:
    // Empty() {}                          // 默认构造
    // Empty(const Empty& rhs) {}          // 拷贝构造
    // ~Empty() {}                         // 析构
    // Empty& operator=(const Empty& rhs) {} // 拷贝赋值
};

// 编译器只在需要时生成,且只在没有用户定义版本时生成

// C++11 的 =default 和 =delete:
class NonCopyable {
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

class MoveableOnly {
    MoveableOnly(MoveableOnly&&) = default;  // 显式要求生成移动构造
};

2.4 使用 const 修饰成员函数

cpp
class TextBlock {
    std::string text;
public:
    // const 成员函数: 承诺不修改对象状态
    const char& operator[](std::size_t pos) const {
        return text[pos];
    }
    
    // 非 const 版本
    char& operator[](std::size_t pos) {
        return text[pos];
    }
};

const TextBlock ctb("Hello");
ctb[0] = 'h';  // 编译错误: ctb 是 const,调用 const 版本的 operator[]

TextBlock tb("World");
tb[0] = 'w';   // OK: 调用非 const 版本

三、实战案例

3.1 案例一:const 正确性的实践

cpp
class Matrix {
    std::vector<std::vector<double>> data;
public:
    Matrix(int rows, int cols) : data(rows, std::vector<double>(cols, 0.0)) {}
    
    // const 版本
    double at(int r, int c) const {
        return data[r][c];
    }
    
    // 非 const 版本
    double& at(int r, int c) {
        return data[r][c];
    }
    
    // const 成员函数可以调用其他 const 成员函数
    double sum() const {
        double total = 0;
        for (const auto& row : data)
            for (double val : row)
                total += val;
        return total;
    }
    
    // 错误: const 函数不能调用非 const 函数
    // void print() const {
    //     at(0, 0) = 1.0;  // 错误: at 的非 const 版本修改了对象
    // }
};

void processMatrix(const Matrix& m) {
    // m 是 const 引用,只能调用 const 成员函数
    double val = m.at(0, 0);  // OK
    // m.at(0, 0) = 1.0;      // 编译错误
    double s = m.sum();       // OK
}

四、本章小结

用 const 和 enum 替代 #define,获得更好的类型安全和调试体验

使用初始化列表而非赋值,效率更高且支持 const 和引用成员

了解编译器默认生成的函数,必要时用 =delete 禁止

const 成员函数承诺不修改对象状态,是接口设计的重要工具


五、思考与练习

思考题

为什么 const 对象只能调用 const 成员函数?

C++11 的统一初始化语法 {} 相比 () 有什么优势?

编程练习

设计一个不可变(immutable)的字符串类,所有操作返回新对象。

实现一个矩阵类,正确使用 const 修饰所有成员函数。


1/9让自己习
0%