Effective c++ 读书笔记

Reference

Effective C++

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

view C++ as a federation of languages

将C++视为一系列次语言(sublanguague)组成的语言

  • C
  • Object-Oriented C++
  • Template C++
  • STL
    对容器, 算法, 迭代器, 函数对象的规约有极佳的紧密配合与协调

条款02 尽量以const,enum,inline替换#define

Prefer consts, enums, inlines to #define

  • 使用#define定义的常量在预处理器替换,
    • 符号没有进入编译器的记号表symbol table, 编译错误时会带来困惑.
    • 预处理器盲目将宏替换为字面常量导致目标码出现多份相应字面常量

条款03 尽可能使用const

Use const whenever possible

  • 对函数返回值使用const
    const T operator+()
  • 成员函数的const性不同可以构成重载

成员函数什么时候应该是const

  • bitwise const
    bitwise const 阵营的人相信, 成员函数只有在不更该对象的任何成员变量(static 变量除外)时才可以说是const. 字面上来看, 就是它不更该对象内的任何一个bit.
    优点是很容易侦测违反点. bitwise正是C++对常量性(constness)的定义, 即const成员函数不可以更改对象内任何non-static成员变量.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class CTextBox {
    public:
    char& operator[](std::size_t position) const // const 声明其实不适当
    {
    return pText[position];
    }
    private:
    char* pText;
    };
  • logical constness
    不幸的是许多成员函数虽然不具备十足的constness却能通过bitwise测试. 这一派主张一个const成员函数可以修改它所处理的对象内的某些bits, 但只有在客户端侦测不出的情况下才得如此. 为了使bitwise的编译器logical一点, 我们引入了mutable关键字.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class CTextBox {
    public:
    std::size_t length() const;
    private:
    char* pText;
    mutable std::size_t textLength; // 最近一次计算的文本区块长度
    mutable bool lengthIsValid; // 目前的长度是否有效
    };

    std::size_t CTextBox::length const
    {
    if (!lengthIsValid) {
    textLength = std::strlen(pText);
    lengthIsValid = true
    }
    return textLength;
    }
  • 避免 const 与非 const 成员函数之间的重复
    对于”bitwise-constness非我所欲”的问题, mutable是一个解决办法, 但它不能解决所有的const相关难题.
    举例说, TextBlock (以及 CTextBlock )中的 operator[]不仅仅返回一个对恰当字符的引用,同时还要进行边界检查、记录访问信息,甚至还要进行数据完整性检测。如果将所有这些统统放在 const 或非 const 函数(我们现在会得到过于冗长的隐式内联函数,不过不要惊慌,在第 30 项中这个问题会得到解决)中,看看我们会得到什么样的庞然大物:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class TextBlock {
    public:
    ...
    const char& operator[](std::size_t position) const
    {
    ... // 边界检查
    ... // 记录数据访问信息
    ... // 确认数据完整性
    return text[position];
    }
    char& operator[](std::size_t position)
    {
    ... // 边界检查
    ... // 记录数据访问信息
    ... // 确认数据完整性
    return text[position];
    }
    private:
    std::string text;
    };

    这会导致代码重复, 以及伴随的编译时间, 维护, 代码膨胀等问题. 我们当然可以令代码放在一个成员函数(当然是私有的)中,然后让这两个版本的 operator[] 来调用它,但是我们的代码仍然有重复的部分, 比如函数调用和return语句。
    我们真正应该做的是实现operator的机能一次并使用它两次. 也就是说, 你必须令其中一个调用另一个, 这促使我们将常量性移除(casting away constness).
    一般而言, 转型是糟糕的想法, 甚至有专门的条款谈述这个事, 劝我们不要这么做.
    但是在本例中, const operator[]完全做到了non-const版本的一切, 唯一的不同就是返回值多了个const修饰. 这种情况下将返回值的const转除是安全的, 因为调用non-const operator都一定得有个non-const object. 所以这样做是安全的.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class TextBlock {
    public:
    ...
    const char& operator[](std::size_t position) const // 同上
    {
    ...
    ...
    ...
    return text[position];
    }
    char& operator[](std::size_t position) // 现在仅调用 const 的 op[]
    {
    return
    const_cast<char&>( // 通过对 op[] 的返回值进行转型,消去 const ;
    static_cast<const TextBlock&>(*this)// 为 *this 的类型添加 const ;
    [position]; // 调用 const 版本的 op[]
    );
    }
    ...
    };

    但是, 我们不应该反过来让const operator调用non-const operator, 因为我们清楚cosnt成员函数承诺绝不改变对象的逻辑状态(logical state), 而non-const operator没有这样的承诺. 而且如果我们这样做, 我们首先需要const_cast<TextBook&>(*this), 这是乌云罩顶的前兆.

总结

  • 某些东西声明为const可以帮助编译器检测出错误用法
  • 编译器强制实施bitwise constness, 但你编写程序时应该使用conceptual constness
  • +当const和non-const成员函数有着实质等价的实现时, 令non-const版本调用const版本可以避免代码重复

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

Make sure that object are initialize before they’re used.

当然如果你使用的是C part of C++不一定要遵守这条规定.

但我们最好遵守这项条款, 手动初始化内置类型.

如果class拥有许多构造函数, 且这个类有很多成员变量或base class, 那么在初始化列表中可以合理地遗漏一些变量. 一般我们将这部分变量的伪初始化放在一个private函数中进行.

尽量保证初始化列表中变量顺序和变量声明顺序一致.

跨编译单元内定义的non-local static对象的初始化次序

一个静态对象在被构造之后,它的寿命一直延续到程序结束。静态对象包括:全局对象、名字空间域对象、类内部的 static 对象、函数内部的 static 对象,文件域的 static 对象。函数内部的静态对象通常叫做 local static 对象(这是因为它们对于函数而言是局部的),其它类型的静态对象称为非局部静态对象。静态对象在程序退出的时候会被自动销毁,换句话说,在 main 中止运行的时候,静态对象的析构器会自动得到调用。

一个编译单元是这样一段源代码:由它可以生成单一目标文件。通常一个编译单元是以单一一个代码文件为基础,还要包括所有被 #include 进来的文件。

C++对跨编译单元内定义的non-local static对象的初始化次序无明确定义, 因为决定它们的初始化次序非常困难.

我们需要做的, 只是将每个non-local static搬到自己的专属函数内(该对象在此函数内被声明为static). 这些函数返回一个reference指向它所含的对象, 然后用户调用这些函数, 而不直接指涉这些对象. 换句话说, non-local static被local static对象代替了. 这是Singleton模式的一个常见实现手法.

这个手法的基础在于:C++保证, 函数内的local static对象会在”该函数被调用期间””首次遇上该对象的定义式”时初始化.

但是任何non-const static对象, 无论local non-local, 在多线程环境下”等待某事发生”都会有麻烦. 处理这个麻烦的一种做法是: 在程序的单线程启动阶段(single-threaded startup portion)手工调用所有reference-returning函数, 这样可消除与初始化有关的竞速形势(race conditions)

为避免在对象初始化之前过早使用, 需要做三件事

  • 手工初始化内置型non-member对象
  • 使用成员初值列
  • 针对初始化次序不确定性, 加强设计(reference-returning-function)

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

Know what functions C++ silently writes and calls

  • C++编译器会为你自动生成默认构造函数, 拷贝构造函数, 拷贝赋值运算符和析构函数

  • 编译器生成的默认构造函数是 non-virtual 的

  • 只有当生成的 copy assignment 代码合法且有适当机会证明它有意义, 其表现才会如copy构造函数. 万一两个条件中有一个不符合, 编译器会拒绝生成operator=.

    1
    2
    3
    4
    class A {
    const int a;
    int& b;
    };

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

Explicitly disallow the use of compiler-generated functions you do not want

  • 编译器生成的函数都是public

  • 阻止编译器自动生成函数的方法

    • 自行声明一个private的函数, 如果有人试图拷贝, 编译时会被阻止. 但这样做member函数和friend函数还是可以调用, 如果有人不慎调用, 链接时会获得链接错误(linkage error), 但是这种做法被广为采用, 如ios_base, basic_ios

    • 我们希望将错误提示尽可能转移到编译期, 我们可以声明一个base_class

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 我们只需要继承这个类
      class Uncopyable {
      protected:
      Uncopyable() {}
      ~Uncopyable() {}

      private:
      Uncopyable(const Uncopyable&);
      Uncopyable& operator=(const Uncopyable&);
      };

      但是这项技术有些微妙之处.

      • 不一定得public继承它

      • Uncopyable的析构函数不一定得是virtual

      • 由于它不含数据, 可能有empty base class optimization的资格, 但是如果它导致了多重继承, 就很可能会阻止空基类优化. 当然, Boost 的noncopyable也不错, 就是名字有点怪.

        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
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        #ifndef BOOST_CORE_NONCOPYABLE_HPP
        #define BOOST_CORE_NONCOPYABLE_HPP

        #include <boost/config.hpp>

        namespace boost {

        // Private copy constructor and copy assignment ensure classes derived from
        // class noncopyable cannot be copied.

        // Contributed by Dave Abrahams

        namespace noncopyable_ // protection from unintended ADL
        {
        class noncopyable
        {
        protected:
        #if !defined(BOOST_NO_CXX11_DEFAULTED_FUNCTIONS) && !defined(BOOST_NO_CXX11_NON_PUBLIC_DEFAULTED_FUNCTIONS)
        BOOST_CONSTEXPR noncopyable() = default;
        ~noncopyable() = default;
        #else
        noncopyable() {}
        ~noncopyable() {}
        #endif
        #if !defined(BOOST_NO_CXX11_DELETED_FUNCTIONS)
        noncopyable( const noncopyable& ) = delete;
        noncopyable& operator=( const noncopyable& ) = delete;
        #else
        private: // emphasize the following members are private
        noncopyable( const noncopyable& );
        noncopyable& operator=( const noncopyable& );
        #endif
        };
        }

        typedef noncopyable_::noncopyable noncopyable;

        } // namespace boost

        #endif // BOOST_CORE_NONCOPYABLE_HPP
  • 在新标准中, 我们可以直接使用= delete, 来防止编译器自动提供函数

条款07: 为多态基类声明virtual析构函数

Declare destructors virtual in polymorphic base classes

如果有这样一种情形, 你需要返回一个堆上的子类型对象, 子类型有很多种(多态), 但是你必须返回基类指针, 这时候如果需要释放堆上的空间, 如果没有虚函数, delete会直接调用~Base(), 只有定义了虚析构函数, delete才会调用正确的析构函数.

虚函数虽好, 但是有两个缺点

  • vptr(虚表指针)在32-bit机上占用32bits, 在64-bit机上占用64bits, 开销较大
  • 因为含虚函数的对象结构和其他语言的对象一致(其他语言的虚函数对应物并没有vptr), 因此不再可能将C++对象传递到(或接受自)其他语言所写的函数

因此, 需要谨慎使用virtual, 与此同时, 当你继承基类的时候, 一定要弄清楚自己的需求(比如是否需要在堆上创建对象), 以及基类的析构函数是否为虚. 当然, 我们可以人为地为哪些容易使用错误的类加上禁止派生的限制(final关键字)

如果你希望有一个抽象类, 但是手上没有任何pure virtual函数, 你可以定义纯虚析构函数.

但是, 你必须为纯虚析构函数提供定义, 否则编译器在派生类析构函数调用基类析构函数时, 连接器会抱怨. (存疑)

总结

  • 给多态基类应该主动声明virtual析构函数
  • 非多态基类,没有virtual函数,不要声明virtual析构函数

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

Prevent exceptions from leaving destructors

构造函数可以抛出异常,析构函数不能抛出异常。

因为析构函数有两个地方可能被调用。一是用户调用,这时抛出异常完全没问题。二是前面有异常抛出,正在清理堆栈,调用析构函数。这时如果再抛出异常,两个异常同时存在,异常处理机制只能terminate().

构造函数抛出异常,会有内存泄漏吗? 不会

1
2
3
4
5
6
7
8
try {
// 第二步,调用构造函数构造对象
new (p)T; // placement new: 只调用T的构造函数
}
catch(...) {
delete p; // 释放第一步分配的内存
throw; // 重抛异常,通知应用程序
}
  • 析构函数绝对不要吐出异常, 如果析构函数调用的函数可能抛出异常, 析构函数应该捕捉任何异常, 然后吞下它们, 或者结束程序(最好如此)
  • 如果用户需要对某个操作函数运行期间抛出的异常作出反应, 那么class应该提供一个普通函数(而非析构函数)执行操作
1
2
3
4
5
6
7
8
9
// 比较好的写法
class DBConn {
~DBConn()
{
if (!closed) {
try (close())
}
}
};

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

Never call virtual functions during construction or destructiom

构造和析构过程中,对基类子对象的虚函数调用通过基类子对象的虚表指针执行(调用的不是派生类版本, 而是基类版本)

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

条款11: 在operator= 里处理自我赋值

1
2
3
4
5
Widget& Widget::operator== (const Widget& rhs){
if(this == &rhs) return *this

···
}

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

Copy all parts of an object

  • 记得实现拷贝构造函数和赋值操作符的时候,调用base的相关函数
  • 不要让某个copy函数调用另一个, 应该让拷贝构造函数和赋值操作符调用一个共同的函数,例如init

资源管理

条款13: 以对象管理资源

Use objects to manage resources

文章目录
  1. 1. Reference
  2. 2. 条款01 视C++为一个语言联邦
  3. 3. 条款02 尽量以const,enum,inline替换#define
  4. 4. 条款03 尽可能使用const
  5. 5. 条款04 确定对象被使用前已先被初始化
  6. 6. 条款05 了解C++默默编写并调用哪些函数
  7. 7. 条款06 若不想使用编译器自动生成的函数, 就该明确拒绝
  8. 8. 条款07: 为多态基类声明virtual析构函数
  9. 9. 条款08: 别让异常逃离析构函数
  10. 10. 条款09: 绝不在构造和析构过程中调用virtual函数
  11. 11. 条款10: 令operator= 返回一个reference to *this
  12. 12. 条款11: 在operator= 里处理自我赋值
  13. 13. 条款12: 复制对象时勿忘其每一个成分
  14. 14. 资源管理
    1. 14.1. 条款13: 以对象管理资源
|