欢迎光临
我们一直在努力

【《Effective C++》精读】构造/析构/赋值运算(下)

精读《Effective C++》|第二章 构造/析构/赋值运算(条款09-12)

  • 🎯 前言
  • 一、条款09:绝不在构造和析构过程中调用 virtual 函数
    • 1. 一个看起来合理但有 bug 的例子
    • 2. 为什么 C++ 要这样设计
    • 3. 藏在 init() 里的隐患
    • 4. 正确解法:把必要信息从 derived 向上传
    • 深度思考:为什么 Java/C# 可以,C++ 不可以?
  • 二、条款10:令 operator= 返回一个 reference to *this
    • 深度思考:返回值 vs 返回引用——为什么这里能返回引用?
  • 三、条款11:在 operator= 中处理"自我赋值"
    • 1. 朴素实现的两个问题
    • 2. 方案 A:证同测试(identity test)
    • 3. 方案 B:精心安排语句顺序
    • 4. 方案 C:copy and swap
    • 深度思考:异常安全的三个等级与自赋值的耦合
  • 四、条款12:复制对象时勿忘其每一个成分
    • 1. 新增成员变量后忘记改 copying 函数
    • 2. 继承带来的"局部拷贝"
    • 3. copy 构造和 copy assignment 不要互相调用
    • 深度思考:为什么编译器不替我们检查"遗漏的成分"?
  • 五、本章知识结构
  • 📝 总结
  • 参考资料
  • 标签

🎯 前言

第二章前四条(条款05-08)讲完编译器默默写的函数、拒绝拷贝、virtual 析构和析构不抛异常。这一篇读后四条,主题更"工程":

  • 条款09:构造/析构时 virtual 函数为什么不下降;
  • 条款10:所有 operator= 都该返回 *this;
  • 条款11:怎么写一个自我赋值安全的 operator=;
  • 条款12:写 copy 函数时别漏掉任何一个成分。

这四条合在一起,几乎就是 C++ 类设计里"值语义"的完整说明书。


一、条款09:绝不在构造和析构过程中调用 virtual 函数

在 base class 构造期间,virtual 函数不是 virtual 函数。

我以前在 Java/C# 里习惯"构造函数里调用一个 virtual 初始化钩子",derived class 重写后就能实现"构造时多态"。在 C++ 里这么写会得到一个让你摸不着头脑的结果——被调用的永远是当前正在构造的那一层的版本。

1. 一个看起来合理但有 bug 的例子

class Transaction {
public:
Transaction() { logTransaction(); } // 最后一步做审计日志
virtual void logTransaction() const = 0;
};

class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const;
};

BuyTransaction b;

构造 b 时,Transaction 构造函数先于 BuyTransaction 构造函数执行。这时候 Transaction::Transaction() 里调用 logTransaction(),被调用的是 Transaction 版本,不是 BuyTransaction 版本——而且因为它是 pure virtual,连接器直接报错。

2. 为什么 C++ 要这样设计

有两层原因。

更直接的原因:derived 成员还没初始化。 base class 构造函数执行时,derived class 的成员变量尚未初始化。如果这时候 virtual 调用下降到 derived 版本,而 derived 版本几乎必然要访问自己的成员变量,就会用到未初始化数据——未定义行为 + 彻夜调试大会串。

更根本的原因:对象的类型在构造期间就是"当前层"。 在 Transaction 构造函数执行期间,对象的类型就是 Transaction,不是 BuyTransaction。不止 virtual 函数会被解析到 base class,dynamic_cast 和 typeid 也把它当 base class 看待。C++ 这样做是安全的——derived 专属部分还不存在,"当它不存在"最稳妥。

析构函数同理。 derived 析构函数一旦开始执行,derived 成员变量就呈现为未定义值;进入 base class 析构函数后,对象就是一个 base class 对象。

3. 藏在 init() 里的隐患

把共同逻辑放进一个 init() 私有函数看起来很 DRY,但同样的坑会藏得更深:

class Transaction {
public:
Transaction() { init(); }
private:
void init() { logTransaction(); } // 这里也是 virtual 调用!
virtual void logTransaction() const = 0;
};

这种写法编译器和连接器通常都不会报错(如果 logTransaction 不是 pure virtual 而是有实现的 impure virtual),程序"兴高采烈地继续向前跑",但调用的是错的版本,调试起来非常痛苦。

4. 正确解法:把必要信息从 derived 向上传

既然 virtual 下降走不通,就让 derived class 在构造时主动把信息向上传给 base class:

class Transaction {
public:
explicit Transaction(const std::string& logInfo) {
logTransaction(logInfo); // non-virtual 调用,安全
}
void logTransaction(const std::string& logInfo) const;
};

class BuyTransaction : public Transaction {
public:
BuyTransaction(/* params */)
: Transaction(createLogString(/* params */)) {}
private:
static std::string createLogString(/* params */);
};

注意 createLogString 被声明为 static——这很关键,它避免了在"对象尚未成熟"时意外访问 derived 成员变量。

深度思考:为什么 Java/C# 可以,C++ 不可以?

这涉及三种语言在对象构造期的类型模型差异。

Java 和 C# 在对象分配时就把"对象头"(包含 vtable 指针 / 类型元数据指针)初始化为最派生类(most derived class)的类型。base class 构造函数执行期间,对象的动态类型已经是 derived 类型,所以 virtual 调用会下降到 derived 版本。但这带来另一个风险:derived 的字段还没初始化(在 Java 里是默认零值,在 C# 里也是默认值),被调用的 override 方法可能看到"半初始化"的字段。

C++ 选择了"构造期间类型逐层退化"的模型:base 构造时对象是 base 类型,derived 成员"逻辑上不存在"。这种模型牺牲了"构造期多态",但换来了更强的不变式:在任何一个构造函数体内,类自身声明的所有成员都已经初始化完毕。换句话说,在 C++ 里,构造函数中能访问到的成员一定是有效的;在 Java/C# 里,构造函数里调用的 virtual override 可能访问到派生类字段的零值。

两种设计各有取舍。C++ 的选择与它"不留下未初始化状态"的零开销哲学是一致的。

📖 C++11 §12.7/4:构造或析构函数直接或间接调用 virtual 函数时,调用的是"当前正在构造或析构的类"的版本,不会下降到 derived class。


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

int x, y, z;
x = y = z = 15; // 连锁赋值

赋值是右结合的,所以上面等价于:

x = (y = (z = 15));

为了让这个协议在自定义类型上成立,所有赋值运算符都要返回左侧对象的引用:

class Widget {
public:
Widget& operator=(const Widget& rhs) {
...
return *this;
}
};

这个协议同样适用于所有赋值相关运算符:

Widget& operator+=(const Widget& rhs) { ...; return *this; }
Widget& operator-=(const Widget& rhs) { ...; return *this; }
Widget& operator=(int rhs) { ...; return *this; }

这只是个协议,不遵守也能编译通过。但所有内置类型和标准库类型(std::string、std::vector、std::complex、std::tr1::shared_ptr 等)都遵守它。没有非标新立异不可的理由,就随众吧。

深度思考:返回值 vs 返回引用——为什么这里能返回引用?

operator= 的语义是"把右侧内容赋给一个已经存在的左侧对象"。左侧对象在调用前就构造好了,函数结束后它仍然存在,所以返回它的引用是安全的——没有悬空引用的问题。

这和 operator+ 必须返回值是本质区别:a + b 的结果是一个新对象,必须以值返回(通常用 NRVO 优化掉拷贝)。

至于为什么不是返回值——按值返回会触发一次额外的拷贝构造,在连锁赋值 a = b = c 中会放大多次。返回引用是零成本的。

唯一需要小心的是"返回 *this"的实现不要写错成 return this;(那是指针)或在函数里把 *this move 掉。

📖 C++ 标准库的 Assignable 概念要求 t = u 返回类型能转换为 T&,这正是该协议的形式化。


三、条款11:在 operator= 中处理"自我赋值"

自我赋值听起来蠢,但完全合法:

Widget w;
w = w; // 显式
a[i] = a[j]; // i == j 时
*px = *py; // px 和 py 指向同一对象时

基类引用也可以指向派生类对象,所以即使静态类型不同,也可能在运行时"别名"到同一个对象。

1. 朴素实现的两个问题

Widget& Widget::operator=(const Widget& rhs) {
delete pb; // 释放当前 bitmap
pb = new Bitmap(*rhs.pb); // 拷贝 rhs 的 bitmap
return *this;
}

如果 this == &rhs,delete pb 同时删掉了 rhs 的 bitmap,末尾 new Bitmap(*rhs.pb) 就是访问已删除对象——未定义行为。

而且它还不具备异常安全性:如果 new Bitmap 抛异常,pb 就指向一块已删除内存,既不能安全读也不能安全写。

2. 方案 A:证同测试(identity test)

Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this; // 自我赋值,直接返回
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}

简单直接,但异常安全问题没解决——new Bitmap 抛异常时 pb 仍然悬空。

3. 方案 B:精心安排语句顺序

Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb; // 先记住原指针
pb = new Bitmap(*rhs.pb); // new 成功后再替换
delete pOrig; // 最后删除旧资源
return *this;
}

这个版本同时具备自我赋值安全性和异常安全性:如果 new 抛异常,pb 仍然指向原对象;如果是自我赋值,pb 和 pOrig 指向同一个 bitmap,但因为先 new 了一份再 delete 旧的,也能正确处理(只是效率略低)。

4. 方案 C:copy and swap

void Widget::swap(Widget& rhs) {
using std::swap;
swap(pb, rhs.pb);
}

Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); // 拷贝构造一份副本
swap(temp); // *this 和副本交换
return *this;
}

更巧妙的变体是 pass by value:

Widget& Widget::operator=(Widget rhs) { // rhs 就是被传对象的副本
swap(rhs);
return *this;
}

  • 自我赋值时,副本和原对象内容一致,swap 后再销毁——正确;
  • new 失败发生在副本构造阶段,*this untouched——异常安全;
  • 代码简洁,但把拷贝动作从函数体移到了"参数构造阶段",对可读性有一点牺牲。

Meyers 的观点:让 operator= 具备异常安全性,往往自动获得自我赋值安全性。所以现代实践里,很多人不再写证同测试,而是把精力放在"语句顺序"或 copy-and-swap 上。

深度思考:异常安全的三个等级与自赋值的耦合

条款29会详细展开异常安全保证,这里先建立基本框架:

  • 基本保证(basic guarantee):异常抛出后,程序处于合法状态,没有资源泄漏,但对象的具体内容不确定;
  • 强烈保证(strong guarantee):异常抛出后,程序状态回滚到调用前,即 commit-or-rollback 语义;
  • 不抛保证(nothrow guarantee):承诺不抛异常。

copy-and-swap 几乎总是提供 strong guarantee:它先在临时对象上完成所有可能失败的工作(拷贝构造),成功后用 noexcept 的 swap 提交结果。这也是为什么它被推崇——"异常安全 + 自赋值安全 + 简洁"三合一。

但它不是免费午餐:

  • 总是会拷贝一份资源,即使原本可以 in-place 修改,带来额外的堆分配和内存占用;
  • 对含多个共享状态(如锁、文件句柄)的对象,swap 的实现可能没那么平凡;
  • pass-by-value 写法对不熟悉该惯用法的维护者不够直观。
  • 实际工程里,性能敏感的路径常用"语句顺序"方案,其余场景倾向 copy-and-swap。关键不是用哪一种,而是清楚自己写的 operator= 提供哪一级保证。

    📖 copy-and-swap 惯用法由 Herb Sutter 在 Exceptional C++ 系列中系统化;C++11 之后 move assignment 的出现让决策更复杂——但条款11的核心原则不变。


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

    编译器生成的 copy 函数会逐成员拷贝;一旦你决定自己写,编译器就"如释重负"地撒手不管——你漏了什么,它也不提醒你。

    1. 新增成员变量后忘记改 copying 函数

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

    Customer::Customer(const Customer& rhs) : name(rhs.name) {
    logCall("Customer copy constructor");
    }
    Customer& Customer::operator=(const Customer& rhs) {
    logCall("Customer copy assignment operator");
    name = rhs.name;
    return *this;
    }

    后来加了一个成员:

    class Date { ... };

    class Customer {
    ...
    private:
    std::string name;
    Date lastTransaction; // 新增
    };

    既有的 copy 构造和 copy assignment 只复制了 name,lastTransaction 被默默漏掉。多数编译器即使在最高警告级别也不吭声——这是编译器对"你拒绝我生成版本"的复仇。

    2. 继承带来的"局部拷贝"

    class PriorityCustomer : public Customer {
    public:
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
    private:
    int priority;
    };

    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : priority(rhs.priority) { // ❌ 漏了 Customer(rhs)
    logCall("PriorityCustomer copy constructor");
    }

    PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
    logCall("PriorityCustomer copy assignment operator");
    priority = rhs.priority; // ❌ 漏了 Customer::operator=
    return *this;
    }

    • copy 构造没有在成员初值列里调用 base class copy 构造,于是 Customer 部分会被 default 构造(name 和 lastTransaction 都是默认值),而不是从 rhs 复制;
    • copy assignment 完全没碰 base class 成分,base 成员保持原值不变。

    正确写法:

    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : Customer(rhs), // 调用 base class copy 构造
    priority(rhs.priority) {
    logCall("PriorityCustomer copy constructor");
    }

    PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
    logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs); // 对 base class 成分赋值
    priority = rhs.priority;
    return *this;
    }

    3. copy 构造和 copy assignment 不要互相调用

    两个函数逻辑相近时,会 tempted to 让一个调用另一个来消除重复。但这在语义上是错的:

    • assignment 调 copy 构造:等于"试图构造一个已经存在的对象",没有合法语法;
    • copy 构造调 assignment:等于"对还没初始化的对象做赋值",assignment 作用于已初始化对象。

    正确做法是把共同机能放进第三个 private 函数,通常命名为 init:

    class PriorityCustomer {
    private:
    void init(const PriorityCustomer& rhs) {
    // 共同逻辑
    }
    public:
    PriorityCustomer(const PriorityCustomer& rhs) {
    init(rhs);
    logCall("copy ctor");
    }
    PriorityCustomer& operator=(const PriorityCustomer& rhs) {
    logCall("copy assign");
    init(rhs);
    return *this;
    }
    };

    深度思考:为什么编译器不替我们检查"遗漏的成分"?

    这其实是一个语言设计权衡。如果编译器在用户自定义的 copy 函数里强制检查"每个成员都必须被复制",会拒绝掉大量合法且有意图的代码——有些类的 copy 本来就需要"部分复制 + 部分重建"(例如持有缓存、统计计数器、日志状态的类)。

    语言没法区分"开发者故意跳过某个成员"和"开发者忘记了某个成员",所以它选择不警告。

    缓解这个问题的工程实践有几个层次:

  • 能用 = default 就用。C++11 起可以显式 = default,让编译器做逐成员拷贝,又能在声明位置表达"这是我特意选择的"。
  • 尽量用 RAII 成员(智能指针、容器、string),让默认拷贝就做对的事,减少需要手写 copy 函数的场景。
  • 在 code review 中把"新增成员是否更新 copy 函数/swap/序列化"当作 checklist。Google 的 style guide 甚至要求相关函数紧邻声明。
  • 借助静态分析工具:Clang-Tidy 的 bugprone-copy-constructor-init、cppcoreguidelines-special-member-functions 等检查能捕获大部分遗漏。
  • 这条规则在 C++ Core Guidelines 里对应 C.21/C.22:“若定义了任何 copy/move/destructor,应当考虑全部五个特殊成员函数是否都需要”。

    📖 C++ Core Guidelines C.21: If you define or =delete any default operation, define or =delete all of them;C.52: Make sure to copy every member.


    五、本章知识结构

    #mermaid-svg-iVUQSBr0Xzd2qp53{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iVUQSBr0Xzd2qp53 .error-icon{fill:#552222;}#mermaid-svg-iVUQSBr0Xzd2qp53 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iVUQSBr0Xzd2qp53 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .marker.cross{stroke:#333333;}#mermaid-svg-iVUQSBr0Xzd2qp53 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iVUQSBr0Xzd2qp53 p{margin:0;}#mermaid-svg-iVUQSBr0Xzd2qp53 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .cluster-label text{fill:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .cluster-label span{color:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .cluster-label span p{background-color:transparent;}#mermaid-svg-iVUQSBr0Xzd2qp53 .label text,#mermaid-svg-iVUQSBr0Xzd2qp53 span{fill:#333;color:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .node rect,#mermaid-svg-iVUQSBr0Xzd2qp53 .node circle,#mermaid-svg-iVUQSBr0Xzd2qp53 .node ellipse,#mermaid-svg-iVUQSBr0Xzd2qp53 .node polygon,#mermaid-svg-iVUQSBr0Xzd2qp53 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .rough-node .label text,#mermaid-svg-iVUQSBr0Xzd2qp53 .node .label text,#mermaid-svg-iVUQSBr0Xzd2qp53 .image-shape .label,#mermaid-svg-iVUQSBr0Xzd2qp53 .icon-shape .label{text-anchor:middle;}#mermaid-svg-iVUQSBr0Xzd2qp53 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .rough-node .label,#mermaid-svg-iVUQSBr0Xzd2qp53 .node .label,#mermaid-svg-iVUQSBr0Xzd2qp53 .image-shape .label,#mermaid-svg-iVUQSBr0Xzd2qp53 .icon-shape .label{text-align:center;}#mermaid-svg-iVUQSBr0Xzd2qp53 .node.clickable{cursor:pointer;}#mermaid-svg-iVUQSBr0Xzd2qp53 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .arrowheadPath{fill:#333333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iVUQSBr0Xzd2qp53 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iVUQSBr0Xzd2qp53 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iVUQSBr0Xzd2qp53 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iVUQSBr0Xzd2qp53 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .cluster text{fill:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 .cluster span{color:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iVUQSBr0Xzd2qp53 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iVUQSBr0Xzd2qp53 rect.text{fill:none;stroke-width:0;}#mermaid-svg-iVUQSBr0Xzd2qp53 .icon-shape,#mermaid-svg-iVUQSBr0Xzd2qp53 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iVUQSBr0Xzd2qp53 .icon-shape p,#mermaid-svg-iVUQSBr0Xzd2qp53 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iVUQSBr0Xzd2qp53 .icon-shape .label rect,#mermaid-svg-iVUQSBr0Xzd2qp53 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iVUQSBr0Xzd2qp53 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iVUQSBr0Xzd2qp53 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iVUQSBr0Xzd2qp53 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    构造/析构/赋值(续)

    条款09构造期virtual

    条款10返回*this

    条款11自赋值安全

    条款12拷贝所有成分

    类型逐层退化

    derived成员未初始化

    解法:信息向上传

    连锁赋值协议

    适用于所有=类运算符

    证同测试

    语句顺序

    copy-and-swap

    异常安全自动达成

    local成员变量

    base class成分

    抽取共同init函数


    📝 总结

    这一章后四条把"对象的复制与赋值"彻底拆开讲透:

    • 条款09让我重新理解了"构造期间的对象类型"——它不是固定的最派生类,而是随构造进度逐层变化;
    • 条款10看似简单,但把 *this 作为 reference 返回是 C++ 值语义能形成连锁表达式的底层约定;
    • 条款11把"自赋值安全"和"异常安全"绑在一起讲,让我意识到很多 bug 不是两个独立问题,而是一个问题的两面;
    • 条款12的"partial copy"是最隐蔽的 bug——编译器不警告、单元测试容易漏掉,只能靠纪律和工具守住。

    第二章整章读完,对"编译器在背后做了什么、自己接管时该做什么"有了完整认识。下一章进入「资源管理」(条款13-17),讲 RAII、智能指针、new/delete 配对,那是 C++ 最能体现"用类型系统守住资源"思想的一章。

    点个赞收藏一下呗~有理解不对的地方欢迎评论区指正。

    参考资料

    • Scott Meyers. Effective C++ (3rd Edition). 条款 09-12.
    • ISO/IEC 14882:2011, §12.7 (Construction and destruction), §12.8 (Copying class objects).
    • Herb Sutter. Exceptional C++. 关于 copy-and-swap 与异常安全的系列条款。
    • Bjarne Stroustrup. C++ Core Guidelines. C.21, C.22, C.52.

    标签

    C++ EffectiveC++ 构造函数 析构函数 赋值运算符 虚函数 自赋值 异常安全 学习笔记 深度思考

    赞(0)
    未经允许不得转载:171主机测评 » 【《Effective C++》精读】构造/析构/赋值运算(下)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址