Effective C++读书笔记(一)

来源:互联网 发布:js格式化html代码 编辑:程序博客网 时间:2024/05/23 00:10

    百度博客本来就垃圾,我以前发表的文章也全是废品。就在csdn这里放上我的读书笔记,自娱自乐下。

1 让自己习惯C++

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

    C语言同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)。

    主要分为4个次语言:C、Object-Oriented C++、Template C++、STL。

    高效守则:对内置类型而言 pass-by-value通常比pass-by-reference高效;但Object-Oriented C++由于构造函数和析构函数的存在,pass-by-reference更高效;Template C++更如此,你甚至不知道要处理的类型。但STL的迭代器和函数对象都是在C指针基础上塑造出来的,pass-by-value再次适用。

  • 请记住:C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

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

    “宁可以编译器替换预处理器”。当你做这样的事情:

#define MAXN 1.653。因为MAXN没进入记号表内,报错时(特别宏定义不在本文件内)只会提1.653而不是MAXN,于是你将因为追踪它而浪费时间。

    如果要在头文件内定义常字符串:const char* const authorName = "Scott Meyers";或者const std::string authorName = "Scott Meyers";

    第二个值得注意的是class专属常量。static const int num = 5;但这是声明式,必须类外写定义式:

const int GamePlayer::num; 因为常量在声明式获得初值,定义时不能赋值。或者你可以在定义时赋值,声明时不赋值(不能都赋值)

    最好的方法为 enum {num = 5}; 同宏一样不能引用,不占空间。

    #define MAX(a,b) (a)>(b)?(a):(b)这样非常麻烦但还是有错误,比如MAX(++a,b)。其实换成Template就行了。

  • 请记住:对于单纯的常量,最好以const对象或enums替换#defines
  • 对于形似函数的宏,最好改用inline函数替换。

条款:03:尽可能使用const

    1>指针、STL、函数

    如果const出现在星号左边,被指物是常量,如果出现在星号右边,指针本身是常量。

    如果被指物是常量,有的喜欢 const int *p; 有的喜欢 int const *p;两种写法都一样

    STL迭代器以指针为根据塑模出来,vec.const_iterator;

    如果令函数返回一个const值,可以避免 if(a * b = c)这种。

    2>const成员函数

    const成员函数实际修饰的是this指针,他使调用const对象成为可能。

    成员函数可以因为const性质不同而重载(同普通函数不一样),这导致一个非常复杂的问题看书上吧(╯﹏╰)

  • 请记住:将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

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

    内置类型初始化与作用域的关系,复杂类型的数组形式。

    构造函数中成员初始化列表不定义顺序,基类先于派生类,而本类的变量也只跟声明的顺序有关(不跟列表顺序、不跟传入参数顺序有关)

    const、引用必须写在成员初始化列表内。

    由于多个类的编译顺序未定义,为了确保一个类调用另一个类的对象是已初始化。

  • 请记住:为内置型对象进行手工初始化,因为C++不保证初始化它们
  • 构造函数最好使用成员初值列表,而不要在构造函数本体内使用复制操作。初值列表列出的成员标量,其排列次序应该和它们在class中的声明次序一样。
  • 为避免“跨编译单元的初始化次序”问题,请以local static对象替换 non-static对象。

2 构造、析构、赋值运算

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

什么时候empty class不再是一个空类?当C++处理过它之后。如果你没声明,编译器就会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。不管怎样编译器也会声明一个析构函数。所有这些函数都是public且inline。

唯有当这些函数被需要(被调用),它们才会被编译器创建出来。

  1. 如果你打算在一个“内涵reference成员”的class内支持assignment操作,你必须自己定义copy assignment操作符。
  2. 面对“内含const成员”的class编译器也是拒绝编译那一行赋值动作。
  3. 如果某个base class将copy assignment操作符声明为private,编译器也将拒绝为其derived class生成一个copy assignment操作符。因为derived class无权调用base class的函数。

  • 请记住:编译器可以案子为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

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

答案的关键是,所有编译器产出的函数都是public,为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为public。因此你可以将copy构造函数或copy assignment操作符声明为private,阻止编译器暗自创建。

但是member函数和friend函数还是可以调用你的private函数。除非你够聪明,不去定义它们,那么如果某些人不慎调用任何一个,会获得一个连接错误(linkage error)。“将成员函数声明为private而且故意不实现它们”这一伎俩是如此为大家接受。

有了上述定义,当客户企图拷贝HomeForSale对象,编译器会阻挠他。如果你不慎在member函数或friend函数之内那么做,轮到连接器发出抱怨。

将连接期错误移至编译期是可能的(而且是好事),只要将copy构造函数和copy assignment操作符声明为private就可以办到,但不是在HomeForSale自身,而是在一个专门为了阻止copy动作而设计的base class内。

这行的通,因为只要任何人-甚至是member函数或friend函数-尝试拷贝HomeForSale对象,编译器变试着生成一个copy构造函数和一个copy assignment操作符。而这些编译器生成的函数会尝试调用其base class的对应兄弟,那些调用会被编译器拒绝。

U你copyable class的实现和运用颇为微妙。包括不一定public继承,Uncopyable的析构不一定得是virtual,Uncopyable不含数据等。但由于它总是扮演base class,可能导致多重继承,而多重继承有时会阻止empty base class optimization。但通常可以忽略这些。

  • 请记住:为驳回编译器自动提供的机制,可将相应的成员函数声明为private且不予实现。使用像Uncopyable这样的base class也是一种做法。

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

当返回的指针指向一个derived class对象,而对象却经由一个base class指针被删除,而目前的base class有个non-virtual析构函数。导致base class成分被销毁,而derived 成分很可能没被销毁,而derived class的析构函数也未能执行起来。

消除这个问题的做法很简单:给base class一个virtual析构函数。此后就会销毁整个对象包括所有的派生成分。任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。

而如果class不含virtual函数,令其析构函数为virtual往往是个馊主意。欲实现virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是有一个所谓vptr指针指出。vptr指向一个由函数指针构成的数组。导致对象的体积增加。而且别的语言没有vptr导致失去了移植性。

  • 请记住:polymorphic(带多态性质的)base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • class的设计目的如果不是左右base class使用,或不是为了具备多态性质,就不该声明virtual函数。

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

当销毁vector时,它有责任销毁其含有的所有元素。假设vector内含有是个元素,而在析构第一个元素期间有个异常被抛出。其他九个元素还是应该被销毁(否则它们保存的任何资源都会发生泄漏)。这时后面的析构函数又抛出异常。这对C++而言太多了,在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。

  • 请记住:析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

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

当我们定义一个derived class对象时,首先base class的构造函数一定会更早被调用。但这时如果调用了一个virtuan函数,该函数是一个base class版本的函数。是的,base class构造期间virtual函数绝不会下降到derived class阶层,取而代之的是base版本。在base class构造期间,virtual函数不是virtual函数。

因为base class构造函数执行时derived class的成员变量尚未初始化。所以C++不允许访问derived版本。更根本的原因,在derived class对象的base class构造期间,对象的类型是base class而不是derived class。

同样的道理也存在析构函数中,对象的类型也是跟着改变而不会下沉到derived class。

可以令derived class将必要的构造信息向上传递至base class构造函数。

  • 请记住:在构造和析构期间不要调用virtuan函数,因为这类调用从不下降至derived class。

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

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。

  • 请记住:令赋值操作符返回一个reference to *this

条款11:在operator=中处理“自我赋值”

主要问题是复制之前先删除了对象。可以通过证同判断、copy and swap技术。但经常良好的顺序就可以避免。

  • 请记住:确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确保任何函数如果操作一个以上的对象,而其中多个对象时同一个对象时,其行为任然正确。

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

如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为。当你的实现代码几乎必然出错时却不告诉你。

当你编写一个copying函数,请确保(1)复制所有local成员变量,(2)调用所有base class内的适当的copying函数。

这两个copying函数往往有近似相同的实现本体,但是令某个copying函数调用另一个copying函数却无法让你达到你想要的目标。copy构造函数调用copy assignment操作符,操作符调用构造函数也没用。一个好的方法是设个小函数。

  • 请记住:copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同技能放进第三个函数中,并由两个copying函数共同调用。

3 资源管理

条款13:以对象管理资源

void f(){    node *p = create();    //调用factory函数    ...    delete p;    //释放p所指对象}

1.这看起来妥当,但或许因为"..."区域内的一个过早return等语句,控制流就绝不会触及delete语句。为确保create返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。标准程序库提供的auto_ptr正是针对这种形式而设计的特别产品。auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。

std::auto_ptr<node> p( creat() );

这个简单的例子示范“以对象管理资源”的两个关键想法:获得资源后立刻放进管理对象内;管理对象运用析构函数确保资源被释放。

由于auto-ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。为了预防这个问题,auto_ptr有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一所有权。举个例子,stl容器要求其元素发挥“正常的”复制行为,因此这些容器容不得auto_ptr。

2.auto_ptr的替代方案是“引用计数性智慧指针”所谓的RCSP提供的行为类似垃圾回收,不同的是RCSPs无法打破环装引用。 std::trl::shared_ptr<node> p( creat() );

尽管如此,auto_ptr和trl::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。

请记住:

  • 为防止资源泄漏,请使用trl对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的raII class分别是 trl::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,赋值动作会使他指向null。

条款14:在资源管理类中小心copying行为

资源在构造期间获得,在析构期间释放。描述了auto_ptr和trl::shared_ptr如何将这个观念表现在heap-based资源上。然后并非所有资源都是heap-based。既然如此,有可能偶尔你会发现,你需要建立自己的资源管理类。

例如,假设我们处理类型为Mutex的互斥器对象(mutex objects),共有lock和unlock两函数可用:

void lock(Mutex *pm); //锁定pm所指的互斥器

void unlock(Mutex *pm); //将互斥器解除锁定

为确保绝不会将一个被锁定的Mutex解锁,你可能会建立一个class来管理,在构造时锁定,析构时解锁。这很好,但如果Lock对象呗复制,会发生什么事?

Lock m11(&m);      //锁定m

Lock m12(m11);    //将m11复制到m12身上。这会发生什么事?

  • 禁止复制。通常不合适。条款6告诉你怎么做: class Lock:private Uncopyable{ }//禁止复制
  • 对底层资源祭出“引用计数法”。幸运的是trl::shared_ptr允许制定所谓的“删除器”,那是一个函数或者函数对象,当引用次数为0时便被调用。删除器对trl::shared_ptr构造函数而言是可有可无的第二参数,所以代码看起来像这样:
    class Lock{public:    explicit Lock(Mutex *pm):mutexPtr(pm, unlock){    //以unlock作为删除器    lock(mutexPtr.get());}private:    std::trl::shared_ptr<Mutex> mutexPtr;    //使用shared_ptr替换 pointer
  • 复制底部资源。某些标准字符串类型“指向heap内存”之指针构成(那块内存被用来存放字符串的组成字符)。这种字符串对象内涵一个指针指向一块heap内存。当这样一个字符串对象被复制,不论指针或其所指内存都会被制作出一个附件。这样的字符串展现深度复制行为。
  • 转移底部资源的拥有权。资源的拥有权从被复制物转移到目标物。这时auto_ptr奉行的复制意义。

请记住

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII class copying行为时:抑制copying、实行引用计数法。不过其他行为也都可能被实现。

条款15:在资源管理类中提供对原始资源的访问

请记住:

  • APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
  • 对原始资源的访问可能经由显示转换(定义个get函数)或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

条款16:成对使用new和delete时要采取相同形式

即将被删除的那个指针,所指的是单一对象或对象数组?这是个必不可缺的问题,因为单一对象的内存布局一般而言不同于数组的内存布局。更明确的说,数组所用的内存通常还包括“数组大小”的记录,以便delete知道需要调用多少次析构函数。
唯一能够让delete知道内存中是否存在一个“数组大小记录”的办法就是:有你来告诉它。
如果对new使用 delete[],结果未定义。delete会读取若干内存把它解释成“数组大小”,然后开始多次调用析构函数。如果对 new[]使用delete也是未定义。会在“数组大小”处开始调用析构函数。
这个规则对于喜欢用typedef的人也很重要,因为它意味typedef的作者必须说清楚,当程序员以new创建该种typedef类型对象时,该以哪一种delete形式删除之。考虑下面这个typedef:
typedef string AddressLines[4];//每个人的地址有4行。
请记住:
  • 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

条款17:以独立语句将newed对象置入智能指针

请记住:

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

4 设计与声明















原创粉丝点击