"> Effective C++读书笔记(上) | Stillwtm's Blog
0%

Effective C++读书笔记(上)

本文是笔者阅读Effective C++第三版时的读书笔记,主要内容包括:

  • 对书上内容的概括提取
  • 笔者自己不清楚的小知识点的提取
  • 对书中每个条款的后的“请记住”部分的(基本上是)原文摘录

这种灰色部分一般是笔者自己的一些注释

原书一共九章,本文(上篇)记录第一至三章的笔记

1. 让自己习惯C++

条款01:视C++为一个语言联邦
  • 将C++视为四种sublanguage,即:
    • C(没有模板、异常、重载)
    • Object-Oriented C++(面向对象、多态性)
    • Template C++ (泛型编程)
      • 由此衍生出模板元编程(template metaprogramming, TMP)
    • STL (容器、迭代器、算法、函数对象)
  • 在sublanguage间切换时,高效的编程守则也会改变,例如:
    • 对内置(C-like)类型,值传递比引用传递更加高效!
    • 迭代器和函数对象由C指针塑造,所以对它们也应该采用值传递

C++高效编程守则取决于你使用C++的哪一部分


条款02:尽量以const,enum,inline替换 #define
  • 宏的定义也许不会被编译器看见(预处理器处理),进而导致报错时不会带有宏的记号信息,导致难以调试;而常量则可以避免此种问题
  • 使用常量可能导致较小量的码(object code);而宏可能会导致多份盲目替换,导致目标码大一些
  • 往往const stringconst char* const要好一些

    这里还不知道是为啥

  • #define一般不能提供封装性,常量则不然

  • in-class初值设定只能对整型常量进行
  • 关于enum hack:

    • enum的地址不合法,但是取const的地址是合法的(有时这是一种约束)
    • enum绝不会导致内存分配,而某些不优秀的编译器也许会给const分配空间

      综上来说,enum的行为某方面更像#define

  • 使用template inline函数替代宏


对于单纯常量,最好以 const对象或 enum替换 #define

对于形似函数的宏,用 inline函数替换 #define


条款03:尽可能使用const
  • 迭代器就像一个T*指针
    • const iterator iter类似T* const t
    • const_iterator类似const T* (用于只读访问)
  • 函数返回值是const可以预防一些奇怪的错误(书P19上方)
  • 两个成员函数只有常量性不同可以被重载
  • 关于bitwise constness和logical constness(见书P21, 派别之争并不是重点,重点后者的思想是保证客户端侦测不出变化,与编译器只认bitwise之间的矛盾,从而导出下一条
  • 在声明变量前加上前缀mutable,const成员函数就可以更改这些变量
  • 在const和non-const之间避免重复:

    • 只应该让non-const函数调用const函数(反之则可能导致成员变量改动,使const函数失去const特性)
    • 具体写法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const char& operator[](std::size_t pos) const {
      ...
      return str[pos];
      }
      char& operator[](std::size_t pos) {
      return
      const_cast<char&>( // 转除op[]返回的const
      static_cast<const ClassName&>(*this)[pos] // 把*this变const以调用const函数
      );
      }

      注意到,这里把*this转成了const ClassName&,相当于进行了一个加const


将某些东西声明为 const可以帮助编译器侦测错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体

编译器强制实施 bitwise constness,但你应该使用 “概念上的常量性”

当 const和 non-const成员函数有等价实现时,令 non-const版本调用 const版本来避免代码重复


条款04:确定对象使用前已先被初始化
  • 确保每一个构造函数都将对象的每一个成员初始化

    • 分清初始化(initialization)和赋值(assignment)
    • 通常使用初始化列表,即直接调用拷贝构造,比先默认构造再赋值来得快
    • const和reference变量必须使用初始化列表

    总而言之,总是在初值列中列出所有成员变量

    • 过多的重复下,可以将那些“赋值表现和初始化一样好”的成员变量移到某个private函数里供构造函数调用
  • class的成员变量以其声明次序被初始化,和初始化列表顺序无关
  • 对于不同编译单元之间的non-local static对象(位于namespace或global作用域)的初始化次序没有明确规定
    • 合适的设计手法是,把non-local static对象变成函数里的static对象,然后让这个函数返回该对象的引用
      • 函数内的local static对象会在“该函数调用期间”“首次遇上该对象之定义式”时被初始化
    • 尽管如此,任何涉及non-const static对象涉及到多线程都会有不确定性,一般是在单线程启动阶段先把这样的reference-returning函数手动调用一遍

为内置型对象进行手工初始化,因为 C++不保证初始化它们

构造函数最好使用初值列代替赋值操作;初值列排列次序应该和声明次序相同

为了免除跨编译单元时初始化次序的问题,请用 local static对象替换 non-local static对象


2. 构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数
  • 编译器默认声明的拷贝构造、赋值、构造、析构函数都是public且inline的
    • 只有这些默认的函数需要被调用,它们才会被创建
    • 编译器产出的析构函数默认non-virtual
  • 有三种情况编译器会拒绝生成一个默认的copy assignment
    • 类中含有reference成员
    • 类中含const成员
    • 基类的copy assignment声明为private

编译器可以暗自为 class创建 default构造函数、copy构造函数、copy assignment操作符,以及析构函数


条款06:若不想使用编译器自动生成的函数,就该明确拒绝
  • 可以通过仅声明,不定义copy构造、copy assignment操作符,并且声明为private,来避免编译器的自动创建和不被希望的调用
    • 进一步,如果不希望friend和member函数调用,可以实现一个基类,并将它的copy构造、copy assignment操作符声明为private,然后继承它(甚至未必要是public继承)

为驳回编译器(暗自)提供的机能,可将相应的成员函数声明为 private并不予实现。或者采用 Uncopyable基类的做法。


条款07:为多态基类声明virtual析构函数
  • 任何class只有带有virtual函数都几乎确定应该也有一个virtual析构函数
  • 如果不作为基类,不应使用virtual, 因为包含了vptr(virtual table pointer),可能会导致:
    • 对象的体积可能会增大50%~100%!
    • 不再和其他语言(如C)中的相同声明拥有一样的结构,从而不能传递至其他语言所写函数
  • string,以及所有STL容器,都不带virtual析构函数!(因而不适合被继承)
  • 如果希望有一个抽象类,可以将类的析构函数声明为纯虚,但是仍然需要提供定义!(否则连接器会报错)

    这里比较奇怪,基类的析构该怎么写还是要怎么写,比如该 delete还是要 delete


带有多态性质的基类应该声明一个 virtual析构函数;如果类中有任何 virtual函数,它就应该有一个 virtual析构函数

类的设计目的如果不是为了作为基类,或者是基类但不是为了具有多态性,就不应该声明 virtual析构函数


条款08:别让异常逃离析构函数
  • 析构函数不应该吐出异常,理由有二:
    • 可能导致异常后面的资源释放不会执行
    • 异常可能会导致程序调用其他的析构函数来释放资源,这可能会造成新的异常(两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为)
  • 解决这种问题的方法:
    • 析构函数内有异常就abort()结束程序
    • 析构函数吞下异常(不一定是个好主意,但是可行!)
    • 重新设计一个普通函数接口来进行操作,从而给客户一个处理异常的空间,同时在析构函数里操作作为双保险

析构函数绝对不要吐出异常;析构函数应该捕捉任何异常,吞下它们或结束程序

如果客户需要对某个操作函数运行期间抛出的异常做出反应,类中应该提供一个普通函数进行该操作


条款09:绝不在构造和析构过程中调用virtual
  • 原因是在基类构造期间运行期类型信息(dynamic_cast和typeid之类)把对象视为base class类型,虚函数也只会被解析至base class(这样的好处是可以防止虚函数调用还没有初始化的派生类成员
  • 析构函数也同样
  • 如果想在构造对象时输出适当的对应版本的信息,可以通过给构造函数增加参数一直传给基类,并且在基类构造函数中使用non-virtual函数

在构造和析构期间不要调用 virtual函数,因为这类调用从不下降至当前执行层的 derived class


条款10:令 operator= 返回一个reference to *this
  • 为了实现连锁赋值,所以返回一个指向操作符左侧实参的引用(也包括+=之类的运算符)

令赋值操作符返回一个 reference to \this*


条款11:在 operator= 中处理“自我赋值”
  • 三种方法处理这种情况:

    • “证同测试”:

      1
      if (this == &rhs) return *this;
    • 保持异常安全性的同时处理了自我赋值

      1
      2
      3
      Bitmap* pOrig = pb;
      pb = new Bitmap(*rhs.pb);
      return *this;

      这段代码的好处是即使 new的时候出现异常,pb也会保持原状

    • 使用所谓copy and swap技术

      1
      2
      3
      Widget tmp(rhs);
      swap(tmp); // 将*this和tmp的数据交换
      return *this;

      而如果rhs是值传递的,还可以省略第一行

      将 copying动作从函数本体内移至 “函数参数 ”构造阶段有时可令编译器生成更高效的代码(也许指用值传递 rhs)


确保当对象自我赋值时 operator=有良好行为;其中技术包括比较地址,安排语句顺序,以及 copy-and-swap

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确


条款12:复制对象时勿忘其每一个成分
  • 对于基类部分的复制,往往在派生类的复制函数中手动调用基类的复制函数,如BaseClass::operator=(rhs)
  • 故编写copying函数时应该:
    • 复制所有local成员变量
    • 调用所有基类的适当copying函数
  • 让copy构造函数和copy assignment操作符之间互相调用是不合理的(如果要代码复用,应该建立一个新的函数给两者调用)

Copying函数应该确保复制 “对象内的所有成员变量 ”及 “所有 base class成分 ”

不要尝试以某个 copying函数实现另一个 copying函数;应该将共同机能放进第三个函数中供两者共同调用


3. 资源管理

条款13:以对象管理资源
  • 以对象管理资源的一般想法:
    • 获得资源后立刻放进管理对象
    • 管理对象运用析构函数确保资源被释放

为防止资源泄漏,请使用 RAII(Resource Acquisition Is Initialization,资源获取就是初始化)对象,它们在构造函数中获得资源,在析构函数中释放资源

两个常被使用的 RAII classes是 tr1::shared_ptrauto_ptr,前者通常是较佳选择,因为其 copy行为比较直观,后者的复制动作会使它指向 null


条款14:在资源管理类中小心copying行为
  • 对于RAII对象的复制,一般有几种态度(前两种情况较多):
    • 禁止复制(手法见条款06)
    • 使用引用计数法
      • 通常只要内含tr1::shared_ptr,便可以实现这种行为
      • tr1::shared_ptr允许指定一个“删除器”作为第二参数,即引用数归零时被调用的行为
    • 复制底部资源,即进行深拷贝
    • 转移底部资源的拥有权,即auto_ptr的行为

复制 RAII对象必须一并复制它所管理的资源,所以资源的 copying行为决定 RAII对象的 copying行为

普遍而常见的 RAII copying行为是:抑制 copying、施行引用计数法;不过也有其他可能


条款15:在资源管理类提供对原始资源的访问
  • 将RAII class对象转换为其内含的原始资源:
    • 显式转换
      • 智能指针的.get()方法
      • 自行在类内提供一个get()方法返回资源
    • 隐式转换
      • 智能指针对->*的重载
      • 在类内提供一个类型转换的函数(例如operator int()之类)

APIs往往要求访问原始资源,所以每一个 RAII class应该提供一个 “取得其所管理之资源 ”的办法

对原始资源的访问可能经由显式或隐式转换;一般而言显式比较安全,但隐式对客户比较方便


条款16:成对使用new和delete时要采取相同形式
  • 在运用typedef的时候要小心t,要注意new这种类型的时候该用什么delete(所以一般不对数组形式做typedef,而是直接使用vector之类的)

如果你在 new表达式中使用 [],必须在相应的 delete表达式中也使用 [];反之亦然


条款17:以独立语句将newed对象置入智能指针
  • 这样做是为了防止“资源被创建”和“资源被转换为资源管理对象之间”发生异常干扰(同语句内C++编译器对各项操作的调用顺序可能有一定的自由度,例如函数参数传递时调用的函数,详见书P76)

以独立语句将 newed对象置于智能指针内;如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏


受笔者知识水平和表达能力所限,有些问题上难免出现疏漏,欢迎在评论区指正