《effective C++》-第1-5章-学习笔记上

《effective C++》-第1-5章-学习笔记上

[TOC]
本文为《effective C++》的学习笔记上半部, 涵盖内容第 1-5 章.

让自己习惯C++

Item1. C++是语言联邦

  1. C 是基础

  2. OOB, 类, 封装, 继承, 多态, 虚函数

  3. Template C++ 范形编程(generic programming)

  4. STL(容器, 迭代器, 算法, 函数对象)

  • 传值: 内置类型: pass-by-value, 自建类型: pass-by-value-to-const

Item2. Prefer const, enum, inline than #define

  • #define 的缺点: 无自动定义域(scope), 语法解析易有 bug, debug 时不会显示变量意义.
  • const 方案:
  1. 一般: 头文件里 const double A = 0.1;
  2. const 指针: const char* Name="ME";
  3. class 成员变量: static.

3.1 注意类里只能声明无法定义(哪怕声明里已经初始化值了), 在类声明下面初始化. 例子如下:

1
2
3
4
5
class CostEstimate {
private:
static const double FudgeFactor;
...};
const double CostEstimate::FudgeFactor = 1.35;

3.2 有些编译器不支持声明里初始化, 同时数组又必须明确大小: 使用 enum hack

1
2
3
4
class GamePlayer {
private:
enum { NumTurns = 5 };};
int scores[NumTurns];

ps. 补充一点, C 中 const 与 C++ const 中的区别. 以下为强制修改 const 变量值.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
const int n = 10;
int *p = (int*)&n; //必须强制类型转换
*p = 99; //修改 const 变量的值
printf("%d\n", n);
return 0;
}

C 的情况下输出为 99, C++ 的情况下输出为 10.
因为在 C 里, n 是加载到内存里, 修改了内存打印值会变化. 但是 C++ 在编译阶段就会把 n 替换为 99, 因此表达式中的 n 一直不变.
C++ 这么做是为了提升执行效率.

  • enum hack 方案
    enum 无法取得地址, 因此特定场景可以利用这一点.

  • inline template 函数方案

Item3. Use const whenever possible

const 一般性知识

  • 对指针 const: *左边->被指物常量(* 修饰整体, 也就是被指物), *右边->指针本身常量(* 没有修饰整体, 也就是指针本身), *两侧->指针被指物都是常量.
  • 迭代器可以看成 T*, const 迭代器->T* const, const_iterator->const T*.
  • 函数返回值设成 const 可以防止用户的误操作引发问题.
1
2
3
4
5
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
Rational a, b, c;
...
(a * b) = c; //无理操作无法通过编译

const 成员函数

  1. const 成员函数的意义: 接口易理解, 操作 const 成员变量.
  2. const 性与否可以导致重载.
  3. const 成员函数里想要修改成员变量: 使用 mutable 关键字.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CTextBlock {
public:
...
std::size_t length() const;
std::size_t length() ; //const性可以被重载
private:
char *pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const{
if (!lengthIsValid) {
textLength = std::strlen(pText);
lengthIsValid = true;}
return textLength;
}
  1. 重载 const 性, 导致代码重复, 不够 fashion: non-const 成员函数里调用 const 成员并把 const 成员函数转换.
1
2
3
4
5
6
7
8
class TextBlock {
public:
const char& operator[](std::size_t position) const{
return text[position];}

char& operator[](std::size_t position){
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);};
}

static_cast 的目的是调用 const 版本的 [] 操作符, 否则函数会优先调用自己, 即 non-const [], 导致无限套娃.
const_cast的目的是把返回值脱掉 const 性.
注意, 反向操作不被允许. 不要在 const 成员函数里调用 non-const 成员函数, 错误.

Item4. Make sure that object is initilized bebore it is used

  • C++(非 C 部分)会初始化对象为 0, null(一部分类型的对象, 例如全局变量, 原理是生成可装载文件时, 能够省略一部分文件大小空间), 然而 C 不会, 为了保持统一, 还是要初始化.
  • 内置类型直接赋值即可, 对象的话需要初始化其成员变量.
  • 初始化成员变量不要混淆赋值(assignment)与初始化(initilization), 前者构造函数体内, 后者为初始化列表.
  • 内置类型上无差异, 但是对类成员变量通过赋值初始化, 需要先调用 default 构造函数, 然后执行赋值语句. 不如初始化列表快, 后者直接在构造过程中完成初始化.
  • const/reference 类型的变量不能通过赋值初始化, 即, 必须使用初始化列表.
  • 初始化次序: 声明的顺序, 但是有一个问题需要解决: 不同编译单元内定义的non-local staic 对象的初始化顺序.
    直观理解就是不同 cpp/cc 文件里面的共用 static 的对象, C++ 没有规定初始化顺序因此可能导致有依赖关系的两个对象不能保证初始化顺序一定一致.
    解决办法: 通过把 non-local staic 对象封装到函数里, 通过函数的调用保证顺序, 同时这样做还有一个好处是, 如果实际没有使用 non-local staic 对象的话, 就不会去调用函数, 省去了默认构造析构的成本(main 函数开始前与结束后的进行的操作). 如果是光秃秃的 static 对象无法避免构造与析构.
    这个思路是Singleton模式(单例).

构造/析构/赋值运算

Item5. Know what functions C++ silently makes and calls

  • 构造/析构/赋值运算若不手动创建, 会被 C++ 默默创建(只在被调用时).
1
2
3
4
5
6
7
8
class Empty{};
//等同于如下默认, 并且都是 public 与 inline 的
class Empty {
public:
Empty() { ... }
Empty(const Empty& rhs) { ... }
~Empty() { ... }
Empty& operator=(const Empty& rhs) { ... }};
  • C++ 自动创建的析构函数为 non-virtual.
  • C++ 自动创建的 copy 构造函数仅把成员变量里的 non-static 的成员变量拷贝一份.
  • C++ 会根据拒绝自动创建 operator=, 例如引用的 copy 赋值是非法的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value);
...
private:
std::string& nameValue;
const T objectValue;
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s; //非法

Item6. Explicitly disallow the use of compilier-generated functions you do not want

两种做法可以实现禁用:

  1. 放入 private, 并且不予实现. 虽然其他成员函数/友元函数可以访问 private 但会在编译期间产生 linkage error.
  2. 继承 base class
1
2
3
4
5
6
7
8
9
10
11
12
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {} //不声明为 virtual 也可以, 因为没有其他成员变量/函数.
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable {
...
};

ps. C++11 引入了 =delete 代替上面的功能.

Item7. 为多态基类声明 virtual 析构函数

  • polymorphic base class(多态类)应该声明一个 virtual 析构函数, 也就是说只要 clas s内带有任何的 virtual 函数, 它就应该拥有一个 virtual 析构函数.

  • 如果类的设计不是作为 base class 使用或者不具备多态性, 不应该声明为 virtual 析构函数. 原因是 vptr 以及 vtbl 会增加类的大小, 极端情况下影响移植性.

  • STL 的容器不是用来继承的, 因为它们都是 non-virtual 析构函数. 再换句话说如果注意析构的对象指针类型正确(不使用多态性), 也是可以继承 STL 容器的.

1
2
3
4
5
6
7
8
9
class SpecialString: public std::string { // bad idea! std::string has a non-virtual destructor
...
};
SpecialString *pss =new SpecialString("Impending Doom");
std::string *ps;
...
ps = pss; // SpecialString* ⇒ std::string* 如果代码设计时明确指针类型就不会有下面的问题了.
...
delete ps;// undefined! In practice, *ps’s SpecialString resources will be leaked, because the SpecialString destructor won’t be called.
  • 设置一个纯虚类专门用作析构用
1
2
3
4
5
class AWOV { 
public:
virtual ~AWOV() = 0; // declare pure virtual destructor
};
AWOV::~AWOV() {} // 必须显示定义

Iem8. 别让异常逃离析构函数

  • 两种方式避免异常逃离析构函数后导致对象无法正常析构的问题.

    1. 析构函数里出现异常就结束程序

      1
      2
      3
      4
      5
      6
      DBConn::~DBConn(){
      try { db.close(); }
      catch (...) {
      make log entry that the call to close failed;
      std::abort();}
      }
    2. 吞下异常

      1
      2
      3
      4
      5
      6
      DBConn::~DBConn(){
      try { db.close(); }
      catch (...) {
      make log entry that the call to close failed;}
      }
      //虽然损失了分析异常机会但是也比资源泄漏等问题好
  • 别让异常直接发生在析构函数内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class DBConn {
    public:
    ...
    void close(){ // new function for client use
    db.close();
    closed = true;}

    ~DBConn(){
    if (!closed) {
    try { // close the connection if the client didn’t
    db.close(); }
    catch (...) { // if closing fails, note that and terminate or swallow
    make log entry that call to close failed;
    ...}
    }
    }
    private:
    DBConnection db;
    bool closed;
    };

Item9. Never call virtual functions during construnction or destruction

  • 问题: 在 base class 构造期间, virtual 函数不是 virtual 函数.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Transaction { 
    public:
    Transaction();
    virtual void logTransaction() const = 0;
    ...
    };
    Transaction::Transaction() {
    ...
    logTransaction(); }

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

    BuyTransaction b;

    也就是说, 先构造 Transaction, 再构造 BuyTransaction 类时使用的 logTransaction 不会是 deried class- BuyTransaction 的 logTransaction, 因为 BuyTransaction 里的 local 成员还没有初始化.

  • 解决办法, 在构造/析构里的 non-virtual 函数里, 把 derived class 中独特于 base class 里的成分传递给 base class.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Transaction {
    public:
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const; // now a non-virtual func
    ...
    };
    Transaction::Transaction(const std::string& logInfo){
    ...
    logTransaction(logInfo);}

    class BuyTransaction: public Transaction {
    public:
    BuyTransaction( parameters )
    : Transaction(createLogString( parameters )) // pass log info to base class constructor
    { ... }
    ...
    private:
    static std::string createLogString( parameters ); //注意此处static的使用,可以保证即便子类没有被初始化也可以传值到父类
    };

Item10. 令 operator= 返回一个 refrence to *this

按照惯例, 返回一个 refrence to *this 可以实现连续 = 赋值. 同理对 operator +=.

1
2
3
4
5
6
7
8
9
10
11
12
int x, y, z;
x = y = z = 15;

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

Item11. 在 operator= 中处理自我赋值

  • 自我赋值很多是不易察觉的隐式方式.

    1
    2
    3
    4
    5
    a[i] = a[j]; //index可能相同
    *px = *py; //指针可能指向相同
    class Base { ... };
    class Derived: public Base { ... };
    void doSomething(const Base& rb,Derived* pd); //可能指向的都是子类
  • 解决方法1: 证同测试(identity test)

    1
    2
    3
    4
    5
    6
    Widget& Widget::operator=(const Widget& rhs){
    if (this == &rhs) return *this;
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
    }
  • 解决方法2: 不去检测, 避免异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Widget& Widget::operator=(const Widget& rhs){
    Bitmap *pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
    }

    //更fashion的方式--swap
    Widget& Widget::operator=(const Widget& rhs){
    Widget temp(rhs);
    swap(temp);
    return *this;
    }

    //pass-by-value, copy 动作从函数体内放在函数参数构造阶段, 提供编译器优化空间
    Widget& Widget::operator=(Widget rhs){
    swap(rhs);
    return *this;
    }

Item12. 复制对象不要忘记复制每一个成分

  • 既然拒绝编译器提供的 copying 函数/copy assignment 操作符, 编译器会提供充足的自由权, 自由的反面是编译器不会去检查很多细节.

  • 对象里的成分不止内置成员变量, 还包括类成员变量(构造函数), 多态时的继承类(构造函数). 对于子类的 copy 问题, 可以在其 copying 函数/copy assignment 操作符调用父类的 copying 函数/copy assignment 操作符.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), // invoke base class copy ctor
    priority(rhs.priority){
    logCall("PriorityCustomer copy constructor");}

    PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
    logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs); // assign base class parts
    priority = rhs.priority;
    return *this;}
  • 不要尝试 copying 函数与 copy assignment 操作符之间的互相调用.

资源管理

Item13. 以对象管理资源

  • newdelete 之间有太多可能性, 包括代码维护性.
  • 更好的方式是 1.获得资源后立即放入管理对象, 2.管理对象利用析构函数确保资源被释放.
  • 引入 auto_ptr(可以完全被指针指针代替)与智能指针, 此处关于智能指针的使用有些落后了(例如 std::shared_ptr), 不再总结.

Item14. 小心资源管理类中的 copying 行为

copying 行为的四种处理方式

  1. 禁止复制
  2. 引用计数
  3. 深拷贝
  4. 转移底部资源的拥有权

Item15. 在资源管理类中提供对原始资源的访问

  • 使用智能指针对原始资源的访问

    1. get() 函数
    2. 重载的 operator ->operator *
  • 构建自己的资源管理类 RAII class, 即资源在构造时获得, 在析构期间释放.

    1. 设置 get() 成员函数显式转换
    2. 使用隐式转换, 复制时很容易出问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    FontHandle getFont(); // from C API — params omitted for simplicity
    void releaseFont(FontHandle fh); // from the same C API
    class Font { // RAII class
    public:
    explicit Font(FontHandle fh): f(fh){} // acquire resource;use pass-by-value, because the C API does
    ~Font() { releaseFont(f ); } // release resource handle copying
    FontHandle get() const { return f; } // explicit conversion functio
    operator FontHandle() const // implicit conversion function
    { return f; }
    ...
    private:
    FontHandle f; // the raw font resource
    };

    void changeFontSize(FontHandle f, int newSize); // from the C API
    Font f(getFont());
    int newFontSize;
    ...
    changeFontSize(f.get(), newFontSize); //显式转换
    changeFontSize(f, newFontSize); //隐式转换

    Font f1(getFont());
    ...
    FontHandle f2 = f1; //原意是 copy 一个 Font 对象, 反而将 f1 隐式转换为底部的 FontHandle 然后才复制它.

Item16. 使用 newdelete 时要采取相同的形式

  • 单个对象: new <-> delete
  • 数组对象: new [] <-> delete[]
  • 使用 typedef/using 时注意不要对数组声明, 否则很容易使用 delete, 代替方案是使用 STL 容器.

Item17. 以独立语句将 newed 对象置入智能指针

  • 不要把智能指针初始化的 new 操作放入函数参数中. 原因是 C++ 中函数参数的初始化顺序是不定的, 哪怕是所有参数的小步骤也是不定的.

    1
    processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

    函数参数初始化子步骤的顺序可能如下, new 后出现了资源泄漏:

    • Execute “new Widget”.
    • Call priority.
    • Call the tr1::shared_ptr constructor.
  • 解决办法: 通过代码语句顺序保证执行顺序.

    1
    2
    std::tr1::shared_ptr<Widget> pw(new Widget); 
    processWidget(pw, priority());

设计与声明

Item18. 让接口容易被正确使用, 不易被误用

  • 原则 1: 非期待的用户行为不应该通过编译.

  • 原则 2: 限制用户输入参数的顺序, 类型, const 性(Item3). 如下面代码, 约束年月日的输入规则.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    struct Day { 
    explicit Day(int d): val(d) {}
    int val;
    };

    //static 的用处参看 Item4
    class Month {
    public:
    static Month Jan() { return Month(1); }
    static Month Feb() { return Month(2); }
    ...
    static Month Dec() { return Month(12); }
    ...
    private:
    explicit Month(int m);
    ...
    };

    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::Mar(), Day(30), Year(1995));
  • 原则 3: 保证行为一致性(consistency), 例如与内置类型行为的一致性, STL 好用的一个原因即是不太容器的操作一致性较好, 例如都有行为一致的 size() 成员函数.

  • 原则 4: 要求客户进行顺序式的输入, 有可能客户会忘记, 因此要使用技术手段先发制人强制客户不忘记.

    1
    2
    Investment* createInvestment(); //客户有可能忘记 delete
    std::tr1::shared_ptr<Investment> createInvestment(); //返回值为智能指针强迫客户把指针放入智能指针进行管理
  • 一个细节: 用户如果不要智能指针非要光指针, 调用 shared_ptr 的删除器构造函数, 第一个参数为被管理的指针, 第二个指针为删除函数把引用次数变为 0.

    1
    2
    3
    4
    5
    6
    std::tr1::shared_ptr<Investment> createInvestment(){
    //使用 static_cast 把 int 0 转换成 null 指针
    std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
    ...
    return retVal;
    }
  • shared_ptr 的一个好处: 消灭 cross-DLL problem, 即一个对象在动态连接库 DLL 中被 new 创建, 去再另一个 DLL 中被 delete, 有些平台可能会导致运行错误.

Item19. 设计 class 犹如设计 type

设计一个 class 之前需要问自己的问题:

  • 新 type 的对象如何创建以及销毁: 构造/析构函数, new/delete.
  • 对象的初始化与对象的赋值的差别: Item4.
  • 新 type 如果被 pass-by-value 意味着什么: copy 函数定义一个 type 的 pass-by-value 行为.
  • 什么是新 type 的合法值: 反映在构造函数/赋值符号/setter 函数上, 也影响异常的抛出.
  • 考虑继承图系吗: virtual/non-virtual 函数的区分使用.
  • 新 type考虑转换吗: 隐式与显式的转换, Item15.
  • 什么样的操作符以及成员函数对此新 type 而言是合理的(Item23,23,26).
  • 什么样的标准函数应该被雪藏: private 属性.
  • 谁该取用新 type 的成员: 决定 private, protected, public 属性.
  • 什么是新 type 的未声明接口: 效率, 异常(Item29), 线程安全.
  • 新 type 的抽象度如何: 是 class 还是 class template.
  • 真的需要一个新 type 吗.

Item20. prefer pass-by-reference-to-const to pass-by-value

  • 原因1: 效率. pass-by-value 会构造一个临时量, 最后再析构, 一旦存在类成员变量以及继承类, 成本膨胀很厉害.
  • 原因2: 避免 slicing 问题, 子类通过父类类型传入 pass-by-value, 导致被切割.
  • 内置类型 pass-by-value 效率更高, 但不能类比, 小的 class 依旧考虑 pass-by-value.

PS. 从 C++11 起一般默认会返回值优化(NRVO), 这一条感觉只适用于之前的 C++ 版本.

Item21. 必须返回对象时, 不要妄想返回其引用

  • 问题: 想要省去 pass-by-value 的成本, 但是返回引用会导致返回销毁的临时对象导致错误.

  • 函数创建对象的两种方式: stack 上创建, heap 上创建.

  • 解决1: heap 上创建返回引用->如何 delete?

  • 解决2: stack 上创建返回引用, 想法使用 static 变量存储.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const Rational& operator*(const Rational& lhs, const Rational& rhs) {
    static Rational result; // static object to which areference will be returned
    result = ... ;
    return result;
    }

    bool operator==(const Rational& lhs, const Rational& rhs); // an operator== for Rationals

    Rational a, b, c, d;
    ...
    if ((a * b) == (c * d)) { //出错
    do whatever’s appropriate when the products are equal;
    } else {
    do whatever’s appropriate when they’re not;}

    因为if (operator==(operator*(a, b), operator*(c, d))) 对比的一直是 static 量, 会一直返回 true.

Item22. 将成员变量声明为 private

  • 保证调用的操作一致性.
  • 封装的好处: 将成员变量隐藏在函数接口的背后, 提供所有实现可能性的弹性. 不封装意味着不能改变, 或者改变成本太大.
  • 不要考虑 protected 的折衷, protected 的性质与 public, 只要改变实现就意味着大量的代码改动, 调试, 测试, 文档.

Item23. Prefer non-member non-frined functions to member functions

  • non-member non-frined 函数带来更好的封装性, 包裹弹性, 机能扩张性.
  • 所谓的封装性与访问可能性成反比, 对 private 的对象访问的可能性越大封装性越低.
  • 可以把 non-member non-frined 函数与类放在同一个 namespace 里, 这样不需要改变类即可扩充功能.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything(); //成员函数做法
...
};

//非成员-友元函数做法
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();}

Item24. 若所有参数都需要类型转换(隐式), 采用非成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Rational {
public:
Rational(int numerator = 0,int denominator = 1); //未声明 explicit, 因此支持隐式转换
int numerator() const;
int denominator() const;
const Rational operator*(const Rational& rhs) const;
private:
...
};

result = oneHalf * 2; // fine
result = 2 * oneHalf; // error!
result = oneHalf.operator*(2); // fine
result = 2.operator*(oneHalf ); // error!

//应该使用非成员函数
class Rational {
... // contains no operator*
};
const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

友元函数要尽量避免, 如果可以应该改变为成员函数.

Item25. 考虑写出一个不抛出异常的 swap 函数

  • 当使用 pimpl 手法时(pointer to implementation), 用默认的 std::swap 效率不高.

  • 当 class 不是 template class 时, 可以全特化(total template specialization) std::swap.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Widget { // class using the pimpl idiom
    public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs) {
    ...
    *pImpl = *(rhs.pImpl);
    ...
    }
    void swap(Widget& other){
    using std::swap; //
    swap(pImpl, other.pImpl);
    }
    ...
    ...
    private:
    WidgetImpl *pImpl; // ptr to object with this
    }

    namespace std {
    template<>
    void swap<Widget>(Widget& a,Widget& b){
    a.swap(b);} // to swap Widgets, call their swap member function
    }
  • 当 class 为 template class 时, 对 function template 偏特化(partially specialize)是不被运行的, C++ 只允许对 class 进行偏特化.

    1
    2
    3
    4
    5
    6
    namespace std {
    template<typename T>
    void swap<Widget<T> >(Widget<T>& a, //不被允许的偏特化
    Widget<T>& b)
    { a.swap(b); }
    }

    解决: 类外声明一个非成员函数, 函数内调用 swap 成员函数, 最好都放在一个 namespace 下面.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace WidgetStuff {
    ...
    template<typename T>
    class Widget { ... };
    ...
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b){
    a.swap(b);}
    }
  • 上面如果没有放在同一个 namespace 下面如何查找到合适的 swap? 依据 C++ 的名称查找法则(name lookup rules).

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T>
    void doSomething(T& obj1, T& obj2){
    //优先查找调用T特化的版本,若不存在调用std下的
    using std::swap; // make std::swap available in this function
    ...
    swap(obj1, obj2);
    ...
    }
  • 为用户定义类型进行 std template 全特化是好的, 但千万不要尝试在 std 内添加某些对 std 而言全新的东西.

实现

Item26. 尽可能延后变量定义式的出现时间

  • 变量定义式的延迟的第一层意思是, 用到前再定义, 万一执行不到使用的过程, 定义也是白白定义.
  • 变量定义式的延迟的第二层意思是, 延迟定义直到能够给它初值实参为止, 这样可以避免构造/析构成本.
1
2
3
4
5
std::string encryptPassword(const std::string& password){
...
string encrypted(password); //而不是string encrypted;encrypted = password;
encrypt(encrypted);
return encrypted;}

Item27. 尽量少做转型

  • 少用 C 的旧式转型语法, 拥抱 C++ 的新式转型语法.

    1
    2
    3
    4
    5
    6
    7
    8
    //旧式
    (T) expression // cast expression to be of type T
    T(expression) // cast expression to be of type T
    //新式
    const_cast<T>(expression)
    dynamic_cast<T>(expression)
    reinterpret_cast<T>(expression)
    static_cast<T>(expression)
  • C++ 中子类的地址不止一个, 一个为 Derived* 指向它的指针, 也有一个 Base* 指向它的指针. 指针间的 offset 取决于不同的平台编译器.

  • 尽量避免转型: 例子1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Window { // base class
    public:
    virtual void onResize() { ... } // base onResize impl
    ...};
    class SpecialWindow: public Window { // derived class
    public:
    virtual void onResize() { // derived onResize impl;
    static_cast<Window>(*this).onResize(); // cast *this to Window, then call its onResize; this doesn’t work!
    ...}
    ...};

    static_cast<Window>(*this).onResize(); 此句中虽然转型成功成基类指针, 但执行的对象是临时的对象, 如果 onResize 函数会修改成员变量的话, 修改的也是临时的基类对象的成员变量, 对实际生成的子类没有任何影响.

    解决办法, 取消转型, 直接使用 Window::onResize();.

  • 避免转型的例子2, dynamic_cast 会很浪费资源. 并且这种需求可以被其他实现替代. 一般而言, 希望使用 dynamic_cast 的场景为, 手头上只有一个 Base* 但是想要调用 Derived* 成员函数. 有 2 种解决办法:

    1. 使用 Derived* 智能指针, 多态多种子类的时候虽然麻烦一些.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      class Window { ... };
      class SpecialWindow: public Window {
      public:
      void blink();
      ...
      };

      typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
      VPW winPtrs;
      ...
      for (VPW::iterator iter = winPtrs.begin(); // undesirable code:
      iter != winPtrs.end(); // uses dynamic_cast
      ++iter) {
      if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
      pw->blink();
      }

      //代替方案
      typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
      VPSW winPtrs;
      ...
      for (VPSW::iterator iter = winPtrs.begin(); // better code: uses
      iter != winPtrs.end(); // no dynamic_cast
      ++iter)
      (*iter)->blink();
    2. Base 类里制造空的 virtual 函数(纯虚函数)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      class Window {
      public:
      virtual void blink() {}
      ...
      };

      class SpecialWindow: public Window {
      public:
      virtual void blink() { ... }
      ...
      };

      typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
      VPW winPtrs;
      ...
      for (VPW::iterator iter = winPtrs.begin();
      iter != winPtrs.end();
      ++iter)
      (*iter)->blink();

Item28. Avoid returning “handles” to object internals

  • handles 指的是引用, 指针, 迭代器.
  • handles 暴露本不想被 read 甚至是 write 的对象. 对策是对返回值 handle 加上 const 修饰.
  • hadles 可能会导致 dangling 空悬, 尤其是生成临时对象的时候.
  • 例外是 STL 里的容器, operator[] 为返回引用的函数, 但这不是常态.

Item29. 为异常安全而努力是值得的

  • 异常安全函数提供三个保证之一

    1. 基本承诺: 如果异常被抛出, 程序内的任何事物仍保持在有效状态下, 没有任何对象或者数据结构会因此而被破坏, 所有对象处于前后一致的状态.
    2. 强烈保证: 如果异常被抛出, 程序状态不变. 如果成功即是完全的成功, 失败则能回退到调用函数之前的状态.
    3. 不抛掷保证: 承诺绝对不抛出异常. 太难被满足.
  • copy and swap 策略可以提供一个实现”强烈保证”的思路. 同时结合 pimpl idiom, 即把隶属于对象的数据封装到另一个类里, 然后赋予原对象一个指针指向数据类.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct PMImpl { 
    std::tr1::shared_ptr<Image> bgImage;
    int imageChanges;
    };

    class PrettyMenu {
    ...
    private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pImpl;
    };

    void PrettyMenu::changeBackground(std::istream& imgSrc){
    using std::swap;
    Lock ml(&mutex);
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc));
    ++pNew->imageChanges;
    swap(pImpl, pNew);
    }
  • 强烈保证的实现很多时候也是不切实际的, 例如短板效应, 需要为最短的那个函数包装, 同时涉及到非局部数据时无法把已经修改的数据改回去.

  • 强烈保证不好实现时, 实现基本承诺也无可厚非.

Item31. 透彻了解 inling 的里里外外

  • inline 的代价: 二进制文件的膨胀->潜在的额外的换页(paging).

  • function template 通常放在头文件里, 但不推荐被声明为 inline. 除非能保证所有 T 下的函数都是 inline 的.

  • inline 与 virtual 冲突.

  • inline 的具体行为要依据编译器, 以及调用方式, 例如函数指针的情况下无法最终 inline. 即便不是函数指针, 构造/析构函数里也有可能产生函数指针指向 inline 函数.

    1
    2
    3
    4
    5
    inline void f() {...} 
    void (*pf )() = f;
    ...
    f(); //最终inline
    pf();//无法最终inline
  • 不推荐将构造/析构函数声明为 inline, 因为构造/析构背后太大 C++ 自动做的工作了, 包括异常.

  • 尤其是 Base 类里的构造函数会被所有 Derived 类继承, 如果同时调用其他对象做成员变量, 会导致 N 多重复的构造函数代码.

  • 考虑更改 inline 带来的冲击, 如果 inline 函数被编进多个程序, 那么大家都得重新编译.

  • 不是所有的调试器都支持 inline 函数的断点调试.

  • inline 只适合小型, 被频繁调用的函数上.

Item32. 将文件间的编译依存关系降至最低

  • 文件间编译依存关系: include 进去的文件只要有一个变化(或者嵌套的 include)都会导致重新编译.

  • pimpl idiom: 把类分为 2 个类, 一个为接口类, 一个为实现接口的类. 然后在接口类声明依赖的对象, 这样实现类里与依赖对象分离.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <string> 
    #include <memory>
    class PersonImpl;
    class Date;
    class Address;
    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::tr1::shared_ptr<PersonImpl> pImpl;
    };
  • 无法最终 inline代替定义的依存性:

    1. 如果使用 object 引用/指针可以完成, 不要使用 object value.
    2. 如果能够, 使用 class 声明式代替定义式.
    3. 为声明式和定义式提供不同的头文件.
  • 接口类被称为 Handle class

  • 或者把 Handle class 抽象为一个抽象基类, 不含成员变量/构造函数, 只有一个 virtual 析构函数以及一系列 virtual 的接口函数. 被成为 interface class.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Person {
    public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    static std::tr1::shared_ptr<Person> //为interface类创建对象的方法
    create(const std::string& name,
    const Date& birthday,
    const Address& addr);
    ...
    };

《effective C++》-第1-5章-学习笔记上

https://www.chuxin911.com/effective_c++_1_20211121/

作者

cx

发布于

2021-11-21

更新于

2022-11-23

许可协议