more effective c++笔记

51 阅读24分钟

悦读上品,得乎益友

1.基础议题

条款1: 仔细区分pointers 和 references

  • 引用不能为空,指针可以为空
  • 引用必须初始化,指针可以不初始化
  • 引用不能改变指向,指针可以重新赋值
  • 特定操作符需返回引用:当重载操作符(如 operator[])时,为了语法上的直观(如 v[i] = 10),必须返回引用而非指针。
  • 函数参数传递指针和传递引用:- 传引用在底层就是用指针实现的,都会多一次指针大小的拷贝。

条款2:最好使用C++转型操作符

  • 尽量使用C++新式转型,避免使用旧式转型:C的旧式转型几乎允许你将任何类型转换为任何其他类型,风险极高。新式转型(static_castconst_castdynamic_castreinterpret_cast)通过名称清晰表达了转型的意图,编译器也更易检查。
  • static_cast用于基本类型转换和下行转换(无安全检查) :它执行隐式转换能进行的任何转换,以及反向的转换(如父类转子类),编译时类型检查(只要相关类型就可转),但不会进行运行时类型检查。
  • const_cast唯一用于去除常量性
  • dynamic_cast用于继承体系中的安全向下转型:它执行运行时类型检查,确保转换的安全性。但成本较高,应谨慎使用,尤其是在性能关键代码中。
  • reinterpret_cast用于底层的强制位模式解释:它依赖于编译器实现,不可移植。通常用于将指针转换为整型或其他不相关类型,应尽量避免在应用层使用。功能上类似C风格转换。

条款3:绝对不要以多态(polymorphically)方式处理数组

Base[10],该数组中存了它了derived类,错误,数据截断等一系列错误。

条款4:非必要不提供default constructor

1.提供缺省构造函数的代价

  • 效率损失:成员函数需测试字段是否初始化,付出时间和空间代价
  • 意义缺失:无意义的初始化导致对象状态不确定

2.不提供缺省构造函数的限制

  1. 数组定义困难:无法直接定义T obj[N]new T[N]
  2. 模板兼容问题:某些模板容器要求类型有缺省构造函数
  3. 虚基类麻烦:派生类必须理解虚基类构造参数

3.最终判断

宁愿带来使用限制,也要保证对象的完整初始化——这才是真正的效率和安全

2.操作符

条款5: 对定制的“类型转换函数”保持警觉

1.单自变量constructors

2.隐式类型转换操作符

class Rational{
    operator double() const; //将Rational 转换为 double

3.解决方案

  1. 使用explicit关键字:这是最直接的方法。将单自变量构造函数声明为explicit,可以禁止编译器用它进行隐式类型转换,但允许显式转换(如static_cast
  2. 提供显式函数替代:用功能对等的普通成员函数(如 toDouble()asDouble() 或标准库中的 c_str())取代隐式类型转换操作符。虽然调用时需显式写出函数名,但换来了代码的清晰和安全
  3. 使用Proxy类(代理类) :这是一种更精巧的技术。通过引入一个新类(如ArraySize)作为构造函数的参数,利用“禁止连续两次用户定制转换”的规则,既能实现用整数构造对象的需求,又能阻止整数被隐式转换为临时对象
  4. 除非你真的、真的很需要,否则不要提供隐式类型转换函数。

条款6:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式

🔧 语法上的“小技巧”:用int参数区分

C++允许重载++--操作符,但前置和后置形式都没有参数,无法直接通过参数类型区分。为了解决这个语言漏洞,标准规定:后置形式接受一个int参数(通常不命名,也不使用),编译器在调用时会自动传一个0值作为占位

  • 前置声明UPInt& operator++(); //返回引用
  • 后置声明const UPInt operator++(int); //返回const对象,为什么必须返回const对象,阻止C++++这样的形式编译通过,阻止对C++返回的临时对象进行++,违背直觉
  1. 前置形式 (++i)累加然后取出。直接修改自身,然后返回修改后的自身的引用。这种方式效率高,没有临时对象产生。

    UPInt& UPInt::operator++() {
        *this += 1;   // 累加
        return *this; // 返回自身引用
    }
    
  2. 后置形式 (i++)取出然后累加。为了返回旧值,必须创建一个临时对象保存当前状态,然后对自身进行累加,最后返回那个临时对象。

    const UPInt UPInt::operator++(int) {
        UPInt oldValue = *this; // 取出(创建临时对象)
        ++(*this);              // 累加(调用前置++,实现代码复用)
        return oldValue;        // 返回旧值
    }
    

以前置形式实现后置,前置形式效率更高,没有临时对象的创建

条款7: 千万不要重载&&,||和,操作符

重载这些操作符会剥夺其固有的短路求值特性,并改变其求值顺序的确定性。

🚨 重载 && 和 ||:短路求值消失

逻辑与(&&)和逻辑或(||)最核心的特性是短路求值。这意味着一旦表达式的值可以由左侧操作数确定,右侧操作数将不会被计算。

  • 内置行为示例

    if (p && p->size() > 10) // 如果p是null,则不会尝试访问p->size()
    

    如果 p 为空指针,左侧为 false,整个表达式确定为 false,右侧 p->size() 根本不会执行,这保证了程序的安全。

  • 重载后的行为
    当重载 && 或 || 时,它们变成了函数调用

    if (expression1 && expression2)  // 内置版本:短路求值
    if (expression1 || expression2)  // 内置版本:短路求值
    
    if (a && b)                      // 如果ab是重载了&&的类型
    // 编译器看到的是:a.operator&&(b) 或 operator&&(a, b)
    

    作为函数参数,两个表达式在进入函数体之前都会被计算。  这意味着短路求值特性完全丢失,右侧表达式(可能包含副作用或重要操作)总是会被执行,即便左侧的结果已经可以决定整个表达式的值。

🚨 重载 ,(逗号)操作符:求值顺序不确定

逗号操作符用于构成逗号表达式,其内置行为是:从左到右依次计算各个表达式,整个逗号表达式的结果是最后一个表达式的结果。

  • 内置行为示例

    int i = 0, j = 1;
    int k = (i++, j++); // i先自增变为1,然后j自增变为2,最后k被赋值为j++的结果(即1)
    

    这里 i++ 保证在 j++ 之前执行。

  • 重载后的行为
    重载 , 操作符同样使其变成了函数调用(如 a, b 变成 a.operator,(b) 或 operator,(a, b))。
    函数参数的求值顺序是未指定的。  这意味着你无法保证 a 一定在 b 之前被计算。当逗号表达式的求值顺序对结果有影响时(如上述例子中的副作用),重载它将带来灾难。

条款8:了解不同意义的new和delete

🧩 三种不同的 new 场景

1. new 操作符 (new operator)

这是你最常用的 new,它完成两件事:

  • 分配内存:自动调用 operator new 分配足够的内存。
  • 构造对象:在分配的内存上调用对象的构造函数。

示例

string* ps = new string("test");

这段代码不能改变其行为(不能手动分配内存然后调用构造函数替代它)。

2. operator new 函数

这是 new 操作符用来分配内存的底层函数。它只负责分配原始内存(类似 malloc),不调用构造函数

  • 你可以重载 operator new 来定制内存分配策略。
  • 其原型通常为:void* operator new(size_t size);

示例

void* rawMemory = operator new(sizeof(string)); // 只分配内存
// 此时 rawMemory 中的对象还未构造,还不能当 string 用
3. Placement new

这是一种特殊的 operator new,它在一个已经分配好的指针上构造对象(调用构造函数)。主要用在内存池、自定义容器等场景。

  • 最常用的形式是:operator new(size_t, void* location),它在指针 location 指向的内存上构造对象。
  • 不能直接使用对应的 delete 来释放 placement new 构造的对象(因为内存不是它分配的)。

示例

void* buffer = malloc(sizeof(string));      // 分配原始内存
string* ps = new (buffer) string("test");   // placement new:在buffer上构造对象
// ...
ps->~string();                               // 必须手动调用析构函数
free(buffer);                                // 然后释放原始内存

⚙️ 对应的 delete 逻辑

1. delete 操作符 (delete operator)

与 new 操作符对应,它完成两件事:

  • 析构对象:调用对象的析构函数。
  • 释放内存:自动调用 operator delete 释放内存。
2. operator delete 函数

与 operator new 对应,它只负责释放原始内存(类似 free)。

  • 你重载 operator delete 通常是为了配合重载的 operator new
3. Placement delete

如果 placement new 在构造对象时抛出异常,C++ 运行时系统会查找对应参数列表的 operator delete(placement delete)来清理内存,防止泄漏。但在正常情况下,你不应该直接调用 placement delete,而应该手动调用析构函数。

🔄 数组形式的 new[] 和 delete[]

条款也强调了 new[] 和 delete[] 的特殊性:

  • new[] :先调用 operator new[] 分配足够容纳多个对象的内存,然后对每个元素调用构造函数
  • delete[] :先对每个元素调用析构函数,然后调用 operator delete[] 释放整体内存。
  • 致命错误:如果对 new[] 分配的内存使用 delete(而非 delete[]),程序行为是未定义的(通常会导致部分对象未析构或内存释放错误)。

💡 核心总结

  1. new 操作符 是语言特性,完成内存分配 + 对象构造。
  2. operator new 是函数,只负责内存分配(可重载)。
  3. Placement new 是在已有内存上构造对象,需要手动管理析构。
  4. new[] 必须配 delete[] ,否则会导致资源泄漏和未定义行为。

3. 异常

条款9: 利用destructors避免资源泄露

利用析构函数避免资源泄露的本质,就是将资源的生命周期与对象的生命周期绑定。这是 C++ 中极其重要的 RAII(资源获取即初始化)  思想的直接体现。它是编写异常安全代码和防止资源泄露的最基本、最强大的武器。

条款10: 在constructros内组织资源泄露(resource leak)

在构造函数内组织资源泄露的本质是:由于对象构造失败时析构函数不会运行,你必须依靠 “已构造成功的成员对象”的析构函数来清理资源。因此,最好的策略就是将所有资源句柄(尤其是裸指针)替换为具有析构函数的 RAII 对象(如智能指针)

条款11: 禁止exceptions流出destructors之外

  1. 基本原则

    • 绝对不能让异常从析构函数中传播出去
    • 如果析构函数可能抛出异常,必须在内部捕获并处理
  2. 主要原因

    • 当容器或数组析构时,会依次调用多个对象的析构函数
    • 如果同时出现多个异常,C++无法处理这种情况,会导致程序终止
    • 异常传播出去会使程序行为不可预测
  3. 实现方法

    • 在析构函数内使用try-catch块捕获所有异常

    • 可以选择:

      • 记录日志后终止程序
      • 吞掉异常(通常不推荐)
      • 提供类方法让用户有机会处理可能出错的资源释放

4.栈展开机制(stack_unwinding)

当异常抛出时,C++会进行栈展开:

  • 从异常抛出点开始,逐层向上寻找匹配的catch语句
  • 在这个过程中,会自动销毁沿途的所有局部对象
  • 这些局部对象的析构函数会被调用

5.双重异常的危险性

如果在栈展开期间:

  • 某个局部对象的析构函数又抛出了新的异常
  • 此时C++运行时系统已经处于处理第一个异常的过程中
  • 同时存在两个活跃的异常,C++无法决定应该处理哪一个
  • 最终结果:程序立即调用terminate()函数,进程被强制结束

条款12: 了解”抛出一个exception“与”传递一个参数“或”调用一个虚函数“之间的差异

1. 异常对象的拷贝机制

抛出异常时,异常对象总是被拷贝(by value),而参数传递可以有多种方式:

// 异常抛出 - 总是拷贝
Widget w;
throw w;  // 抛出的是w的拷贝,不是w本身

// 函数参数 - 可以传引用
void func(Widget& w) { }  // 直接操作原对象

关键区别:

  • 抛出异常时,编译器会创建一个异常的静态拷贝
  • 即使原对象在栈展开过程中被销毁,异常对象依然存在
  • 异常对象存储在特殊的内存位置(不是栈也不是堆)

2. 类型转换的限制

异常匹配的类型转换比函数参数严格得多:

// 函数参数允许的隐式转换
void func(double d) { }
func(1);  // int→double 隐式转换 OK

// 异常匹配几乎不允许隐式转换
try {
    throw 1;  // 抛出int
}
catch(double d) { }  // 不会捕获!int不能转成double
catch(const std::exception& e) { }  // 也不行,没有继承关系

// 仅允许的两种转换:
// 1. 继承关系:catch (base) 可以捕获 derived
// 2. 类型化指针→void*:catch (void*) 可以捕获任何指针

3. 匹配顺序的差异

  • 虚函数:根据对象的动态类型选择最匹配的函数(运行时多态)
  • 异常处理:按照catch子句的书写顺序匹配,选择第一个匹配的,不是最佳匹配

cpp

// 虚函数 - 最佳匹配
class Base { virtual void f(); };
class Derived : public Base { void f(); };

Base* pb = new Derived;
pb->f();  // 调用Derived::f() - 最佳匹配

// 异常 - 顺序匹配
try {
    throw Derived();
}
catch(Base b) { }  // 第一个匹配,被捕获!
catch(Derived d) { }  // 永远不会执行(unreachable)

4. 异常catch的两种写法

cpp

// 方式1:传值(会再次拷贝)
catch(Widget w) { }  // 拷贝异常对象到w

// 方式2:传引用(推荐)
catch(Widget& w) { }  // 直接引用异常对象
catch(const Widget& w) { }  // const引用

建议: 总是使用引用捕获,避免不必要的拷贝,也能保留多态性。

总结表

特性抛出异常传递参数调用虚函数
对象传递总是拷贝可选传值/传引用取决于参数类型
类型转换极少(继承和void*)丰富(隐式转换)支持向上转换
匹配规则顺序匹配重载决议动态类型匹配
性能较慢(拷贝+栈展开)快速快速(虚表)

条款13:以by reference方式捕捉exceptions

1. 为什么不能用值捕获(catch by value)

class Base {
public:
    virtual const char* what() const { return "Base"; }
};

class Derived : public Base {
public:
    virtual const char* what() const { return "Derived"; }
};

try {
    throw Derived();  // 抛出Derived对象
}
catch (Base b) {  // 值捕获!发生对象切片
    std::cout << b.what();  // 输出"Base",丢失多态信息
}

值捕获的两大问题:

  • 对象切片:派生类对象被切割成基类部分,虚函数调用失效
  • 二次拷贝:抛出时一次拷贝,catch时又一次拷贝,性能损失

2. 为什么重新抛出要用 throw 而不是 throw w

// 错误做法:throw w
catch (const Base& w) {
    // 想要重新抛出当前捕获的异常
    throw w;  // 问题:抛出的是w的拷贝,不是原异常对象
    // 如果w是基类引用,这里会发生对象切片!
}

// 正确做法:throw;
catch (const Base& w) {
    // 记录日志或其他操作
    throw;  // 重新抛出原来的异常对象,保留完整类型
}

// 完整示例
try {
    throw Derived();  // 抛出Derived对象
}
catch (const Base& w) {
    // throw w;  // 如果错误地写成这样,会抛出Base的拷贝,丢失Derived部分
    throw;  // 正确:重新抛出原来的Derived对象
}

throw w 的两大问题:

  • 对象切片throw w 会创建一个新异常对象,如果w是基类引用,新对象会被切片
  • 丢失类型信息:重新抛出的对象类型是w的静态类型,而不是原异常的实际类型

throw; 的两大优势:

  • 保留原异常:直接重新抛出当前捕获的异常对象
  • 保持多态性:异常对象的动态类型保持不变

3.核心结论

用by reference方式捕获异常是C++异常处理的黄金法则,它同时解决了对象切片、类型安全和性能损失三大问题。记住:捕获用 const&,重新抛出用 throw;。

条款14:明智运用exception specifications

此章节在现在C++中已经不做讨论,略。

条款15:了解异常处理(exception handling)的成本

成本一:编译时成本(代码体积)

即使程序从未使用try/catch/throw,只要启用了异常支持,编译器就会为每个函数生成异常表,记录栈展开时需要调用的析构函数信息。这导致可执行文件体积增加5-15%

成本二:进入try块的成本

// 即使不抛出异常,进入try块也有开销
try {  // 这里:需要"激活"异常处理机制,约几条指令
    func();
}
catch(...) {
}
  • 进入try块会激活异常处理机制
  • 成本大约相当于几条指令
  • try块嵌套越深,成本越高

成本三:抛出异常的成本(异常路径)

void func() {
    std::string s = "hello";
    std::vector<int> v(1000);
    
    throw std::runtime_error("error");  // 抛出点
    // 栈展开:自动销毁s和v
}

try {
    func();  // 正常路径:接近普通函数调用
}
catch(...) {
    // 异常路径:成本是正常路径的千倍
}

抛出异常的三个阶段:

  1. 抛出异常:创建异常对象、查找catch、约10000-100000条指令
  2. 栈展开:销毁沿途所有局部对象,调用每个对象的析构函数
  3. 捕获处理:执行catch块代码,销毁异常对象

核心结论

异常处理有三重成本:

  • 编译时:代码体积增加5-15%
  • 正常路径:进入try块有少量开销
  • 异常路径:抛出异常成本是正常返回的千倍以上

使用原则:

  • 异常只用于真正的异常情况(如内存耗尽、构造函数失败)
  • 不要用异常处理预期中的错误(如输入验证、文件不存在)
  • 性能关键代码避免使用异常

条款16:谨记 80-20法则

一个程序80%的执行时间花费在20%的代码上。 优化工作的关键是找出这20%的代码进行优化,而不是均匀地优化所有代码。

条款17:考虑使用lazy evaluation(缓式评估)

1. Reference Counting(引用计数)

string s1 = "Hello"; string s2 = s1;


  在对s2只进行读操作时,s2只会和s1共享数据,只有对s2进行真正的写操作,资源拷贝才会发生。

### 2. 区分读和写

### 3. Lazy (缓式取出)

  有些LargeObject的体积很大,欲取出此类对象的所有数据,数据库相关操作程序可能成本极高,Lazy (缓式取出)只有在真正访问**对象的该数据**时才会从数据库中加载。<br>
  利用此技巧可以实现出“demand-paged”式的对象初始化行为,即在初始化LargeObject时,成员变量字段赋空,只有在对成员变量进行操作时,该字段才会从数据库中读取。

### 4. Lazy Expression Evaluation(表达式缓评估)

  不立刻计算表达式的值,只在需要时计算,可以用数据结构存储这种操作。

## 条款18: 分期摊还预期的计算成本(over-eager evaluation)

```cpp
class Test{
    public:
        int min() const;
        int max() const;
        int avg() const;
        ...
};

  eager evaluation 调用时立即评估   lazy evaluation 调用时存在某些数据结构中,只有在“这些函数的返回值真正需要被用到时”才计算  &emsp;over-eager evaluation,随时记录数据集中的最小值,最大值,使用时立刻返回。

1. Catching

  指将频繁访问的数据暂存在更快的存储层,以减少后续访问延迟。

2. Prefetching

  Prefetching 指提前预测即将访问的数据,并将其主动加载到缓存中(空间局部性
  vectort初始化有10个元素,但其在new操作时往往会申请两倍以上的空间。避免数组扩张的成本,空间换时间。

条款19:了解临时对象的来源

临时对象是不可见的:不会在源码出现。

1.当隐式类型转换(implicit type conversions)被施行起来以求函数调用能够成功

1.1函数参数传值
func(MyClass());  // 这里的 MyClass() 是临时对象
1.2 对象被传递给一个reference-to-const参数时
char s[25];
int test(const string& str);
test(s) //可以通过编译,期间会产生一个string临时对象,str参数被绑定到该临时对象上

当对象被传递给一个reference-to-non-const参数时,这种转换并不会发生,例子如下:

char s[25];
int test( string& str);
test(s) //无法通过编译,如果产生一个string临时对象,str参数被绑定到该临时对象上,str的改动不会影响s,违背了引用的直觉

2.当函数返回对象的时候

条款20:协助完成“返回值优化(RVO,return value optimition)"

在早期 C++(C++98 时代),下面代码在直觉上会产生多个对象

class Widget {};
Widget makeWidget() {
    return Widget();
}
Widget w = makeWidget();

如果按“语法表面理解”,似乎会发生:

  1. 构造一个临时 Widget
  2. 拷贝到函数返回值临时对象
  3. 再拷贝到 w

看起来是:

1 次构造 + 2 次拷贝 + 若干析构

但编译器可以消除这些临时对象,只会发生一次构造。RVO 的核心思想是:

直接在调用者的内存空间中构造返回对象。

函数匿名对象和命名对象编译器都可以进行RVO。

class Widget {};
Widget makeWidget() {
    Widget w;
    return w;
}
Widget w = makeWidget();

条款21:利用重载技术(overload)避免隐式类型转换(implicit typr conversions)

class Rational {
public:
    Rational(int numerator, int denominator = 1);
};

Rational r(1, 2);
r + 2;   // 2 会被隐式转换成 Rational(2,1)

不如直接提供

Rational operator+(const Rational&, int);
Rational operator+(int, const Rational&);

条款22: 考虑以操作符复合形式(op=)取代其独身形式(op)


T& T::operator+=(const T& rhs)
{
    // 核心逻辑
    return *this;
}  //  operator+=没有临时对象的产生,直接复值给左值自变量

T operator+(const T& lhs, const T& rhs)
{
    //核心逻辑
    return 局部对象
} //有临时对象的产生
T operator+(T lhs, const T& rhs)
{
    lhs += rhs;
    return lhs;
} //通过前者实现后者

operator+=没有临时对象的产生,因故比operator+有更高的性能倾向。

条款23: 考虑使用其他程序库

iostream库(c++)类型安全,可扩充,stdio库(c)性能更高

条款24: 了解virtual functions、multiple inheritance、 virtual base classes、runtime type identification(RTTI)的成本

  虚函数通过virtual tables (vtbl)与 virtual table pointer (vptr)实现,vbtl是一个虚函数指针构成的数组,在单一继承时,一个class只需一个vbtl和vptr就可以,在多重继承中,需要多个,每个基类对应一个。

  凡是声明虚函数的对象,都有一个隐藏的数据成员vptr,用来指向vtbl的位置,vptr在构造函数执行过程中被逐步设置(派生类对象生成,先执行基类构造函数,此时vptr被设置成基类的,再执行剩下的派生类特有的构造函数部分,此时vptr被设置成派生类的)。

  virtual base classes解决菱形继承中基类成员重复的问题

class A{...};
class B: virtual public A {};
class C: virtual public A {};
class D: public B, public C {};

其中A是virtual base class, B和C都采用虚拟继承,利用指针指向虚拟virtual base class成分,D对像的内存布局如下图:

虚继承内存分布.png

上图如果考虑到vptrs,D对象的内存布局为

虚继承内存分布2.png

Rtti 即是runtime type identification让基类指针在运行时识别对象的真实类型机制,其类型也存储在vbtl中,

rtti.png

1. 虚函数为什么不能inline

Base* p = new Derived;
p->f();  // 虚调用

底层等价于 p->vptr->f(p); vptr在运行时通过对象的构造函数才能确定。

技术 Techniques, Idioms, Patterns,本章讨论的是设计模式

条款25: 将constructor 和 non-member functions 虚化

1.协变返回类型(Covariant Return Types)

class Base {
public:
    virtual Base* clone() const {
        return new Base(*this);
    }
};

class Derived : public Base {
public:
    Derived* clone() const override {  // ✅ 协变返回
        return new Derived(*this);
    }
};

返回值类型变为子类完全合法,所谓constructor指的是一种设计模式

Base* p1 = new Derived;
Base* p2 = p1->clone();  // ⭐ 像“虚构造”

2. 将非成员函数的行为虚化

ostream& operator<<(ostream& s, const NLComponent& c)
{
 return c.print(s); //print为虚函数
}

写一个虚函数做实际工作,非虚函数只负责调用虚函数。

条款26: 限制某个class所能产生的对象数量

1. 允许零个或一个对象

class Singleton {
private:
    Singleton() = default;
    ~Singleton() = default;
    
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11保证静态局部变量的线程安全初始化
        return instance;
    }
    
    void doSomething() {
        // 业务逻辑
    }
};

也可以加入

private:
    static size_t numObjects ;

用来限制对象的数量。

2. 不同的对象构造状态

私有构造函数的类不允许作为base class

3. 允许对象的生生灭灭

4. 一个用来计算对象个数的base class

条款27: 要求(或禁止)对象产生于heap之中

1. 要求对象产生于heap之中(Heap-Based Objects)

修改类constructor和dctor的属性为private、protected,比如单例模式的设计无营养

2. 判断某个对象是否位于Heap内

重写operator new,当使用new operator申请对象时,记录该对象的地址到一个list中,就可以判断对象是否位于heap内,小聪明。

3. 禁止对象产生于heap中

重写static void *operator new(size_t size)操作符,并将其声明为private。

条款28: 智能指针

条款29: 引用计数(reference counting)

1.共享机制

2.写时复制

lazy evaluation的一种方式 String的operator[]有const版本和非const版本,编译在调用时根据对象是否是const调用相应的operator[]版本。 String的实现方式和shared ptr的实现方式非常相似,都用了写时复制和引用计数。

条款30:Proxy classes(替身类、代理类)

访问模式:User → Proxy → RealObject,可以帮助我们完成多维数组的重载,左值/右值的区分,压制隐式类型转换。 略

条款31:让函数根据一个以上的对象类型来决定如何虚化

一个“虚函数调用动作”称为“message dispatch”,函数根据多个函数而虚化则被称为multiple dispatch。 此章主要讨论这个问题,用到时查看。略

杂项讨论

条款32: 在未来时态下发展程序

条款33: 将非尾端类(non-leaf classes)设计为抽象类(abstract classes)

条款34: 如何在同一个程序中结合c++和c

extern C的作用

  1. C++有函数重载 → 编译时会修改函数名(比如func变成_Z4funci
  2. C语言没有函数重载 → 函数名就是原名(就是func

如果不加extern "C",C++编译器会把C函数名也改掉,导致链接时找不到函数。

条款35:让自己习惯于标准c++语言