一文读懂《Effective C++ 第三版》的55条黄金法则

一文读懂《Effective C++ 第三版》的55条黄金法则

按书中章节顺序,系统梳理 Scott Meyers 的 55 条条款,对初学者易混淆处配代码示例。
摘要: 本文是《Effective C++》第三版(Scott Meyers 著)的导读,按原书 9 章顺序逐条梳理 55 个 C++ 编程黄金法则。全书以「C++ 是四个次语言(C、OO、Template、STL)的联邦」为认知框架,涵盖习惯 C++、资源管理、构造析构赋值、接口设计、继承多态、模板泛型编程、自定义内存管理与现代标准库。每条条款以「核心思想 + 易错代码示例 + 现代 C++ 对照」呈现,适合已掌握 C++ 基本语法想写出工业级代码的开发者。


导语:这本书是什么,给谁看?

《Effective C++》第三版(2005)是 Scott Meyers 的代表作,被誉为 C++ 程序员的「必读三剑客」之一。全书围绕 55 条具体、可操作的指导原则 展开,回答一个问题:

C++ 是一门强大但容易误用的语言,怎样用它写出正确、高效、可维护的代码?

本书的技术基准与阅读说明

  • 本书基于 C++98 标准 (第三版前言写于 2005 年)。其中第 54 条提到的 TR1,后来大部分内容进入了 C++11/14 标准库(如 std::shared_ptrstd::functionstd::bind)。
  • 本文在每个相关条款处,会用 // [C++11+] 旁注现代 C++ 的改进写法,帮助你在学原理的同时对接现代实践。
  • 适合人群:已经掌握 C++ 基本语法(会写类、指针、继承),但想写出「真正像样的 C++」的开发者。

一个贯穿全书的认知框架:C++ 不是单一语言

Meyers 在条款 1 提出一个关键观点:与其把 C++ 看成一门语言,不如看成 四个「次语言(sublanguage)」的联邦。理解这四块,是理解全书 55 条条款的前提------因为同一条规则,在不同次语言里可能完全不同。

次语言 内涵 典型规则风格
C 区块、语句、预处理器、内置数据类型、指针、数组 指针、数组用 raw,无资源管理
Object-Oriented C++ 类、封装、继承、多态、虚函数 虚析构、public 继承、纯虚函数
Template C++ 模板、泛型编程、TMP(模板元编程) typename、代码膨胀、traits
STL 容器、迭代器、算法、函数对象 迭代器、operator<size()

为什么这个划分重要? 因为它解释了为什么 C++ 的规则看起来「自相矛盾」:在 C 次语言里,传值往往更高效;但在 OO 次语言里,传 const 引用才正确;到了 STL,迭代器和函数对象又该传值。本书的 55 条,本质就是在帮你判断「我现在在哪个次语言,该用哪套规则」。


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

条款 1:视 C++ 为一个语言联邦

即上文介绍的「四大次语言」。核心结论:不要期望用一套统一的心智模型来理解 C++,而要分场景切换。

条款 2:尽量以 constenuminline 替换 #define

#define 在预处理阶段就被替换掉,它根本不进入符号表,导致调试时只能看到魔数而看不到名字,也没有作用域和类型安全。

cpp 复制代码
// ===== 目标:避免 #define 的各种坑 =====

// ❌ 错误:预处理器宏,调试看不到名字 ASPECT_RATIO
#define ASPECT_RATIO 1.653

// ✅ 正确:用 const 常量
const double AspectRatio = 1.653; // 进入符号表,可调试、有类型

// ---------- 特殊情况 1:class 内部常量 ----------
class GamePlayer {
private:
  // enum hack:在类内定义编译期常量,且不占对象内存、不能取地址
  // ⚠ 误区:直接写 static const int NumTurns = 5; 老编译器可能不支持「类内初值」
  enum { NumTurns = 5 };
  int scores[NumTurns]; // 数组大小需要编译期常量,enum hack 最稳妥
};

// ---------- 特殊情况 2:宏函数 → inline ----------
// ❌ 危险:宏函数有参数多次求值、作用域问题
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

// ✅ 安全:inline 模板函数,类型安全、参数只求值一次
template <typename T>
inline void callWithMax(const T& a, const T& b) { // const& 避免拷贝
  f(a > b ? a : b);
}

初学者常见误解 :以为 const 常量一定好于宏。其实有两种情况 #define 无法被简单替代:一是类内编译期整型常量 (用 enum hack 最保险);二是条件编译#ifdef),这仍是预处理器的专利。

条款 3:尽可能使用 const

const 出现在星号左边,被指物是常量;出现在右边,指针本身是常量;两边都有,都是常量。

cpp 复制代码
// ===== 目标:理解 const 的两种"常量性" =====

char greeting[] = "Hello";
char* p = greeting; // 非常量指针,非常量数据
const char* p = greeting; // 非常量指针,常量数据(const 在 * 左)
char* const p = greeting; // 常量指针,非常量数据(const 在 * 右)
const char* const p = greeting; // 常量指针,常量数据

// ---------- bitwise constness vs logical constness ----------
class TextBlock {
public:
  // 编译器认定的 const:对象任何 bit 都不能变(bitwise constness)
  const char& operator[](std::size_t position) const {
    return text[position];
  }
  char& operator[](std::size_t position) {
    return text[position];
  }
private:
  char* text;
};

// ⚠ 难点:logical constness------"在客户端看来没变,但内部缓存可以变"
// 例:一个文本对象,内容不变,但首次访问时统计字数并缓存
class CTextBlock {
public:
  std::size_t length() const {
    if (!lengthIsValid) { // 编译期 const,却想修改成员
      textLength = std::strlen(pText);
      lengthIsValid = true;
    }
    return textLength;
  }
private:
  char* pText;
  std::size_t textLength; // mutable:即使在 const 成员函数中也可修改
  mutable bool lengthIsValid; // 这正是 logical constness 的实现手段
};

核心要点

  1. bitwise(物理)常量性:编译器默认执行的检查------对象任何一个字节都不能变。
  2. logical(逻辑)常量性 :程序员认为合理的检查------「调用者看得到的部分不变」,但内部缓存、计数器可以变。用 mutable 实现。
  3. const 成员函数的重载const 对象调用 const 版本,非 const 对象调用非 const 版本------这是实现「只读/可写」两套接口的标准手法。

条款 4:确定对象被使用前已先被初始化

C++ 的初始化规则极不直观:成员变量的初始化发生在进入构造函数体之前。构造函数体内的赋值,其实已是「第二次」操作。

cpp 复制代码
// ===== 目标:用初始化列表代替构造函数体内赋值 =====

class PhoneNumber { /*...*/ };
class ABEntry { // Address Book Entry
public:
  // ❌ 低效:先默认构造,再赋值(两次操作)
  ABEntry(const std::string& name, const std::string& addr,
          const std::list<PhoneNumber>& phones) {
    theName = name; // 这是赋值,不是初始化!默认构造已先发生
    theAddress = addr;
    thePhones = phones;
    numTimesConsulted = 0;
  }

  // ✅ 高效:初始化列表,直接用参数拷贝构造(一次操作)
  ABEntry(const std::string& name, const std::string& addr,
          const std::list<PhoneNumber>& phones)
  : theName(name), // 冒号后是成员初始化列表
    theAddress(addr),
    thePhones(phones),
    numTimesConsulted(0) {} // 内置类型初始化列表和赋值效率差不多,但为统一风格也写上
private:
  std::string theName;
  std::string theAddress;
  std::list<PhoneNumber> thePhones;
  int numTimesConsulted;
};

两个易错点

  1. 初始化列表的顺序必须与成员声明顺序一致------因为 C++ 总是按声明顺序初始化成员,与你在列表里写的顺序无关。乱写虽然能编译,但可能埋下隐患。

  2. 跨编译单元的静态对象初始化次序未定义 ------「非局部静态对象」的初始化顺序在不同翻译单元间是无定义的。解决办法是 「单例模式」包装

cpp 复制代码
// ===== 目标:解决跨编译单元静态对象初始化次序问题 =====

// FileSystem 是个全局静态对象,在 tfs.cpp 里定义
// 另一个目录编译单元 Directory.cpp 里要用它,但谁先初始化?未定义!

// ✅ 解法:用函数返回引用,替代全局对象(Meyers 单例)
class FileSystem { /* ... */ };
FileSystem& tfs() { // 代替直接的全局对象
  static FileSystem fs; // 局部 static,首次调用时才构造
  return fs; // 之后直接返回引用
}

class Directory {
public:
  Directory() {
    std::size_t disks = tfs().numDisks(); // 通过函数访问,保证 tfs 已初始化
  }
};
Directory& tempDir() {
  static Directory td;
  return td;
}
// 现在无论 tfs 和 tempDir 在哪个编译单元,调用时对方必然已构造好。

第 2 章 构造、析构、赋值运算(Constructors, Destructors, and Assignment Operators)

这 8 条(条款 5--12)是全书最常考、最实用的部分。掌握它们,能避免 C++ 工程中绝大多数的资源泄漏和诡异崩溃。

条款 5:了解 C++ 默默编写并调用哪些函数

编译器会「悄悄」为空类生成 6 个函数(C++11 后还有移动构造/移动赋值,本书按 C++98 讲 4 个):

cpp 复制代码
// ===== 目标:看清编译器自动生成的"隐形函数" =====

class Empty {};

// 你以为 Empty 是空类,编译器其实会生成:
class Empty {
public:
  Empty() {} // 默认构造(仅当没有任何构造函数时)
  Empty(const Empty& rhs) {} // 拷贝构造
  ~Empty() {} // 析构(非虚!)
  Empty& operator=(const Empty& rhs) {} // 拷贝赋值
  // [C++11+] 还会有:Empty(Empty&&) 和 operator=(Empty&&),条件性地生成
};

Empty e1; // 触发默认构造 + 析构
Empty e2(e1); // 触发拷贝构造
e2 = e1; // 触发拷贝赋值

// ⚠ 编译器何时"拒绝"生成 operator=?
// 当类含 reference 成员、const 成员,或基类把 operator= 设为 private 时
class NamedObject {
  std::string& nameRef; // 引用成员:必须初始化且不能再绑定别人
  const int value; // const 成员:不能修改
  // 编译器拒绝生成 operator=(因为无法合法赋值这两个成员)
};

要点:编译器「按需」生成这些函数------只有当你的代码真的用到时,它们才会被创建出来。赋值运算符生成的条件最苛刻(参考成员、const 成员等会导致拒绝生成)。

条款 6:若不想使用编译器自动生成的函数,就该明确拒绝

经典场景:你想禁止某个类的拷贝(如单例、资源管理类)。

cpp 复制代码
// ===== 目标:禁止拷贝 =====

// 方法1(原书写法):把拷贝构造和赋值声明为 private 且不实现
class HomeForSale {
private:
  HomeForSale(const HomeForSale&); // 只声明,不定义
  HomeForSale& operator=(const HomeForSale&); // 谁调用谁链接报错
public:
  HomeForSale() {}
};

// 方法2(更优雅):继承一个专门用于"禁用拷贝"的基类
class Uncopyable { // 一个可复用的工具基类
protected:
  Uncopyable() {}
  ~Uncopyable() {}
private:
  Uncopyable(const Uncopyable&);
  Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable { // 继承后自动禁用拷贝
  // 不用再写任何东西
};

// [C++11+] 现代写法:用 = delete,最清晰
class HomeForSale11 {
public:
  HomeForSale11() = default;
  HomeForSale11(const HomeForSale11&) = delete; // 显式删除
  HomeForSale11& operator=(const HomeForSale11&) = delete;
};

条款 7:为多态基类声明虚析构函数

这是 C++ 最经典的「坑」之一:用基类指针 delete 派生类对象时,若析构函数非虚,会导致派生类部分不析构------资源泄漏。

cpp 复制代码
// ===== 目标:理解虚析构函数的必要性 =====

class TimeKeeper { // ⚠ 反例:基类析构非虚
public:
  TimeKeeper() {}
  ~TimeKeeper() {} // 非虚!
};
class AtomicClock : public TimeKeeper {};

TimeKeeper* ptk = getTimeKeeper(); // 返回派生类指针,静态类型是基类
delete ptk; // 未定义行为:只析构了基类部分,派生类资源泄漏

// ✅ 正确:多态基类的析构函数必须是虚的
class TimeKeeper2 {
public:
  TimeKeeper2() {}
  virtual ~TimeKeeper2() {} // 虚析构,delete 基类指针会正确调用派生类析构
};

// ⚠ 误区:不是所有类都该有虚析构!
// 如果类不是基类、不需要多态,加 virtual 会引入虚函数表指针,增加对象体积。
// 所以:只有当一个类至少含有一个虚函数(即设计为多态基类)时,才该有虚析构。

条款 8:别让异常逃离析构函数

析构函数中的异常是灾难------因为它可能发生在另一个异常展开的过程中,导致程序直接终止。

cpp 复制代码
// ===== 目标:析构函数吞掉异常,把异常处理转交给普通函数 =====

class DBConnection {
public:
  void close(); // 可能抛异常
};

// ✅ 正确做法:提供一个普通函数让用户处理异常,析构函数只兜底
class DBConn {
public:
  void close() { // 给用户的机会:主动调用可处理异常
    db.close();
    closed = true;
  }
  ~DBConn() {
    if (!closed) {
      try { db.close(); } // 析构时若用户忘了 close,兜底
      catch (...) {
        // 记录日志、吞下异常,不要让它传播
        std::abort(); // 或记录日志后静默
      }
    }
  }
private:
  DBConnection db;
  bool closed;
};

核心思想 :析构函数必须不抛异常。如果某个操作可能抛异常,应提供一个普通成员函数让用户显式调用并处理;析构函数只作为「最后兜底」,内部 try/catch 吞掉异常。

条款 9:绝不在构造和析构过程中调用虚函数

这是初学者最常踩的雷:在基类构造函数里调用虚函数,期望触发派生类的版本------但 C++ 在构造期间,「虚函数」其实不是虚的。

cpp 复制代码
// ===== 目标:理解构造期"虚函数失效" =====

class Transaction { // 基类:所有交易的基类
public:
  Transaction() { init(); } // 构造时调用虚函数
  virtual void logTransaction() const = 0; // 纯虚:记录具体交易
private:
  void init() { logTransaction(); } // 问题源头
};

class BuyTransaction : public Transaction {
public:
  virtual void logTransaction() const override { /* 记录买入 */ }
};

BuyTransaction b; // ⚠ 你以为会调 BuyTransaction::logTransaction
                  //    实际调用的是 Transaction::logTransaction(纯虚,链接错误或未定义行为)

为什么? 构造 BuyTransaction 时,先构造基类 Transaction 部分。在基类构造函数执行期间,对象的动态类型被视为 Transaction,而非 BuyTransaction------因为此时派生类还没构造,根本不存在。这同理适用于析构。

正确做法:让派生类把信息传给基类构造函数,而不是靠虚函数回调。

cpp 复制代码
class Transaction {
public:
  explicit Transaction(const std::string& logInfo) { logTransaction(logInfo); }
  void logTransaction(const std::string& info) const { /* 非虚,安全 */ }
};
class BuyTransaction : public Transaction {
public:
  BuyTransaction() : Transaction("Create BuyTransaction") {} // 把信息传上去
};

条款 10:令 operator= 返回一个 reference to *this

约定俗成:赋值运算符应返回当前对象的引用,以支持链式赋值 a = b = c

cpp 复制代码
class Widget {
public:
  Widget& operator=(const Widget& rhs) {
    // ...
    return *this; // 返回 *this 的引用
  }
  Widget& operator+=(const Widget& rhs) { // 所有赋值相关运算符都遵循此约定
    // ...
    return *this;
  }
};

条款 11:在 operator= 中处理「自我赋值」

cpp 复制代码
// ===== 目标:自赋值安全 + 异常安全 =====

class Bitmap { /* ... */ };
class Widget {
public:
  Widget& operator=(const Widget& rhs) {
    // ❌ 危险写法:先 delete 自己的 pb,若 rhs 就是 *this,则 pb 被毁
    delete pb;
    pb = new Bitmap(*rhs.pb); // 若 rhs == *this,rhs.pb 已悬空
    return *this;
  }
private:
  Bitmap* pb;
};

// ✅ 改法1:identity test(最直接)
Widget& operator=(const Widget& rhs) {
  if (this == &rhs) return *this; // 自赋值检查
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}
// 但这仍不是异常安全的(new 失败则 pb 已毁)。

// ✅ 改法2:先拷贝再释放(同时解决自赋值和异常安全)
Widget& operator=(const Widget& rhs) {
  Bitmap* pOrig = pb; // 记住原 pb
  pb = new Bitmap(*rhs.pb); // 先 new,若抛异常,pb 仍指向原对象
  delete pOrig; // 成功后才 delete 原对象
  return *this;
}

// ✅ 改法3:copy and swap 惯用法(最优雅)
Widget& operator=(Widget& rhs) { // 注意:按值传参,已自动拷贝
  swap(rhs); // 交换 this 和 rhs 的资源
  return *this; // rhs 析构时自动释放旧资源
}

核心 :好的 operator= 应同时满足三个目标------自我赋值安全、异常安全、效率合理。「先拷贝后释放」和「copy and swap」往往同时满足前两个。

条款 12:复制对象时勿忘其每一个成分

当你为类手写了拷贝构造和赋值,又添加了新成员时,两个函数都要更新------编译器不会提醒你。

cpp 复制代码
// ===== 目标:避免"复制遗漏" =====

class Customer {
public:
  Customer(const Customer& rhs) : name(rhs.name) {}
  Customer& operator=(const Customer& rhs) {
    name = rhs.name;
    return *this;
  }
private:
  std::string name;
};

// 后来给 Customer 加了基类和成员:
class PriorityCustomer : public Customer { // 有基类了!
public:
  PriorityCustomer(const PriorityCustomer& rhs)
  : Customer(rhs), // 必须显式调用基类拷贝构造,否则基类默认构造
    priority(rhs.priority) {}
  PriorityCustomer& operator=(const PriorityCustomer& rhs) {
    Customer::operator=(rhs); // 必须显式调用基类 operator=
    priority = rhs.priority;
    return *this;
  }
private:
  int priority;
};

两个易错点

  1. 派生类拷贝/赋值时,必须显式调用基类的对应函数,否则基类部分只被默认构造/未赋值。
  2. 拷贝构造和拷贝赋值是两个独立函数------若发现它们的逻辑重复,不要让它们互相调用 (语义不对),而应提取一个 privateinit() 函数共用。

第 3 章 资源管理(Resource Management)

「资源」= 用完必须归还的东西(内存、文件句柄、锁、网络连接...)。这一章的核心思想是 RAII(Resource Acquisition Is Initialization)

条款 13:以对象管理资源

把资源放进对象里,利用 C++ 析构函数「必然被调用」的特性,保证资源释放。

cpp 复制代码
// ===== 目标:用 RAII 对象管理资源,避免手工 delete =====

class Investment { /* ... */ };

// ❌ 危险:手工 delete,任何一个 return / 异常都会导致泄漏
Investment* createInvestment();
void f() {
  Investment* pInv = createInvestment();
  // ... 此处可能 return 或抛异常
  delete pInv; // 可能永远到不了
}

// ✅ 正确:用智能指针接管(原书是 auto_ptr,现代用 unique_ptr)
void f() {
  std::auto_ptr<Investment> pInv(createInvestment()); // [原书] auto_ptr
  // [C++11+] std::unique_ptr<Investment> pInv(createInvestment());
  //         unique_ptr 更优:不可拷贝,只能移动,避免 auto_ptr 的拷贝陷阱
}
// 离开作用域,析构自动 delete pInv,无论是否异常

// ⚠ auto_ptr 的坑:拷贝会转移所有权,源对象变 NULL
std::auto_ptr<Investment> p1(createInvestment());
std::auto_ptr<Investment> p2(p1); // p2 现在拥有对象,p1 变 NULL
p1->something(); // 解引用 NULL,崩溃!
// [C++11+] unique_ptr 直接禁止拷贝,编译期就防住这个坑

条款 14:在资源管理类中小心 copying 行为

并非所有 RAII 类都能简单「禁止拷贝」。当一个 RAII 对象被拷贝时,你要在四种语义中选一种:

cpp 复制代码
// ===== 目标:RAII 类被拷贝时,选择合适的语义 =====

// 场景:管理互斥锁
class Lock {
public:
  explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); }
  ~Lock() { unlock(mutexPtr); }
private:
  Mutex* mutexPtr;
};

// 问题:Lock m1(&m); Lock m2(m1); ------拷贝一个锁是什么意思?

// 选择1:禁止拷贝(条款6),适合大多数锁
class Lock1 : private Uncopyable { /* ... */ };

// 选择2:共享底层资源(引用计数),像 shared_ptr
class Lock2 {
public:
  explicit Lock2(Mutex* pm) : mutexPtr(pm, unlock) { // 指定删除器为 unlock
    lock(mutexPtr.get());
  }
private:
  std::shared_ptr<Mutex> mutexPtr; // 引用计数;最后一个引用消失时调用 unlock
};

// 选择3:深拷贝底层资源(如标准 string 拷贝 buffer)
// 选择4:转移所有权(像 unique_ptr,原对象失去资源)

核心:拷贝 RAII 对象时,关键是想清楚「底层资源应该怎样被复制」------禁止、共享、深拷贝、转移,四选一。

条款 15:在资源管理类中提供对原始资源的访问

RAII 类封装了资源,但很多 API 仍需要原始指针。RAII 类应提供「显式或隐式」的转换接口。

cpp 复制代码
// ===== 目标:让 RAII 类能与接收原始指针的 API 交互 =====

class Investment { /* isTaxFree() ... */ };
Investment* createInvestment();
int daysHeld(const Investment* pi); // C API,要原始指针

// RAII 类
std::shared_ptr<Investment> pInv(createInvestment());
// daysHeld(pInv); // ❌ 类型不匹配

// ✅ 显式转换(更安全,推荐)
int days = daysHeld(pInv.get()); // .get() 取出原始指针

// 隐式转换(方便但危险)
class FontHandle { /* ... */ };
class Font {
public:
  Font(FontHandle fh) : f(fh) {}
  operator FontHandle() const { return f; } // 隐式转换运算符
private:
  FontHandle f;
};
// 优点:Font 可直接传给 C API;缺点:可能在意外处隐式转换,误用。

原则显式访问(.get())通常比隐式转换安全。只有当「原始资源访问频繁、风险可控」时才考虑隐式转换。

条款 16:成对使用 newdelete 时要采取相同形式

cpp 复制代码
// ===== 目标:new [] 必须配 delete [],new 必须配 delete =====

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];

delete stringPtr1; // ✅ 单一对象
delete [] stringPtr2; // ✅ 数组------少了 [] 会导致只析构第一个,其余泄漏

// ⚠ 误区:typedef 别名隐藏了"数组"本质
typedef std::string AddressLines[4]; // 每个对象是 4 个 string 的数组
std::string* pal = new AddressLines; // 看起来像 new 单对象,实则是数组
delete pal; // ❌ 行为未定义!应该是 delete [] pal
delete [] pal; // ✅

// 建议:尽量用 vector<string> 代替数组 typedef,避免这种混淆。

条款 17:以独立语句将 newed 对象置入智能指针

cpp 复制代码
// ===== 目标:避免异常导致资源泄漏 =====

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

// ❌ 危险写法
processWidget(std::shared_ptr<Widget>(new Widget), priority());
// 编译器可能这样求值:(1) new Widget  (2) priority()  (3) 构造 shared_ptr
// 若第 2 步 priority() 抛异常,则第 1 步 new 的对象就泄漏了(还没进 shared_ptr)

// ✅ 安全写法:用独立语句把 new 包进智能指针
std::shared_ptr<Widget> pw(new Widget); // 先确保资源被接管
processWidget(pw, priority()); // 这时即使异常,pw 也会析构释放

条款 18:让接口容易被正确使用,不易被误用

进入第 4 章前的小结条款------好的接口设计能让错误用法在编译期就被挡住

cpp 复制代码
// ===== 目标:设计"防呆"的接口 =====

class Date {
public:
  // ❌ 差接口:用户可能传 (2, 30, 1995) 这种非法日期
  Date(int month, int day, int year);
};

// ✅ 好接口:用类型系统防止误用
struct Month { static const Month Jan() { return Month(1); } /* ... */ };
struct Day { explicit Day(int d) : val(d) {} int val; };
struct Year { explicit Year(int y) : val(y) {} int val; };
class Date {
public:
  Date(const Month& m, const Day& d, const Year& y); // 只能传正确类型
};
Date d(Month::Jan(), Day(30), Year(1995)); // ✅
// Date d(30, Month::Jan(), 1995); // ❌ 编译错误:类型不匹配,挡住错误

// 另一例:返回智能指针而非裸指针,强迫调用者用 RAII
std::shared_ptr<Investment> createInvestment(); // 调用者拿到的天然是 RAII

第 4 章 设计与声明(Designs and Declarations)

这一章关注接口设计------如何让你的函数签名、类设计在语义上正确、易用、高效。

条款 19:设计 class 犹如设计 type

定义一个新类,等于定义一个新类型。设计前自问:新对象如何创建/销毁?初始化与赋值的区别?传值语义?合法值范围?继承体系?类型转换?操作符?要不要写文档? 这些问题的答案,就是你的类的设计。

条款 20:宁以 pass-by-reference-to-const 替换 pass-by-value

cpp 复制代码
// ===== 目标:理解传值 vs 传 const 引用的代价 =====

class Person {
public:
  Person(); // 假设构造较"贵"
  virtual ~Person(); // 多态,虚析构
private:
  std::string name, address;
};
class Student : public Person {
private:
  std::string schoolName, schoolAddress;
};

// ❌ 传值:极其昂贵
// 调用一次 = 1 次 Student 拷贝构造 + 1 次 Person 拷贝构造
//          + 4 次 string 拷贝构造 + 5 次 析构(出作用域时)
bool validateStudent(Student s);

// ✅ 传 const 引用:无构造/析构,且 const 保证不被修改
bool validateStudent(const Student& s); // 只传一个引用,高效

但注意「切片问题(slicing)」------这是初学者极易忽略的陷阱:

cpp 复制代码
class Window {
public:
  virtual void onResize() { /* 基类逻辑 */ }
};
class SpecialWindow : public Window {
public:
  virtual void onResize() { /* 派生类扩展逻辑 */ Window::onResize(); }
};

// ❌ 传值会"切掉"派生类特性
void printWindow(Window w); // 即使传 SpecialWindow,w 的类型也是 Window
SpecialWindow sw;
printWindow(sw); // sw 被拷贝构造为 Window,派生部分被切掉
// ✅ 传引用则保留多态
void printWindow(const Window& w); // 虚函数正常派发

特例 :STL 迭代器、函数对象,因为它们「就是」一个指针或小对象,传值反而比传引用更高效------这是回到条款 1 的「四大次语言」规则。

条款 21:必须返回对象时,别妄想返回其 reference

cpp 复制代码
// ===== 目标:避免返回指向局部对象或堆对象的引用 =====

class Rational { /* ... */ };
const Rational operator*(const Rational& lhs, const Rational& rhs);

// ❌ 误区1:返回局部变量的引用------悬空引用
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
  Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
  return result; // result 出作用域被销毁,引用悬空!
}

// ❌ 误区2:返回堆对象引用------谁来 delete?
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
  Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
  return *result; // 连续调用 w = a*b*c 就泄漏中间对象
}

// ✅ 正确:虽然要付出一次构造的代价,但保证正确
const Rational operator*(const Rational& lhs, const Rational& rhs) {
  return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
// [C++11+] 现代编译器有 RVO(返回值优化),这个"代价"经常被消除

要点:绝不返回指向局部栈对象、堆对象(若无人 delete)、或已在析构对象内部的引用/指针。该传值就老老实实传值。

条款 22:将成员变量声明为 private

cpp 复制代码
// ===== 目标:成员变量为什么不该是 public =====

class AccessLevels {
public:
  int getReadOnly() const { return readOnly; } // 只读访问
  void setReadWrite(int v) { readWrite = v; } // 写访问,可加约束
  int getReadWrite() const { return readWrite; }
private:
  int readOnly;
  int readWrite;
  // public 成员无法提供"只读""只写""加约束""通知"等封装能力
};

// ⚠ protected 不比 public 更"安全":派生类仍可直接改,且一旦公开成员被改,
//   所有派生类都得跟着改------封装性同样脆弱。所以成员都设为 private。

封装的好处 :通过函数访问,你能控制只读/只写、加约束、做通知、改实现------而 public/protected 数据成员让这些都做不到。

条款 23:宁以 non-member、non-friend 替换 member 函数

cpp 复制代码
// ===== 目标:追求更大的封装性 =====

class WebBrowser {
public:
  void clearCache();
  void clearHistory();
  void removeCookies();
  // 方案A:把 clearEverything 设为 member
  void clearEverything() {
    clearCache();
    clearHistory();
    removeCookies();
  }
};

// 方案B:non-member non-friend 函数,更优
void clearBrowser(WebBrowser& wb) {
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}

为什么 B 更好? 越多代码能访问类的 private 数据,封装性越差。member 函数能访问所有 private 成员;而 clearBrowser 是 non-member non-friend,它不能访问任何 private 数据,封装性更强。所以一个「只是组合了几个 public 成员函数」的功能,应做成 non-member(通常放在与类同一个命名空间下)。

条款 24:若所有参数皆需类型转换,请为此采用 non-member 函数

cpp 复制代码
// ===== 目标:让 operator* 支持两侧都能隐式转换 =====

class Rational {
public:
  Rational(int numerator = 0, int denominator = 1); // 未 explicit,允许隐式转换
  int numerator() const { return n; }
  int denominator() const { return d; }
private:
  int n, d;
};

// 方案A:member 函数
class Rational {
public:
  const Rational operator*(const Rational& rhs) const; // this 在左侧
};
Rational oneHalf(1, 2);
Rational result = oneHalf * 2; // ✅ oneHalf.operator*(2):2 隐式转 Rational
result = 2 * oneHalf; // ❌ 2.operator*(oneHalf):2 不是 Rational,编译失败!

// 方案B:non-member 函数,两侧都能转换
const Rational operator*(const Rational& lhs, const Rational& rhs) {
  return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}
result = 2 * oneHalf; // ✅ 2 隐式转 Rational
result = oneHalf * 2; // ✅

条款 25:考虑写出一个不抛异常的 swap 函数

标准 std::swap 通过拷贝三个对象来交换------对「实现 pimpl 手法的类」很昂贵。你可以为你的类特化 swap

cpp 复制代码
// ===== 目标:为 pimpl 类高效且不抛异常的 swap =====

class WidgetImpl { // 实现细节:大数据
public:
  int a, b, c;
  std::vector<double> v;
};

class Widget { // pimpl:用指针指向实现
public:
  Widget(const Widget& rhs);
  Widget& operator=(const Widget& rhs) {
    *pImpl = *(rhs.pImpl); // 标准赋值,内部会用 swap
    return *this;
  }
private:
  WidgetImpl* pImpl;
};

// 高效 swap:只交换指针,绝不抛异常
namespace std {
  template <>
  void swap<Widget>(Widget& a, Widget& b) {
    swap(a.pImpl, b.pImpl); // 只换指针,廉价
  }
}

// 调用时:用 using 让编译器选最优的 swap
template <typename T>
void doSomething(T& obj1, T& obj2) {
  using std::swap; // 引入 std::swap
  swap(obj1, obj2); // 编译器选 T 自己的 swap 或 std::swap
}

关键 :(1) 类自己的 swap 应不抛异常;(2) 类内提供一个 public swap 成员,类外提供 non-member swap(或 std::swap 全特化);(3) 调用时写 using std::swap; 然后裸调 swap(a,b),让 ADL(实参依赖查找)选最佳版本。


第 5 章 实现(Implementations)

这一章关注代码实现层面的最佳实践------定义位置、编译期依赖、异常安全、内联策略。

条款 26:尽可能延后变量定义式的出现时间

cpp 复制代码
// ===== 目标:避免"定义了却没用上"的浪费 =====

// ❌ 过早定义:若 encryptPassword 抛异常,password 就白构造了
std::string encryptPassword(const std::string& password) {
  std::string encrypted; // 过早定义
  if (password.length() < 8)
    throw std::logic_error("Password too short");
  encrypted = password;
  encrypt(encrypted);
  return encrypted;
}

// ✅ 延后定义:用初值直接构造
std::string encryptPassword(const std::string& password) {
  if (password.length() < 8)
    throw std::logic_error("Password too short");
  std::string encrypted(password); // 直到能构造时才定义
  encrypt(encrypted);
  return encrypted; // 一次构造 + 一次拷贝,无浪费
}

循环内的变量定义:若每次循环都要赋值,建议「定义在循环外 + 赋值」;若直接构造更清晰,则「定义在循环内」。一般除非 (1) 赋值比构造+析构便宜,且 (2) 处理的是效率敏感的代码段------否则定义在循环内更清晰。

条款 27:尽量少做转型动作

cpp 复制代码
// ===== 目标:理解 C++ 转型的真面目 =====

class Base { public: virtual ~Base() {} };
class Derived : public Base { public: int special() { return 42; } };

Base* b = new Derived;

// C 风格转型(多种形式,C++ 中都不推荐)
(Derived*)b; // 旧式
Derived* d1 = static_cast<Derived*>(b); // 现代:编译期检查,相对安全

// ⚠ 误区:转型不只是"告诉编译器类型",它真的可能产生新代码!
class Window {
public:
  virtual void onResize() {}
};
class SpecialWindow : public Window {
public:
  virtual void onResize() {
    static_cast<Window>(*this).onResize(); // ❌ 错!转型生成 *this 的基类副本,
                                           //    onResize 作用在副本上,不影响 *this
    onResize(); // ✅ 正确:直接调
  }
};

// ⚠ dynamic_cast 代价高昂(深度继承 + 多继承下尤其),尽量用类型安全的容器替代
//   避免:for (auto& w : windows) if (dynamic_cast<SpecialWindow*>(w)) ...

// [C++11+] 现代建议:优先用虚函数设计;不要用旧式转型。

条款 28:避免返回 handles 指向对象内部成分

cpp 复制代码
// ===== 目标:避免"const 性被绕过"和"悬空句柄" =====

class Rectangle {
public:
  const Point& upperLeft() const { return pData->ulhc; } // 返回内部点引用
  // 即使是 const 成员函数,返回的引用仍让外部可修改(如果非 const)
private:
  RectData* pData;
};

// ⚠ 问题1:如果返回引用,const 性被穿透
const Rectangle r;
Point& p = r.upperLeft(); // 若 upperLeft 返回非 const 引用,r 的内部数据可被改!

// ⚠ 问题2:悬空句柄
class GUIObject { /* ... */ };
const Rectangle boundingBox(const GUIObject& obj); // 返回值(临时对象)
const Point* p = &(boundingBox(obj).upperLeft()); // ❌ boundingBox 临时对象析构后,p 悬空

要点 :成员函数若返回指向内部成员的引用/指针/迭代器,会降低封装性 (外部能改内部),并可能造成悬空句柄 。尽量避免,若必须返回,应返回 const 引用或值。

条款 29:为「异常安全」而努力是值得的

异常安全有三个层次的保证:

cpp 复制代码
// ===== 目标:实现"强异常安全"的菜单管理 =====

class PrettyMenu {
public:
  void changeBackground(std::istream& imgSrc);
private:
  Mutex mutex; // 互斥锁
  Image* bgImage; // 当前背景图
  int imageChanges; // 改变次数
};

// ❌ 不异常安全
void PrettyMenu::changeBackground(std::istream& imgSrc) {
  lock(&mutex);
  delete bgImage; // 1. 已 delete 旧图,若 new 抛异常,bgImage 悬空
  ++imageChanges; // 2. 已自增,若下面抛异常,计数器不一致
  bgImage = new Image(imgSrc);
  unlock(&mutex);
}

// ✅ 强异常安全(commit-or-rollback 语义)
void PrettyMenu::changeBackground(std::istream& imgSrc) {
  Lock m1(&mutex); // 用 RAII 管理锁(条款13)
  Image* pOld = bgImage;
  bgImage = new Image(imgSrc); // 先 new,若失败,bgImage 仍是旧值
  delete pOld; // 成功后才删旧的
  ++imageChanges;
}

// ✅ 更优:用智能指针 + pimpl,让"改变"自然满足强保证
class PrettyMenu2 {
  std::shared_ptr<Image> bgImage; // 智能指针
  void changeBackground(std::istream& imgSrc) {
    Lock m1(&mutex);
    bgImage.reset(new Image(imgSrc)); // 若 new 失败,bgImage 不变;若成功,旧图自动释放
    ++imageChanges;
  }
};

三保证

  1. 基本保证:异常发生时不泄漏、不损坏(对象仍可用)。
  2. 强烈保证 :异常发生时程序状态回滚到调用前(copy-and-swap 是经典手法)。
  3. 不抛保证(nothrow) :绝不抛异常(如 int 赋值、swap)。

「强烈保证」往往通过 copy-and-swap 实现:先在副本上做修改,最后一次性 swap。

条款 30:透彻了解 inlining 的里里外外

cpp 复制代码
// ===== 目标:明智地内联 =====

// 适合内联:小、频繁调用、逻辑简单的函数
class Person {
public:
  int age() const { return theAge; } // 类内定义,隐式 inline,小函数适合
private:
  int theAge;
};

// 不适合内联:大函数、构造析构(看似空,其实调用基类和成员的构造析构)
class Derived : public Base {
public:
  Derived() {} // 看起来空,但隐式调用 Base 构造 + 成员构造,并不"空"
};

// ⚠ inline 是对编译器的"建议",过度使用会导致代码膨胀(体积增大、指令缓存命中率下降)
// 不要 inline 构造析构、虚函数(虚函数要靠 vptr 派发,通常无法内联)
// 函数模板通常放在头文件,但"在头文件"不等于"必须 inline"

要点:将 inline 限定于「必将成为 inline」或「明确获益」的小函数。内联会增大目标码体积,影响指令缓存。

条款 31:将文件间的编译依存关系降至最低(Pimpl 惯用法)

cpp 复制代码
// ===== 目标:改动实现不引起大规模重编译 =====

// ❌ 问题:定义类的头文件包含所有实现的头文件
//   Person.h 中:
#include <string>
#include "Date.h"
#include "Address.h"
class Person {
public:
  Person(const std::string& name, const Date& birthday, const Address& addr);
  std::string name() const;
  std::string birthDate() const;
  std::string address() const;
private:
  std::string theName; // 实现:需要 string 的完整定义
  Date theBirthDate; // → Date.h 一变,所有 include Person.h 的都要重编译!
  Address theAddress;
};

// ✅ 解法1:Pimpl(pointer to implementation)手法
//   Person.h:
class PersonImpl; // 前向声明,不包含 Date.h、Address.h
class Person {
public:
  Person(const std::string& name, const Date& birthday, const std::Address& addr);
  std::string name() const;
  std::string birthDate() const;
  std::string address() const;
private:
  std::shared_ptr<PersonImpl> pImpl; // 指针:实现细节隐藏
};

//   Person.cpp:
#include "Person.h"
#include "PersonImpl.h" // 真正依赖 Date.h、Address.h 的地方
class PersonImpl {
  std::string theName;
  Date theBirthDate;
  Address theAddress;
};
// 现在改 Date 的实现,只需重编译 Person.cpp,包含 Person.h 的其他文件无感。

// 设计原则:
// (1) 让成员改为指针(pimpl),或用对象句柄
// (2) 尽量用 class 前向声明代替 #include
// (3) 为声明和实现分别提供头文件(如 Person.h 和 PersonImpl.h)

核心思想:依赖「声明」而非「定义」。让头文件只暴露接口,把实现细节放进指针指向的内部类------这样修改实现不影响外部编译。


第 6 章 继承与面向对象设计(Inheritance and Object-Oriented Design)

这是 OOP 思想的核心章节,涵盖何时用继承、用什么继承、虚函数如何设计、多继承如何用

条款 32:确定你的 public 继承塑模出 is-a 关系

cpp 复制代码
// ===== 目标:public 继承必须是"是一个"关系 =====

class Student : public Person { /* ... */ }; // ✅ 学生是人
class Car : public Vehicle { /* ... */ }; // ✅ 汽车是交通工具

// ⚠ 反例:看似 is-a,其实不是
class Bird { public: virtual void fly(); };
class Penguin : public Bird {}; // ❌ 企鹅是鸟,但不会飞!
// is-a 不是"日常语义",而是"适用于基类的一切都适用于派生类"
// 企鹅不能 fly,所以不能让 Bird 有无条件的 fly------要么把 fly 下移到 FlyingBird

核心D public 继承 B 意味着「每个 D 都是 B」,即「B 能做的,D 都能做」。如果某 D 不能做 B 能做的某事,就不是 is-a,不要用 public 继承。

条款 33:避免遮掩继承而来的名称

cpp 复制代码
// ===== 目标:理解名称遮掩,正确地 using 或转发 =====

class Base {
public:
  virtual void mf1() = 0;
  virtual void mf1(int); // 重载
  virtual void mf2();
  void mf3();
  void mf3(double);
};

class Derived : public Base {
public:
  virtual void mf1(); // 遮掩 Base::mf1 和 Base::mf1(int)!
  void mf3(); // 遮掩 Base::mf3 和 Base::mf3(double)!
  // ⚠ 即使参数不同,也会遮掩------C++ 在内层作用域找到名字后就不往外找了
};

Derived d;
int x;
d.mf1(); // 调 Derived::mf1
d.mf1(x); // ❌ 编译错误:Base::mf1(int) 被遮掩

// ✅ 解法1:using 声明,把基类的重载引入派生类作用域
class Derived : public Base {
public:
  using Base::mf1; // 让 Base 的所有 mf1 重载在 Derived 可见
  using Base::mf3;
  virtual void mf1(); // 加自己的版本
  void mf3();
};
d.mf1(x); // ✅ 现在能调 Base::mf1(int)

// ✅ 解法2:private 继承 + 转发函数(不想继承 Base 全部接口时)
class Derived : private Base {
public:
  virtual void mf1() { Base::mf1(); } // 显式转发
  virtual void mf1(int x) { Base::mf1(x); } // 只转想暴露的
};

条款 34:区分接口继承和实现继承

cpp 复制代码
// ===== 目标:纯虚函数 / 虚函数 / 非虚函数 三种声明,三种语义 =====

class Shape {
public:
  virtual void draw() const = 0; // 纯虚:只声明接口,派生类必须实现
  virtual void error(const std::string& msg); // 虚:接口 + 默认实现,派生类可改写
  int objectID() const { return id; } // 非虚:接口 + 强制实现,派生类不该改
};

class Circle : public Shape {
public:
  virtual void draw() const { /* 画圆 */ } // 必须实现纯虚
  // error 用基类默认实现,objectID 不变
};

// ⚠ 危险默认实参值的"陷阱"
//   纯虚函数其实也可以有"定义",但只能通过类名限定调用:Shape::draw() = 默认
class Airport { /* ... */ };
class Airplane {
public:
  virtual void fly(const Airport& dest) = 0; // 纯虚,提供默认实现
};
void Airplane::fly(const Airport& dest) { /* 默认飞行算法 */ } // 纯虚的定义

class ModelA : public Airplane {
public:
  virtual void fly(const Airport& dest) { Airplane::fly(dest); } // 复用默认
};
// 这样默认实现不会被"无意继承"------派生类必须明确表态

三种成员函数语义

  • 纯虚函数:只继承接口。派生类必须实现。
  • (非纯)虚函数:继承接口 + 默认实现。派生类可改写,也可不改用默认。
  • 非虚函数:继承接口 + 强制实现。派生类不该改(违反 is-a)。

条款 35:考虑虚函数以外的其他选择

虚函数并非唯一的多态手段。Meyers 提出 NVI(Non-Virtual Interface)手法Strategy 模式

cpp 复制代码
// ===== 目标:用更灵活的多态替代直接 public 虚函数 =====

class GameCharacter {
public:
  // ✅ NVI:public 非虚函数 + private 虚函数
  //   好处:public 函数保证"事前事后"操作(如日志、锁、检查)
  int healthValue() const {
    // ... 事前工作(如锁、日志)
    int retVal = doHealthValue(); // 调真正的虚函数
    // ... 事后工作
    return retVal;
  }
private:
  virtual int doHealthValue() const { // 派生类可改写
    // 默认实现
  }
};

// ✅ Strategy:用函数指针/对象,运行时灵活替换"策略"
class GameCharacter2;
int defaultHealthCalc(const GameCharacter2& gc);
class GameCharacter2 {
public:
  typedef int (*HealthCalcFunc)(const GameCharacter2&); // 函数指针类型
  explicit GameCharacter2(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
  int healthValue() const { return healthFunc(*this); }
private:
  HealthCalcFunc healthFunc; // 不同角色可注入不同算法
};
// [C++11+] 现代写法:用 std::function<int(const GameCharacter2&)>,能存 lambda、bind 等

NVI 的本质:把「公开接口」(非虚)和「可定制实现」(私有虚)分开,让基类在调用前后能插入统一逻辑(模板方法模式)。

条款 36:绝不重新定义继承而来的 non-virtual 函数

cpp 复制代码
class Base {
public:
  void mf() { /* 基类版本 */ } // 非虚
};
class Derived : public Base {
public:
  void mf() { /* 派生类版本 */ } // ❌ 重新定义非虚函数
};

Derived d;
Base* pb = &d;
Derived* pd = &d;
pb->mf(); // 调 Base::mf(静态绑定看静态类型 Base)
pd->mf(); // 调 Derived::mf(静态绑定看静态类型 Derived)
// 同一个对象,调同样的函数,结果不同------设计上就是错的

核心:非虚函数是静态绑定的,重新定义它没有多态效果,只会让同一对象在不同指针类型下行为不一致。如果派生类要改行为,那它就该是虚函数。

条款 37:绝不重新定义继承而来的缺省参数值

cpp 复制代码
// ===== 目标:理解默认参数是静态绑定的,虚函数是动态绑定的 =====

class Shape {
public:
  virtual void draw(int color = 1) const { // 默认色 = 1
    std::cout << color; // 基类画
  }
};
class Rectangle : public Shape {
public:
  virtual void draw(int color = 2) const { // ⚠ 重新定义默认参数!
    std::cout << color;
  }
};

Shape* ps = new Rectangle;
ps->draw(); // 调 Rectangle::draw(虚函数动态绑定),但默认参数用的是 Shape::draw 的 color=1
// 原因:默认参数是静态绑定(按 ps 的静态类型 Shape),而虚函数是动态绑定
// 结果:调用了 Rectangle 的实现,却用了 Shape 的默认值------诡异!

// ✅ 解法:NVI + 在基类设一次默认参数
class Shape {
public:
  void draw(int color = 1) const { // 默认参数只在这里设
    doDraw(color); // 调虚函数
  }
private:
  virtual void doDraw(int color) const = 0;
};
class Rectangle : public Shape {
private:
  virtual void doDraw(int color) const { // 派生类只改实现,不碰默认参数
    // 用 color 画矩形
  }
};

C++ 这么设计是为了运行时效率:动态绑定默认参数需要运行期查表。代价是「虚函数动态绑定、默认参数静态绑定」的诡异行为。NVI 是优雅解法。

条款 38:通过复合塑模 has-a 或「根据某物实现出」

cpp 复制代码
// ===== 目标:区分 has-a(应用域)和 is-implemented-in-terms-of(实现域)=====

// 情况1:has-a------对象含有其他对象(应用领域)
class Address { /* ... */ };
class PhoneNumber { /* ... */ };
class Person {
private:
  Address address; // Person 有一个地址
  PhoneNumber phone; // Person 有一个电话
};

// 情况2:is-implemented-in-terms-of------用某个对象实现另一个(实现领域)
class Set {
private:
  std::list<int> rep; // 用 list 实现 Set
public:
  void insert(int x) { if (!contains(x)) rep.push_back(x); }
  // 注意:Set 不允许重复,但 list 允许------所以 Set 不是 list
  //       Set 不该用 public 继承 list,而是用 list 实现 Set
};
// ⚠ 误区:不能写 class Set : public std::list ------因为 Set 行为 ≠ list 行为(无重复)

要点 :「复合」既可表达 has-a (应用域:人「有」地址),也可表达 is-implemented-in-terms-of(实现域:用 list「实现」Set)。区分标准:组合的两者在「概念域」上是平等关系(has-a),还是纯粹实现工具(实现域)。

条款 39:明智而审慎地使用 private 继承

cpp 复制代码
// ===== 目标:private 继承 = "根据某物实现出"(仅实现,不接口)=====

class Timer {
public:
  void onTick() const; // 定时器,每 tick 一次
};
class Widget : private Timer { // private 继承
private:
  virtual void onTick() const; // 重写以接收 timer 的 tick
  // private 继承意味着:Widget 不是 Timer(不是 is-a),
  //                     只是 Widget 借用 Timer 的实现。外部无法把 Widget* 当 Timer*
};

// 通常"复合"比 private 继承更好(条款38),但 private 继承适用于:
// (1) 需要重写基类的虚函数时(复合做不到)
// (2) 涉及 EBO(空基类优化)时
class Empty {}; // 空类,sizeof 应为 1
class HoldsAnInt {
private:
  int x;
  Empty e; // 复合:sizeof(HoldsAnInt) = 8(含 padding)
};
class HoldsAnInt2 : private Empty { // private 继承空类
  int x; // EBO:sizeof(HoldsAnInt2) = 4,省了空间
};

核心 :private 继承在语义上等同于「is-implemented-in-terms-of」,但通常不如复合(条款 38)。只有当需要重写基类的虚函数,或追求 EBO 时,才考虑 private 继承。

条款 40:明智而审慎地使用多重继承

cpp 复制代码
// ===== 目标:处理多重继承的"菱形继承"问题 =====

class File { /* ... */ };
class InputFile : public File { /* ... */ }; // 两个都继承 File
class OutputFile : public File { /* ... */ };
class IOFile : public InputFile, public OutputFile { /* ... */ };
// ⚠ 问题:IOFile 里有两份 File 的数据!sizeof 偏大,File 成员访问二义

// ✅ 解法:虚继承,让 File 只存一份
class File { /* ... */ };
class InputFile : virtual public File { /* ... */ };
class OutputFile : virtual public File { /* ... */ };
class IOFile : public InputFile, public OutputFile { /* ... */ };
// 现在 IOFile 只有一份 File------但虚继承有运行期成本(更慢、对象更大)

多重继承的取舍

  • 单一继承更简洁,能用就用单一继承。
  • 多重继承确实需要时(如组合多个接口),用之,但要警惕:
    • 名称冲突 :两个基类有同名成员,派生类要 using 或转发消除二义性。
    • 菱形继承:用虚继承解决,但代价是体积和速度。
  • 推荐策略:用多重继承把「接口(纯虚)」和「实现」分开------继承多个接口类 + 至多一个实现类,这是 Java/C# 接口思想的 C++ 等价物。

第 7 章 模板与泛型编程(Templates and Generic Programming)

这一章对初学者最难,涉及模板的特殊语法、类型推导、代码膨胀、traits 技巧。我会重点用代码讲清楚。

条款 41:了解隐式接口和编译期多态

cpp 复制代码
// ===== 目标:类(OOP)vs 模板(GP)的接口与多态差异 =====

class Widget {
public:
  Widget();
  virtual ~Widget();
  virtual std::size_t size() const;
  virtual void normalize();
  void swap(Widget& other);
};
// OOP:Widget 的"显式接口"------签名写死的成员函数
//      多态发生在"运行期",通过虚函数 + 动态绑定

// 模板:T 的"隐式接口"------只要 T 支持所需表达式即可,没有签名约束
template <typename T>
void doProcessing(T& w) {
  if (w.size() > 10 && w != someNastyWidget) { // T 必须支持 size()、operator>
    T temp(w);
    temp.normalize();
    temp.swap(w);
  }
}
// 模板的多态发生在"编译期"------T 不同,编译出不同版本的 doProcessing

核心 :OOP 的接口是显式 的(签名),多态在运行期 (虚函数);模板的接口是隐式 的(基于有效表达式),多态在编译期(实例化)。

条款 42:了解 typename 的双重含义

cpp 复制代码
// ===== 目标:理解 typename 区分"嵌套类型"和"成员变量" =====

template <typename T> // typename 等价于 class,仅声明模板参数
void f(T t) {}

// ⚠ 难点:嵌套从属类型名必须加 typename
template <typename C>
void print2nd(const C& container) {
  // C::const_iterator 是"嵌套从属类型名"------C 是模板参数,const_iterator 依赖 C
  // C++ 默认认为 C::const_iterator 是个"成员变量",加 typename 告诉它是类型
  typename C::const_iterator it(container.begin()); // typename 必须加!
  // 不加 typename 编译器会以为 C::const_iterator * it(...) 是个乘法表达式
}

// ⚠ 例外:基类列表和成员初始化列表中的嵌套类型,不加 typename
template <typename T>
class Derived : public Base<T>::Nested { // 基类列表:不加 typename
public:
  Derived(int x) : Base<T>::Nested(x) { // 初始化列表:不加 typename
    typename Base<T>::Nested temp; // 函数体内:要加 typename
  }
};

规则当模板参数依赖的嵌套名表示类型时,必须加 typename(基类列表和成员初始化列表中除外)。这是初学者最常被编译器报错的地方。

条款 43:学习处理模板化基类内的名称

cpp 复制代码
// ===== 目标:模板派生类如何调用模板基类的成员 =====

class CompanyA { public: void sendCleartext(const std::string&); };
class CompanyB { public: void sendCleartext(const std::string&); };

template <typename Company>
class MsgSender { // 模板基类
public:
  void sendClear(const std::string& info) {
    Company c;
    c.sendCleartext(info);
  }
};

template <typename Company>
class LoggingMsgSender : public MsgSender<Company> { // 模板派生类
public:
  void sendClearMsg(const std::string& info) {
    // ❌ 编译失败:编译器看不到 MsgSender<Company>::sendClear
    // 原因:在模板实例化前,编译器不知道 MsgSender<Company> 是什么样------
    //       因为可能存在 MsgSender 的特化版,连 sendClear 都没有!
    sendClear(info); // ❌ 找不到
  }
};

// ✅ 解法1:用 this-> 前缀
this->sendClear(info);
// ✅ 解法2:用 using 声明
using MsgSender<Company>::sendClear;
sendClear(info);
// ✅ 解法3:基类限定(但失去虚函数动态绑定)
MsgSender<Company>::sendClear(info);

为什么? 模板基类的「实化版本」可能对原模板做了特化,导致某些成员根本不存在。C++ 编译器保守起见,默认不查找模板基类的成员------你必须显式告诉它「我确定有这个成员」。

条款 44:将与参数无关的代码抽离 templates

cpp 复制代码
// ===== 目标:避免"模板代码膨胀" =====

// ❌ 危险:模板为每个类型生成一份代码,即使是"逻辑相同"的部分
template <typename T, std::size_t n>
class SquareMatrix {
public:
  void invert(); // 求 n*n 矩阵的逆
private:
  T data[n * n];
};
SquareMatrix<double, 5> sm5; // 生成一份 invert() 代码
SquareMatrix<double, 10> sm10; // 又生成一份几乎相同的 invert()------代码膨胀!

// ✅ 解法:把"与参数无关"的部分提到基类
template <typename T>
class SquareMatrixBase { // 不含 n
protected:
  void invert(std::size_t matrixSize); // 接受 size 参数,逻辑通用
};
template <typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
public:
  void invert() { this->invert(n); } // 委托给基类,复用代码
};
// 现在 SquareMatrix<double,5> 和 <double,10> 共用 SquareMatrixBase<double>::invert

核心 :模板会为每个不同的模板参数生成代码,重复的代码会膨胀可执行文件(影响缓存、增大体积)。原则:把「不依赖模板参数」的代码抽离到非模板基类,只让「确实依赖参数」的部分留在模板里。

条款 45:运用成员函数模板接受所有兼容类型

cpp 复制代码
// ===== 目标:让智能指针支持"派生类指针 → 基类指针"转换 =====

class Top { /* ... */ };
class Middle : public Top { /* ... */ };
class Bottom : public Middle { /* ... */ };

// 普通指针能自然转换:Bottom* → Middle* → Top*
// 但智能指针是类,需要"泛化拷贝构造"才能支持

template <typename T>
class SmartPtr {
public:
  // 成员函数模板:泛化的拷贝构造
  template <typename U>
  SmartPtr(const SmartPtr<U>& other) // 接受任意兼容类型 U 的 SmartPtr
  : heldPtr(other.get()) {} // 用原始指针初始化,编译器会检查 T* 和 U* 是否兼容
  T* get() const { return heldPtr; }
private:
  T* heldPtr;
};

SmartPtr<Bottom> pb;
SmartPtr<Top> pt = pb; // ✅ 调用泛化构造,等同于 Top* = Bottom*
// SmartPtr<Top> pt2 = SmartPtr<int>(...); // ❌ int* 不能转 Top*,编译错误------类型安全

核心 :要让模板类型(如智能指针)支持「派生→基类」的转换,必须用成员函数模板(泛化拷贝/赋值构造) 。但同时要保留类型安全------编译器会在初始化时检查底层指针是否可转换。

条款 46:需要类型转换时请为模板定义非成员函数

cpp 复制代码
// ===== 目标:模板版的 Rational + operator*(条款24 的泛化版)=====

template <typename T>
class Rational {
public:
  Rational(const T& numerator = 0, const T& denominator = 1);
  const T numerator() const;
  const T denominator() const;
};

// ❌ 直接定义模板 operator*:无法推导出"2 * oneHalf"的左侧 2
template <typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { /* ... */ }
Rational<int> oneHalf(1, 2);
oneHalf * 2; // ✅ 推导出 T=int
2 * oneHalf; // ❌ 推导失败:从 int 推 T 不行(模板推导不做隐式转换)

// ✅ 解法:在类内声明友元(friend)函数,借助类实化时已知 T
template <typename T>
class Rational {
public:
  friend const Rational operator*(const Rational& lhs, const Rational& rhs); // 友元声明
  // 类被实例化时 T 已知,友元函数体可直接用 Rational<T>
};
// 函数体可在类外定义(但要小心,定义为模板会再次引入推导问题)

这一条极其反直觉:模板的 operator* 必须通过「在类内声明 friend」让 T 在类实例化时被「注入」函数签名,才能支持两侧类型转换。

条款 47:请使用 traits class 表现类型信息

cpp 复制代码
// ===== 目标:用 traits 在编译期查询"迭代器种类" =====

// STL 迭代器分 5 类:input、output、forward、bidirectional、random_access
// 不同种类支持不同操作。traits 让我们在编译期知道种类。

template <typename IterT>
struct iterator_traits { // 标准 traits 模板
  typedef typename IterT::iterator_category iterator_category;
  // 对原生指针有偏特化版:typedef random_access_iterator_tag iterator_category
};

// 用 traits + 函数重载,针对不同迭代器选最优实现
template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
  doAdvance(iter, d, // 委派
            typename std::iterator_traits<IterT>::iterator_category());
}

// 重载1:random access 迭代器(最强大)
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
  iter += d; // 直接 +=,只有 random access 支持
}

// 重载2:bidirectional 迭代器
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
  if (d >= 0) while (d--) ++iter;
  else while (d++) --iter; // 双向 ++ / --
}

// 重载3:input 迭代器(只能单向前进)
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
  if (d < 0) throw std::out_of_range("Negative distance");
  while (d--) ++iter;
}

traits 用法的三步

  1. 定义一组重载函数(或模板),针对不同类型信息分别实现。
  2. 建立控制流程,用 traits 类取得类型信息并传给重载函数。
  3. traits 本身是模板,对要支持的类型(含原生指针)做特化。

条款 48:认识 template 元编程

cpp 复制代码
// ===== 目标:TMP 在编译期完成计算 =====

// 经典例子:编译期计算阶乘
template <unsigned n>
struct Factorial {
  enum { value = n * Factorial<n - 1>::value }; // 递归,n! = n * (n-1)!
};
template <>
struct Factorial<0> { // 特化:0! = 1(递归终止)
  enum { value = 1 };
};
// 用法:Factorial<5>::value 在编译期就等于 120

// [C++11+] 现代写法用 constexpr
constexpr unsigned factorial(unsigned n) {
  return n == 0 ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120, ""); // 编译期断言

TMP(Template Metaprogramming) 是「编写在编译期执行的 C++ 程序」。它的循环靠模板递归 ,分支靠模板特化 ,变量靠枚举值 (C++11 后用 constexpr)。优点是把计算从运行期挪到编译期,可生成更高效的代码;缺点是语法晦涩、编译慢、错误信息难懂。


第 8 章 定制 new 和 delete(Customizing new and delete)

这一章讨论全局或类内 operator new/operator delete 的重写,适合对性能和内存布局有极致要求的场景。

条款 49:了解 new-handler 的行为

cpp 复制代码
// ===== 目标:内存不足时的处理策略 =====

// new-handler:当 operator new 无法分配内存时调用的函数
void outOfMem() {
  std::cerr << "Unable to satisfy request for memory\n";
  std::abort();
}

int main() {
  std::set_new_handler(outOfMem); // 注册 new-handler
  int* pBigData = new int[10000000000L]; // 分配失败时调 outOfMem
  // ...
}

// 为特定类定制 new-handler:用 RAII 切换
class Widget {
public:
  static void* operator new(std::size_t size) throw(std::bad_alloc);
};
class NewHandlerHolder { // RAII 保存/恢复 new-handler
public:
  explicit NewHandlerHolder(std::new_handler h) : handler(h) {}
  ~NewHandlerHolder() { std::set_new_handler(handler); }
private:
  std::new_handler handler;
};
// Widget::operator new 内:临时设自己的 handler,出作用域恢复原 handler

// ⚠ new-handler 能做的四件事:
// (1) 让更多内存可用(释放预留块)
// (2) 安装另一个 new-handler(换策略)
// (3) 卸载 new-handler(设 nullptr,让 next new 抛 bad_alloc)
// (4) 抛 bad_alloc / 不返回(abort)

核心 :当 operator new 失败,它会反复调用当前 new-handler 直到能分配为止。new-handler 负责释放内存或调整策略。类可以有自己的 new-handler(通过「mixin」风格基类实现)。

条款 50:了解 new 和 delete 的合理替换时机

替换 operator new/delete 的理由包括:(1) 检测内存使用错误(越界、未释放);(2) 强化性能(自定义分配器,如固定大小对象池);(3) 收集使用统计。

cpp 复制代码
// ===== 目标:替换全局 operator new 做内存统计/检测 =====

static const int signature = 0xDEADBEEF; // 前后签名,检测越界
typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc) {
  using namespace std;
  size_t realSize = size + 2 * sizeof(int); // 前后各放一个签名

  void* pMem = malloc(realSize);
  if (!pMem) throw bad_alloc();

  *(static_cast<int*>(pMem)) = signature; // 前签名
  void* pUser = static_cast<Byte*>(pMem) + sizeof(int); // 返回给用户的指针
  *(reinterpret_cast<int*>(static_cast<Byte*>(pUser) + size)) = signature; // 后签名
  return pUser;
}
// 这种自定义分配器可在 free 时检查签名是否被破坏,发现内存越界写入

注意 :替换时要满足 operator new/delete 的约定(正确处理 size == 0 的情况、nothrow 版本、内存不足时调用 new-handler 等)。

条款 51:编写 new 和 delete 时需固守常规

cpp 复制代码
// ===== 目标:正确实现 operator new 的约定 =====

void* operator new(std::size_t size) throw(std::bad_alloc) {
  using namespace std;
  if (size == 0) size = 1; // C++ 保证 new 0 字节也返回合法指针

  while (true) {
    void* p = malloc(size);
    if (p) return p; // 成功

    new_handler globalHandler = set_new_handler(0); // 取出当前 handler
    set_new_handler(globalHandler);
    if (globalHandler) (*globalHandler)(); // 调它,期望它释放内存
    else throw bad_alloc(); // 没 handler 就抛异常
  }
}

// ⚠ 类内 operator new 会被继承的派生类调用!派生类可能更大
class Base {
public:
  static void* operator new(std::size_t size) throw(std::bad_alloc) {
    if (size != sizeof(Base)) // 大小不对(派生类调来的)
      return ::operator new(size); // 委托给全局 new
    return customAlloc(sizeof(Base));
  }
};

约定要点 :(1) 处理 size == 0 的请求;(2) 内存不足时调用 new-handler 并重试;(3) 类内 operator new 要处理「被派生类调用」(size 不等于本类)的情况。

条款 52:写了 placement new 也要写 placement delete

cpp 复制代码
// ===== 目标:placement new 和 placement delete 必须成对 =====

// 普通 operator new
void* operator new(std::size_t size) throw(std::bad_alloc);
// placement new:带额外参数
void* operator new(std::size_t size, void* memory) throw();

class Widget {
public:
  // 普通 new
  static void* operator new(std::size_t size) throw(std::bad_alloc);
  static void operator delete(void* p) throw();
  // placement new/delete 必须成对
  static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc);
  static void operator delete(void* p, std::ostream& log) throw(); // 配对的 placement delete
};

// ⚠ 为什么需要配对的 placement delete?
//   Widget* w = new (std::cerr) Widget; // 先 placement new,再调构造
//   若构造抛异常,编译器要调 placement delete------若没提供,内存泄漏

核心operator new 的「额外参数」版本叫 placement new(最常见的是定位 new,传一个 void*)。写了带额外参数的 placement new,就必须写签名匹配的 placement delete------否则构造抛异常时无法正确释放。


第 9 章 杂项讨论(Miscellany)

条款 53:不要轻忽编译器的警告

不同的编译器对代码的「含义」可能有不同理解(尤其涉及模板、转型、STL)。一条经验法则:严肃对待编译器的警告,至少要理解每条警告的来源,确定它不是真实问题后再忽略。

不要假设「在 A 编译器无警告,在 B 也安全」------本书举了一个继承相关代码在不同编译器下表现完全不同的例子。靠警告维护代码的健壮性,但不要依赖特定编译器的特定警告策略

条款 54:让自己熟悉包括 TR1 在内的标准程序库

TR1(Technical Report 1) 是 C++ 标准委员会在 C++0x(即后来的 C++11)发布前的一组扩展提案。Meyers 在第三版重点介绍了 TR1 的 14 个组件。本书原书推荐通过 Boost 等实现获取这些组件。

对现代读者(2026 年)的更新 :TR1 的几乎所有内容都已正式进入 C++11/14/17 标准库。原书提到的核心组件,今天直接用标准库即可:

TR1 组件 现代标准(C++11+)替代
tr1::shared_ptrtr1::weak_ptr std::shared_ptrstd::weak_ptr
tr1::function std::function
tr1::bind std::bind(或用 lambda)
tr1::hash std::hash
tr1::tuple std::tuple
tr1::array std::array
tr1::regex std::regex
tr1::type_traits std::is_samestd::is_class 等(<type_traits>
tr1::result_of std::result_of(C++17 后 std::invoke_result
tr1::reference_wrapper std::reference_wrapperstd::ref/std::cref
tr1::random <random>
tr1::unordered_map/set std::unordered_map/unordered_set

学习建议:今天读这一条时,跳过 TR1 历史细节,直接对照上表学习现代标准库的对应组件。Boost 仍是 C++ 新组件的试验场,值得关注。

条款 55:让自己熟悉 Boost

Boostboost.org)是 C++ 标准库的「准标准」与孵化器------许多 C++11/14/17 的功能(智能指针、bindfunctiontupleregextype_traitsunordered 等)都来自 Boost。即使到了现代 C++,Boost 仍有大量尚未进入标准的优秀库(如 asio 网络库、filesystem(已入标准)、program_optionsmpl/fusion 元编程等)。

给现代读者的建议

  • 把 Boost 当作「未来标准库的预览」------很多 Boost 库后来都被标准化了。
  • 学习 Boost 也能让你更熟悉本书(条款 54 的 TR1 内容几乎都源自 Boost)。
  • 但也要警惕:Boost 有些库过重、编译慢,在生产中要按需选取。

结语:55 条条款的内在脉络

读完全书,你会发现这 55 条并非零散的「技巧集」,而是有一条清晰的主线:

主线一:用对象管理资源(RAII)

贯穿第 2-3 章。从「虚析构(条款7)」到「对象管理资源(条款13)」到「智能指针」------核心是让 C++ 的析构语义替你管理资源生命周期 ,避免手工 delete/free

主线二:让编译器替你挡错

  • const(条款3)、explicit(条款18)、类型系统(条款18)、=delete(条款6)让错误用法在编译期被拒绝。
  • 用虚函数表(条款7、32、34)、初始化列表(条款4)、NVI(条款35)让接口行为符合预期。

主线三:理解 C++ 的「多重身份」

回到条款 1------C++ 是 C、OO、Template、STL 的联邦。本书每一条都隐含「判断你在哪个次语言」。比如传值(条款20)在 C/STL 域高效,在 OO 域却导致切片。

主线四:编译期 vs 运行期

模板章(第7章)展现 C++ 最强大的能力------把计算和决策推到编译期(TMP、traits),换取零开销运行期抽象。

给不同读者的阅读建议

你的情况 建议路径
C++ 新手(刚学完语法) 重点读第 1-3 章,把 RAII 和 const 用熟,先写出「不泄漏、不崩溃」的代码
有 OOP 经验 精读第 2、4、6 章,理解虚析构、接口设计、is-a/has-a 区分
写库/模板代码 死磕第 7 章,配《C++ Templates》和《Effective Modern C++》
追求极致性能 关注第 5 章(Pimpl、inline、异常安全)+ 第 8 章(自定义 new)

与现代 C++ 的衔接

本书基于 C++98,但 Meyers 的思想------RAII、异常安全、值语义、编译期多态------是 C++ 永恒的内核。读完本书后,强烈推荐衔接阅读:

  1. 《Effective Modern C++》(Meyers 著) ------本书的精神续作,覆盖 C++11/14 的 42 条新准则(auto、右值引用/移动语义、智能指针、constexpr 等)。
  2. 《C++ Primer》(5th)------查漏补缺现代语法。
  3. 《C++ Concurrency in Action》(Williams 著)------并发编程,本书完全未涉及,但现代 C++ 离不开它。

一句话总结本书的精神:C++ 给你「零开销抽象」的承诺,但前提是你按它的规则用------本书就是那份规则的清单。


本文按《Effective C++》第三版(Scott Meyers, 2005)章节顺序整理,对易混淆处配代码示例并辅以现代 C++(C++11/14/17)的对照说明。建议边读边对照原书,本文是导读而非替代。

相关推荐
有点。1 小时前
C++深度优先搜索(三)
开发语言·c++·深度优先
程序喵大人2 小时前
【C++进阶】STL算法与函数对象 -【C++进阶】STL算法与函数对象
开发语言·c++·算法
我不会插花弄玉2 小时前
10.list【由浅入深-C++】
数据结构·c++·list
旖旎夜光3 小时前
C++(类与对象)(下)
开发语言·c++·学习
江屿风3 小时前
【科普】【差集&交集概念落地云相册】流食般投喂
开发语言·c++·笔记·算法·云相册
2401_8685347811 小时前
RTOS之RT-Thread & Linux:线程/进程管理与调度核心差异分析
c++·python
郝学胜-神的一滴12 小时前
Qt 高级编程 040:按钮悬浮弹出滑块弹窗的完整攻略
开发语言·c++·qt·软件工程·用户界面
程序喵大人17 小时前
【C++进阶】STL算法与函数对象 - 02 sort为什么需要随机访问迭代器
开发语言·c++·算法
ZhangShao060717 小时前
题解:CF2249B
c++·算法