Effective_Cpp中的55个建议

本文将Effective C++中55条建议的关键内容进行了记录和总结。
注:本文适合用来复习,无法用来代替第一遍学习。
其中有几条还有待复习,在前面标注了?。
模板与泛型编程部分略过了几条,现在还读不太懂,相信有了更多的经验之后再来读第二遍,会有更多的收获。

改善程序的55个具体做法

让自己习惯C++

  • 1:视C++为一个语言联邦
    C++有四个次语言,分别是C、C with Classes、Template C++、STL。在不同的次语言之间切换时,某些高效编程的策略会改变。

  • 2:尽量以const,enum,inline替换#define

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    1.如果你想这么用
    #define AAA 666
    请替换为
    const int aaa=666
    2.#define CAL_MAX(a,b) (a>b?a:b) 改为
    template<typename T>
    inline T calMax(const T &a,const T &b){
    return a>b?a:b;
    }
    3.使用这种方式:
    class Game{
    private:
    enum{ Num=5 };
    int scores[Num];
    }
  • 3:尽可能使用const
    1.尽量把某些符合const特性的地方加上const,比如局部变量、全局变量、函数参数、函数返回类型,成员函数本体,这样可以让编译器帮你找出程序的错误。
    2.当有const和non-const两个成员函数时,可以让non-const版本来调用const版本实现,避免代码重复。p24

  • 4:确保对象使用前已被初始化。
    1.内置的int double等数据类型一定要手工初始化,因为C++不保证初始化它们。
    2.构造函数最好使用成员初始化列表,因为如果放在函数体内就成了赋值了。这样先初始化一遍,然后进行赋值,之前的初始化就白做了。(初始化列表的成员顺序一定要和成员的声明顺序相同。)
    3.当好几个文件中都有全局静态变量,并且他们互相调用时,这时每个静态变量的初始化顺序是不确定的,可能会发生错误,可以用以下方式来避免。

    1
    2
    3
    4
    5
    6
    7
    之前: static int test;
    改为:
    int& test(){
    static test;
    return test;
    }
    即用local static来代替non-local static

构造/析构/赋值运算

  • 5:了解C++默默编写并调用哪些函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    如果你写下:
    class Empty{};
    就好像你写下:
    class Empty{
    public:
    Empty(){...}
    Empty(const Empty& rhs){...}
    ~Empty(){...}

    Empty& operator=(const Empty& rhs){...}
    };
  • 6:若不想使用编译器自动生成的函数,就该明确拒绝。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    如果你自己写出那个函数,编译器就不会再自动生成了。
    看一个常规操作:
    class HomeForSale{...};
    ...
    HomeForSale h1,h2;
    h1=h2;
    但是如果你觉得每一个HomeForSale类都是独一无二的,不想让h1=h2这样的事情发生,那么你可以这样:
    class Uncopyable{
    protected:
    Uncopyable(){}
    ~Uncopyable(){}
    private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
    };
    然后让某个类来继承这个类就行。
    class HomeForSale: private Uncopyable{...};
  • 7:为多态基类声明virtual析构函数
    1.带有多态性质的基类应该声明一个虚析构函数。如果一个类里面有任何虚函数,那么它也应该有一个虚析构函数。
    2.如果一个类的设计目的不是为了多态,那么就不该声明virtual析构函数。比如string和STL的容器就不是被设计为基类,你也不能继承它们。

  • 8:别让异常逃离析构函数
    1.析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序。
    2.如果客户需要对类中某个函数抛出的异常做出反应,那么这个类应当给用户提供一个普通函数,在里面调用那个会抛出异常的函数。

  • 9:绝对不要在构造和析构过程中调用虚函数
    当构造子类的时候,需要先去调用父类的构造函数,这时候子类还不存在,是无法去自动调用子类的虚函数的。

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

    1
    2
    3
    4
    Widget& operator=(const Widget& rhs){
    ...
    return *this;
    }
  • 11:在operator=中处理自我赋值

    1
    2
    3
    4
    w=w;
    a[i]=a[j];
    *px=*py;
    这几种都会发生“自我赋值”。

如下代码,初看可能没什么问题:

1
2
3
4
5
6
7
8
9
10
11
12
class Bitmap{...};
class Widget{
...
private:
Bitmap* pb;
};
//一个不安全的operator=实现
Widget& Widget::operator=(const Widget& rhs){
delete pb;
pb=new Bitmap(*rhs.pb);
return *this;
}

但是如果发生自我赋值的时候,上面代码是不是就有问题了!可以改成下面这种方式:

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;
}

也可以这样:

1
2
3
4
Widget& Widget::operator=(Widget rhs){
swap(rhs);
return *this;
}

  • 12:复制对象时勿忘其每一个成分
    1.如果你自己写了一个拷贝构造函数或=运算符,那么如果你在里面落掉了某个成员,编译器是不会提醒你的。
    2.如果你没有落掉,那很好。但当你再为这个类添加一个成员时,不要忘了在自己写的拷贝函数里也加上这一成员。
    3.还有如果一个子类继承了你的这个类,那么这个子类在调用拷贝函数时,就不会自动调用父类中你写的拷贝函数了,需要你手动调用。
    4.不要尝试以某个拷贝函数实现另一个拷贝函数,应该将共同功能的代码放入第三个函数,然后在这两个拷贝函数中调用。

资源管理

  • 13:以对象管理资源
    1.为了防止忘记调用delete释放空间,可以借用智能指针:
    1
    2
    3
    4
    void f(){
    std::auto_ptr<Investment> pInv(createInvestment());
    ...
    }

但是在c++11之后,就已经弃用auto_ptr了,可以把auto_ptr改成shared_ptr
2.为防止资源泄露,请使用RAII(资源获取时机便是初始化时机)对象,他们在构造函数中获得资源并在析构函数中释放资源。
3.两个常被使用的RAII classes分别是shared_ptr和auto_ptr。前者通常是最佳选择,若选择auto_ptr,复制动作会使它指向null。

  • 14:?在资源管理类中小心copying行为
    1.复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
    2.普遍而常见的RAII类copying行为是:抑制copying、施行引用计数法。不过其他行为也都可能被实现。

  • 15:?在资源管理类中提供对原始资源的访问
    1.APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
    2.对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

  • 16:成对使用new和delete时要采用相同形式
    如果你用的new xxx[],那么就要用delete [] xxx。

  • 17:以独立语句将newed对象置入智能指针
    1.如果你想这么写:

    1
    processWidget(new Widget,priority());

那你最好改成这种:

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

那么priority抛出异常时,会防止内存泄漏。
2.以独立语句将newed对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以差距的资源泄漏。

设计与声明

  • 18:让接口容易被正确使用,不易被误用。
    1.好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
    2.促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。
    3.阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
    4.shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁。

  • 19:?设计class犹如设计type
    1.定义出高效的classes是一种挑战,可以从这些方面考虑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    新type的对象应该如何创建和销毁?
    对象的初始化和对象的赋值该有什么样的差别?
    新type的对象如果被passed by value,意味着什么?
    什么是新type的合法值?
    你的新type需要配合某个继承图系吗?
    你的新type需要什么样的转换?
    什么样的操作符和函数对此新type而言是合理的?
    什么样的标准函数应该驳回?
    谁该取用新type的成员?
    什么是新type的未声明接口?
    你的新type有多么一般化?
  • 20:最好用pass-by-reference-to-const替换pass-by-value
    1.引用传值,本质上也是传递一个指针,如果是传递内置类型,就不如采用直接传值了。(另外STL的迭代器和函数对象也是)
    2.用引用传值通常比较高效,并可避免切割问题。
    3.切割问题:当使用传值方式时,一个子类对象被传递,被当一个父类对象接收时,此时只能调用父类中拥有的操作,子类扩展的就被切割了。

  • 21:?必须返回对象时,别妄想返回其reference。
    注:(如果不是重载*运算符,是不是第三种代码就可以用了?这种方式和左值引用有什么区别?)
    1.以下几种都是糟糕的代码:

    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
    const Rational& operator*(const Rational& lhs,const Rational& rhs){
    Rational result(lhs.n*rhs.n,lhs.d*rhs.d);
    return result;
    }
    -----------------------
    const Rational& operator*(const Rational& lhs,const Rational& rhs){
    Rational * result=new Rational(lhs.n*rhs.n,lhs.d*rhs.d);
    return result;
    }
    //当这种情况时会发生资源泄露:
    Rational w,x,y,z;
    w=x*y*z;
    -----------------------
    const Rational& operator*(const Rational& lhs,const Rational& rhs){
    static Rational result;
    result = ...;
    return result;
    }
    //以下情况时会出错:
    Rational a,b,c,d;
    ...
    if((a*b)==(c*d)){
    ...
    }
    //此时永远都是true;
    -----------------------

2.绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

  • 22:将成员变量声明为private
    1.如果是public:如果用户能直接访问成员变量,那么以后你就无法更改这个变量了,因为一旦你改了,可能就需要再去更改大量的用户代码!
    2.如果仅声明为protected也不行:一旦你以后更改了这个变量,那么他的子类就会遭到破坏,因为它的子类很可能已经对这个变量进行了某些操作!
    3.封装的重要性远比你想象的要重要。
    4.切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
    5.protected不比public更具封装性。

  • 23:?宁以non-member、non-friend替换member函数。
    1.这样做可以增加封装性、包裹弹性和机能扩充性。

  • 24:?若所有参数皆需类型转换,请为此采用non-member函数。

  • 25:?考虑写出一个不抛异常的swap函数
    1.特化std::swap是什么意思?
    2.不要尝试在std内加入某些对std而言全新的东西。

实现

  • 26:尽可能延后变量定义式的出现时间。
    1.比如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    string encryptPassword(const std::string &password){
    string encry;
    fun();
    return encry;
    }
    //如果再运行fun()的时候抛出异常,encry就白定义了,所以改成下面这样会好一些:
    string encryptPassword(const std::string &password){
    fun();
    string encry;
    return encry;
    }

2.另外:

1
2
3
4
5
6
string encryptPassword(const std::string &password){
string encry;
encry==password;
...
return encry;
}

不如改成:

1
2
3
4
string encryptPassword(const std::string &password){
string encry(password);
return encry;
}

3.尽可能延后变量定义式的出现。这样做可增加程序清晰度并改善程序效率。

  • 27:尽量少做转型动作
    1.如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast,试着改成无需转型的设计。
    2.如果转型是必要的,试着将它藏在某个函数背后,客户再调用这个函数即可。
    3.宁可使用C++新式转型,不要使用旧式转型,前者很容易辨识出来,并且有着分门别类转型方式。

  • 28:避免返回handles指向对象内部成分。
    1.如果你用public成员函数返回一个private成员,那么这个私有成员的权限也就谈不上私有了。
    2.避免返回handles指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生”虚吊号码牌”的可能性降到最低。

  • 29:为异常安全而努力是值得的
    1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
    2.“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
    3.函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

  • 30:透彻了解inlining的里里外外
    1.inline只是对编译器的一个申请,不是强制命令(如果函数复杂了,编译器就会把它当普通函数对待)。这项申请可以隐喻提出,也可以明确提出。如下就是隐喻提出:

    1
    2
    3
    4
    5
    6
    7
    8
    class Person{
    public:
    ...
    int age() const { return theAge; }
    ...
    private:
    int theAge;
    }

2.如果你让一个函数指针指向内联函数,那么通过函数指针来调用函数时,此时不是通过内联的方式调用。
3.内联函数也是有代价的,它会把所有用到这个函数的地方都插入这些代码,那么当你需要更改这个函数的时候,就需要重新编译所有客户端,而不是只重新链接就好。
4.将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
5.不要只因为function template出现在头文件,就将他们声明为inline。

  • 31:将文件间的编译依存关系降到最低。
    1.如果一个头文件被改变,那么所有依赖这个头文件的类都需要重新编译。
    2.尽量以class声明式替换class定义式。
    3.程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。

继承与面向对象设计

  • 32:确定你的public继承塑模出is-a关系。
    1.public继承意味着is-a。适用于base classes身上的每一件事情也一定适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

  • 33:避免遮掩继承而来的名称
    1.只要名称相同,子类的成员函数就会把基类中所有同名函数覆盖掉。
    2.为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //using声明式
    class Derived: public Base{
    public:
    using Base::mf1;
    using Base::mf3;
    virtual void mf1();
    void mf3();
    ...
    }

    //转交函数
    class Derived:private Base{
    public:
    virtual void mf1(){
    Base::mf1();
    }
    ...
    }
  • 34:?区分接口继承和实现继承
    1.声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
    2.实际上任何class如果打算被用来当做一个基类,都会拥有若干虚函数。
    3.接口继承和实现继承不同。在public继承之下,derived classes总是继承基类的接口。
    4.pure virtual只具体指定接口继承。
    5.简朴的非纯虚函数具体制定接口继承及缺省实现继承。
    6.非虚函数具体指定接口继承以及强制性实现继承。

  • 35:?考虑virtual函数以外的其他选择。

  • 36:绝不重新定义继承而来的非虚函数。

  • 37:绝不重新定义继承而来的缺省参数值。
    1.缺省参数是静态绑定的,你如果重新定义了,你在调用该函数时会动态调用子类的,但是他的默认参数却是基类的。

  • 38:通过复合塑模出has-a或根据某物实现出
    1.复合的意义和public继承完全不同。
    2.?在应用域,复合意味着has-a。在实现域,复合意味着is-implemented-in-terms-of(根据某物实现出)

  • 39:明智而审慎地使用private继承

  • 40:明智而审慎地使用多重继承
    1.非必要不要使用虚基类,平时请使用非虚继承。如果你必须使用虚基类,尽可能避免在其中饭放置数据。
    2.多重继承比单一继承复杂。他可能导致新的歧义性,以及对虚继承的需要。
    3.虚继承会增加大小、速度、初始化(及赋值)复杂度等成本。如果虚基类不带任何数据,将是最具实用价值的情况。
    4.多重继承的确有正当用途。其中一个情节涉及public继承某个接口类和私有继承某个协助实现的class的两相结合。

模板与泛型编程

  • 41:了解隐式接口和编译器多态
    1.classes和templates都支持接口和多台。
    2.对classes而言接口是显式的,以函数签名为中心。多台则是通过virtual函数发生于运行期。
    3.对template参数而言,接口是隐式的,奠基于有效表达式。多台则是通过template具现化和函数重载解析发生于编译器。

  • 42:了解typename的双重意义
    1.c++并不总是把class和typename视为等价。有时候你一定得用typename。
    2.(如果某个名称依赖于某个template参数,称之为从属名称)你使用这个从属名称时前面要加上typename

    1
    2
    3
    4
    5
    6
    7
    template<typename C>
    void print2nd(const C& container){
    if(container.size()>=2){
    //C::const_iterator iter(container.begin());//错误,const_iterator可能会被当做某个全局变量。
    typename C::const_iterator iter(container.begin());//你使用这个从属名称时前面要加上typenameme
    }
    }

3.在使用typename标示从属类型名称时,不能在继承类和成员初值列内用。
4.平时在声明template参数时,class和typename可以互换。

  • 43-47未读

定制new和delete

  • 48:了解new-handler的行为。
    1.set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
    2.Nothrow new是一个颇为局限的工具,它只适用于内存分配,后继的构造函数调用函数可能抛出异常。

  • 49-52未读

杂项讨论

  • 53:不要轻忽编译器的警告
    1.严肃对待编译器发出的警告信息。努力在你的编译器的最高警告级别下争取无任何警告。
    2.不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。

  • 54:让自己熟悉包括TR1在内的标准程序库

  • 55:让自己熟悉boost。
    1.boost是一个标准程序库,相当于STL的延续和扩充,里面包含了各种工具类,一些新的特性会被先加在这里面。