Effective C++ 笔记 part 1、2

  • Effective C++
  1. 视 C++ 为一个语言联邦
    1. C
    2. object-oriented C++面向对象:封装、继承、多态
    3. Template C++泛型编程:包括TMP(Template Metaprogramming 模板元编程)
    4. STL:是一个Template程序库,包括容器、迭代器、算法、函数对象
  2. 宁以编译器替换预处理器

尽量以 `const`、`enum`、`inline` 替换 `#define`

    1. 对於单纯常量,最好以const对象或enums替换#defines(宏定义出错时,编译器会给出替换之后的错误信息,避免因追踪变量而浪费时间)
    2. 对于形似函数的宏,最好改用inline函数替换#defines(#define单纯文本替换,函数语义不清晰,使用时多加小括号)
  1. 尽可能使用const
  1. const 写在T之前和之后无所谓,写在*之前表示指向常量的指针,写在*之后表示指针常量。STL的迭代器相当于一个T*指针,const_iterator相当于 const T*
  2. 将某些东西声明为const可以帮助编译器侦测出错误用法

使用const成员函数,以pass by reference-to-const方式传递对象,提升效率

  1. 两个成员函数如果只是常量性不同,可以被重载。调用时区分const.
  2. const成员函数不能调用non-const成员函数,当non-const和const成员函数有着实质等价的实现时,在non-const中调用const版本可以复用代码
  3. bitwise constness与logical constness:

bitwise constness:(C++编译器认为的cosntness)。const成员函数不改变对象的任意一个bit。bitwise constness允许发生此现象:当某个指针隶属于对象,而指针所指物不隶属于对象时,成员函数更改了指针所指物。

logical constness:一个const成员函数可以修改它所处理的对象的某些bits。

达到logical constness:可以通过mutable关键字实现mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。mutable释放掉non-static成员变量的bitwise constness约束

  1. 确定对象被使用前已经初始化
  1. array(来自c part of c++)不保证其内容被初始化,vector(来自stl part of c++)有此保证
  2. 确保每一个构造函数都将每一个成员初始化
    1. 对象的成员变量的初始化动作发生在这些成员的default构造函数被自动调用之时,即进入构造函数本体之前。在构造函数内为成员变量赋值(xxx=y)的动作是赋值而不是初始化(相当于首先调default构造函数为成员变量设初值,然后立刻为它们赋予新值)
    2. 使用成员初值列(member initialization list)可以避免上述现象,初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参,进行copy构造。对大多数类型而言,只进行一次copy构造比上述先初始化再用拷贝赋值运算符高效许多。对内置类型而言,二者成本相同,但为了一致性,最好也采用initialization list的方式进行初始化
    3. 总是在初值列中列出所有成员变量,以免还得记住哪些成员变量可以无需初值。(如国内值类型成员在成员初值列遗漏了它,就没有初值,引发不明确行为)
    4. const或references成员变量,一定需要初值,不能赋值
    5. 为内置型对象进行手工初始化,因为c++编译器不保证初始化它们。
  3. 成员初始化次序:

base classes更早于derived classes被初始化

class成员变量按照其声明次序被初始化,即使它们在成员初值列中以不同的次序出现。

  1. local static对象与non-local static对象:

static对象不包括stack和heap-based对象。包括global对象,定义于namespace作用域内的对象,在classed内、在函数内、在file作用域内被声明为static的对象。

local static对象:函数内的static对象

non-local static对象:其他static对象

  1. 编译单元:产出单一目标文件(.o)的源码。基本上是单一源码文件加上其所含入的头文件。

问题:c++对“定义域不同编译单元内的non-local static对象”的初始化次序无明确定义。如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。

解决:用local static对象替换non-loacl static对象:将每个non-local static对象搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不是直接指涉这些对象。

  1. 了解c++编译器默默编写并调用哪些函数
  1. 如果自己没有声明,编译器就会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。如果自己没有声明任何构造函数,编译器会为它声明一个default构造函数(如果自己有声明一个有参构造函数,则编译器不会再产出一个无参构造函数)。唯有当这些函数被调用时,它们才会被编译器创建出来。
    1. 编译器产出的析构函数是non-virtual的,除非该class的base class自身生命有virtual析构函数,此时这个函数的虚属性主要来自与其base class。
    2. 编译器产出的copy构造函数和copy assignment操作符只是单纯地将来源对象的每一个 non-static成员变量拷贝到目标对象

问题:c++并不允许将reference改指不同对象。如果打算在一个”内含reference成员”或“内含const成员”的class内支持赋值操作,必须自己定义copy assignment操作符

    1. 如果某个base classses将copy assignment操作符声明为private,编译器将拒绝为其derived classed生成一个copy assignment操作符(因为编译器为derived classes所生的copy assignment操作符想象中可以处理base class成分,但它们却无法调用base class 的assignment成员函数)

6.若不想使用编译器自动生成的函数,就要明确拒绝

  1. 所有编译器产出的函数都是public类型的,为阻止这些函数被创建出来,可将相应的成员函数声明为private并且不予实现。或在base class中声明private的函数然后在derived class中private继承。
  1. 为多态基类声明virtual析构函数
  1. 只有当class内至少一个virtual函数时,为它声明一个virtual析构函数
    1. 任何class只要带有virtual函数都应该有一个virtual的析构函数(derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的是对象的derived成分没被销毁)
    2. 如果class无virtual函数(通常表示不意图作为一个基类),则不用virtual的析构函数(vptr指针会使对象体积增加,同类型(例如c++中 Point类,c中自定义的Point类型)不能再传递至其他语言所写的函数,除非明确补偿vptr)
  2. virtual函数的实现:

为实现virtual函数,对象必须携带一些信息(主要用于在运行期决定哪一个virtual函数该被调用),这份信息有vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组(vtbl virtual table),每个带有virtual函数的class都有一个vtbl。当对象调用某一virtual函数,编译器在该对象的vptr所指的那个vtbl中寻找适当的函数指针。

  1. 析构函数运作顺序:最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个baseclass的析构函数被调用
  2. STL容器的析构函数均是non-virtual,所以不能继承包括STL容器在内的任一个带有non-virtual析构函数的class
  3. pure virtual 析构函数:当希望拥有一个抽象class,但手上没有合适的pure virtual函数是,可以为该class声明一个pure virtual析构函数。因为该class有一个pure virtual函数,所以它是抽象class,又由于它有个virtual析构函数,所以不需担心析构函数的问题。

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

  1. 析构函数吐出异常,程序可能过早结束或引发不明确行为。(假设对象含有多个元素,在析构第一个元素时发生异常,但其他9个元素也应该被销毁(否则会内存泄露),在析构第二个元素时又发生了异常,两个同时作用的异常导致不明确行为)
  2. 当析构函数中调用可能引发异常的函数时,析构函数可以:
    1. 如果该函数抛出异常就结束程序,通常通过调用abort函数完成

try(可能引发异常得函数)

catch(…){

        // 记录信息之类的操作

        std::abort();

}

  1. 吞下因函数调用失败引发的异常  // 不推荐

在catch(…){//不调用abort}

  1. 重新设计可能抛出异常的函数,使调用者有机会对可能出现的问题做出反应。

如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

9.绝不在构造和析构函数中调用virtual函数

  1. base class构造期间virtual函数绝不会下降到derived class阶层(在derived class对象的base class构造(析构)期间,对象的类型是base class而不是derived class)
  2. 一旦derived class析构函数开始执行,对象内的dericed class成员变量便呈现未定义值
  3. 如果class有多个构造函数,每个都要执行某些相同的工作,可以将共同的初始化代码放进一个初始化函数中
  4. 当希望在base class中构造时需要调用适当版本的函数时,可以在base class中调用non-virtual函数,然后要求derived class的构造函数传递必要信息给base class构造函数

10.令operator= 返回一个reference to *this

包括+=、-=、*=等操作符也是一样,返回一个reference to *this 可以实现连续赋值

       类名& operator=(const 类名& rhs){

       return *this; //

}

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

       先进行证同测试 if(this == rhs) return *this;

  1. 类名& operator=(const 类名& rhs){

       // 先记住原本的成员指针变量   //这样在new失败的情况下不会删除原本的内存

       // 令原先的指针变量 = new T( );

       //  delete 记住的                

       // return *this;

}

  1. copy and swap

类名& operator=(const 类名& rhs){

       类名 temp(rhs);

       swap(temp);

       return *this;

}

12.复制对象时勿忘其每一个成分

  1. copying函数应该确保复制”对象内的所有成员变量”及“所有base class成分”(调用base class copying函数)

不要尝试以某个copying函数实现另一个copying函数(即不能用operator=调用拷贝构造函数或者相反),应该将共同机能放进第三个函数中并由两个copying函数共同调用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章