学习《Effective C++》

学习《Effective C++》

#@date:         2014-06-16
#@author:       gerui
#@email:        [email protected]

  前几天买了好几本书,其中有一本是《Effective C++》,准备好好学习一下C++.书中提出了55条应该遵循的条款,下面将逐一学习。点击查看Evernote原文

一、让自己习惯C++

1. 视C++为一个语言联邦

  将C++分为4个次语言。即:C, Objective-Oriented C++, Template C++, STL.

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

  宁可以编译器替换预处理器:
  1) 预处理器`#define N 1.653' 将所有出现N的地方替换为1.653,当出现错误报的是1.653导致目标有问题,而不是N。如果使用变量,则可轻易地判断。此外,替换会造成代码在多处出现,增加代码量。所以尽量定义为常量,const double N = 1.653;
  2) 如果在数组初始化的时候,编译器需要知道数组的大小,这样,不可以使用变量进行数组初始化,这时#define可以,但我们最好使用enum{N=3;}来替代define.
  3) 使用#define定义一个三目运算符也会产生问题,如果你想获得高效,建议使用inline内联函数。
  但#include,以及#ifdef/#ifndef都是必需的,但我们要尽量限制预处理器的使用。

3. 尽可能使用 const

  1) const 表示不可以改变,如果修饰变量,则表示这个变量不可变,如(a);如果修饰指针,表示指针指向的位置不可改变,如(b)。

const char * p = "hello"; //(a) *p的hello不可变, 与char const * p = "hello"等价
char * const p = "hello"; //(b) 表示p的值不可变,即p不能指向其它位置

  2) STL迭代器的const

std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //类似T* const
*iter = 10;  //没问题,改变iter所指物
++iter;      //错误!iter是const
std::vector<int>const_iterator cIter = vec.begin();  //类似const T*
*iter = 10;  //错误,*iter是const
iter++;      //没问题,可以改变iter

  3) 使函数返回一个常量值,可以避免意外错误。如下代码,错把==写成=,一般程序对*号之后进行赋值会报错,但在自定义操作符面前不会(因为自定义*号后返回的是Rational对象实例的引用,可以拿来赋值,不会报错)。如果*不写成const,则下面的程序完全可以通过,但写成const之后,再对const进行赋值就出现问题了。

class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
Rational a, b, c;
if(a * b = c);  //把==错写成=,比较变成了赋值

  4) 函数的参数,如果无需改变其值,尽量使用const,这样可以避免函数中错误地将==等于符号误写为=赋值符号,而无法察觉。

  5) const作用于成员函数,两个作用,a)可以知道哪些函数可以改变成员变量,哪些函数不可以;b)改善C++效率,通过reference_to_const(即const对象的引用)方式传递对象。下面是常量函数与非常量函数的形式:

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[]
 */
TextBlock tb("hello");            //non-const 对象
cout<<tb[0]<<endl;    //调用的是non-const TextBlock::operator[]
tb[0] = 'x';          //没问题,写一个non-const对象

const TextBlock cTb("hello");    //const 对象
cout<<cTb[0]<<endl;   //调用的是const TextBlock:operator[]
cTb[0] = 'x';         //错误,写一个const对象

  6) bitwise const主张const成员函数不可以改变对象内任何non-static成员变量;logical const主张成员函数可以修改它所处理的对象内的某些bits,但要在客户端侦测不出的情况下才得如此。编译器默认执行bitwise。如果想要在const函数中修改non-static变量,需将变量声明为mutable(可变的)

class TextBlock{
    private:
        char* pText;
        mutable std::size_t textLength;
        mutable bool lengthIsValid;
    public:
        ...
        std::size_t length() const; 
};

std::size_t TextBlock::length() const{
    if (!lengthIsValid){
        textLength = std::strlen(pText);  //加上mutable修饰后,便可以修改其值
        lengthIsValid = true;
    }
}

  7) 避免const和non-const成员函数重复

  思想很简单,如果const和non-const成员函数功能相当时,就用non-const函数去调用const函数(不能反过来…o_O)。

class TextBlock{
    public:
        const char& operator[](std:size_t position) constP
            ...
            return text[position];
        }

        char& operator[] (std:size_t position){
            return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
        } 
};

4. 确定对象使用前先被初始化

  1) 对内置类型(基本类型)手动进行初始化。

int x = 0;
const char* p = "Hello World";
double d;
std:cin >> d;

  2) 内置类型以外的类型,初始化要靠构造函数。类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操作。初值列成员变量的排列顺序与其声明顺序相同。

class PhoneNumber { ... };
class ABEntry {
    public:
        ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones);
    private:
        std::string theName;
        std::string theAddress;
        std::list<PhoneNumber> thePhones;
        int numberTimesConsulted;
};

ABEntry::ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones){
    theName = name;               //这些都是赋值,而非初始化
    theAddress = address;
    thePhones = phones;
    numberTimesConsulted = 0;
}

/**
 *使用成员初值列,效率更高
 */
ABEntry::ABEntry(const std:string& name, const std::string& address, const std:list<PhoneNumber>& phones)
    :theName(name), theAddress(address), thePones(phones), numberTimesConsulted(0)    //成员初值列
{
    ...
} 

  3) 为避免"跨编译单元之初始化次序"问题,以local static对象替换non-local static对象。

//FileSystem源文件
class FileSystem{
    public:
        ...
        std::size_t numDisks() const;
};

extern FileSystem tfs;

//Directory源文件,与FileSystem处于不同的编译单元
class Directory{
    public:
        Directory(params);
        ...
};
Directory::Directory(params){
    ...
    //调用未初始化的tfs会出现错误
    std::size_t disks = tfs.numDisks();
}

  这样的话,Directory类会调用一个non-local的tfs,而这个tfs未必经历了初始化处理。我们要有效避免这个情况,使获取的tfs对象保证是初始的,可以使用如下的一个函数获取,这就像Singleton(单例)模式一样。

class FileSystem { ... };
FileSystem& tfs(){
    static FileSystem fs;
    return fs;
}

class Directory { ... };
Directory::Directory(params){
    std::size_t disks = tfs().numberDisks();
}

Directory& tempDir(){
    static Directory td;
    return td;
}

  经过上面的处理,将non-local转换了local对象,这样做的原理是:函数内的local static 对象会在"该函数被调用期间”,“首次遇上该对象之定义式"时被初始化,这样就保证了对象被初始化。这样做的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。

二、构造/析构/赋值运算

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

  1) 编译器会自动为class创建default构造函数、copy构造函数、copy assignment操作符、以及析构函数。

  2) 如果用户声明了一个构造函数,则编译器则不会再为它声明default构造函数。

  3) 拷贝构造函数可以通过=()实现。默认的拷贝构造函数对指针进行地址的复制,这样会产生多个对象共用一块地址,会产生问题,可以自己实现拷贝构造函数,实现值的复制。

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

  1) 不允许用户进行对象的拷贝。一般编译器会提供默认拷贝,可将相应的成员函数声明为private并且不予以实现。但这是有个问题,member(成员)函数和friend(友元)函数仍然可以调用。

  2) 在不想实现的函数中不写函数参数的名称。

class HomeForSale{
    public:
        ...
    private:
        HomeForSale(const HomeForSale&);
        HomeForSale& operator=(const HomeForSale&);
};

  3) 将错误移至编译期,更早地发现错误往往更好。定义一个Uncopyable的基类,其它类继承该类,当执行拷贝时,要调用基类拷贝构造函数,就会出现问题。

class Uncopyable{
    protected:
        Uncopyable();
        ~Uncopyable();
    private:
        Uncopyable(const Uncopyable&);
        Uncopyable& operator=(const Uncopyable&);
};

7. 为多态基类声明virtual析构函数

  1) polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数。这样,每个派生类都要实现析构函数,防止指向derived classes的对象没有析构函数。

  2) 如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

  3) Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不该声明virtual析构函数。

  4) 含有纯虚函数的类是抽象类。

class AWOV{
    public:
        virtual ~AWOV() = 0;   //纯虚函数
};

  5) 不是所有类都是被设计作为基类来使用的。如string类和STL容器类,所以这些类不需要声明为virtual。

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

  1) 析构函数绝对不要吐出异常。如果析构函数调用的函数可能抛出异常,析构函数应该捕捉异常,然后吞下它们或结束程序。

class DBConnection{
    public:
        static DBConnection create();
        void close();
};

class DBManager{
    public:
        ~DBManager(){
            db.close();  //析构函数关闭数据库连接
        }

    private:
        DBConnection db;
};

//调用析构函数时可能会发生异常
DBManager dbM(DBConnection::create());

  上面的代码为了帮助忘记关闭数据库连接的客户关闭连接,在析构函数中调用了close函数,但这个函数可能出现异常,这种必须调用可能产生异常的函数时,需要进行异常捕获。如下:

DBManager::~DBManager(){
    try { db.close(); }
    catch(...){
        //可以记录错误后退出程序
        std::abort();
    }
}

  2) 上面这个问题还不是完善的方案,即使析构函数捕获到异常,客户也无法处理异常,客户需要对某个函数运行期间抛出的异常进行反应,那么class应该提供一个普通函数来执行该操作。

class DBManager(){
    public:
        void close(){
            db.close();
            closed = true;
        }
        ~DBManager(){
            if (!closed){
                try { db.close(); }
                catch(...) {
                    //错误日志...
                }
            }
        }
    private:
        bool closed;
        DBConnection db;
};

  这里面加了一个close函数,客户可以自己调用close函数,当发生异常时,进行异常处理。如果客户没有调用close函数,则可以在析构函数中自动调用。所以,在写程序时,一定要将会发生异常的函数作为一个普通函数,这样可以提供更多的选择。

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

  1) 在构造和析构函数期间不要调用virtual函数,因为这类调用从不下降到derived class(子类)。父类的构造函数先于子类执行,所以父类的自身成分早于子类构造,子类的virtual函数还没有生成,所以即使调用virtual函数,也只会调用父类的virtual函数,即这个被声明为virtual的函数在构造函数中毫无意义。

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

  1) 令赋值(assignment)操作符返回一个reference to *this。这样就可以像基本式一样连续赋值,如基本式的连续赋值:int a,b,c; a=b=c=1

class Widget{
    public:
        Widget& operator+=(const Widget& src){
            ...
            return *this;
        }

        Widget& operator=(const Widget& src){
            ...
            return *this;
        }
}

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

  1) 确保当对象自我赋值时,operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。

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

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

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

  2) 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

三、资源管理

13. 以对象管理资源

  1) 防止资源泄漏,请使用RAII(Resource Acquisition is Initialization;资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。

  2) 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null,即只有一个对象指向这个资源。

14. 在资源管理类中小心copying行为

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

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

  1) APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理的资源”的方法。
  
  2) 对原始资源的访问可能经由显式转换或隐式转换。显式转换更安全,隐式转换更方便。
  

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

  1) 如果new数组时使用[],那么释放资源时就要用delete[],这会调用多个析构函数去释放资源;如果使用new对象不使用[],释放时一定不要使用[]。保持两者一致

std::string str = new std::string;
std::string strArr = new std::string[20];
//释放资源
delete str;
delete[] strArr;

17. 以独立语句将newed对象转入智能指针

  1) 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,很难察觉到资源泄漏。

    processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

上面的代码存在需要三个步骤:

  • 调用priority()
  • 执行new Widget
  • 调用tr1::shared_ptr构造函数

但C++的编译器对这三个执行的次序并不固定,而Java和C#则以特定的顺序完成。但C++中可以确定的是,new Widget一定比tr1::shared_ptr先执行,但对priority()函数的调用却没有限定。如果以下面的顺序:

  • 执行new Widget
  • 调用priority()函数
  • 调用tr1::shared_ptr构造函数

这就会引发一个问题,如果第二步priority()函数发生异常,那么new Widget就无法放入shared_ptr中,这样就会造成资源泄漏(shared_ptr用来进行资源管理)。正确的做法是将语句分离,先创建资源并放到资源管理器后,再进行下步操作。

    //先创建对象并置入资源管理器中
    std::tr1::shared_ptr<Widget> pw(new Widget);
    //再进行下步操作
    processWidget(pw, priority);

四、设计与声明

18. 让接口容易被正确使用,不易被误用

  1) 好的接口容易被正确使用。
  2) 保持接口的一致性,与内置类型行为兼容。
  3) 为阻止误用,可以采用建立新类型、限制类型上的操作,束缚对象值,消除客户的资源管理责任。
  4) tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁。

19. 设计class犹如设计type

  1) class的设计就是type的设计。在定义一个新type之前,请确定符合一些规范。

20. 宁以pass-by-reference-to-const替换pass-by-value

  1) 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
  2) 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

21. 必须返回对象时,别妄想返回其reference

  1) 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向local static对象而有可能同时需要多个这样的对象。

22. 将成员变量声明为private

  1) 切记将成员变量声明为private。这样可以保证数据的一致性、可细微划分方向控制、允诺条件获得保证,并提供class作者以充分的实现弹性。
  2) protected并不比public更具封装性。

23. 宁以non-member、no-friend替换member函数

  1) 宁可拿non-member non-friend函数替换member函数。这样可以增加封装性、包裹弹性和机能扩充性。

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

  1) 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

25. 考虑写出一个不抛异常的swap函数

  1) 当std::swap效率不高时,可以提供自己版本的swap,但要保证这个swap不会抛出异常。
  2) 如果提供一个member swap,也应该提供一个non-member swap来调用前者。
  3) 调用swap时,使用using std::swap;声明std::swap,然后调用swap并且不加任何“命名空间修饰符”。
  4) 为“用户定义类型”进行std template特化是好的,但不要尝试在std内加入某些对std而言是全新的东西。

五、实现

26. 尽可能延后变量定义式的出现时间

  1) 尽可能延后变量定义的时间。这样可以增加程序的清晰度并改善程序效率。

std::string encryptPass(string& pass){
    using namespace std;
    //在抛出异常前定义,如果抛出了异常,则没有必要定义这个变量
    string encrypted;
    if (pass.length() < MinLenth){
        throw login_error("Password is too short");
    }
    //应该把变量移动这里
    //string encrypted;
    ...
    return encrypted;
}

27. 尽量少做转型动作

  1) 两个旧式转型。
   1. (T)expression
   2. T(expression)
  2) 四个新式转型。
   1. const_cast: 将const转为non-cast。
   2. dynamic_cast: 将父类转为子类(耗费重大,循环中尽量不要用)。
   3. reinterpret_cast: 执行低级转型,根据编译器不同有所改变,不可以移植。(很少用)。
   4. static_cast: 做上面三个转型的逆操作。
  3) 如果可以,尽量避免转型,特别是在注重效率的代码避免使用dynamic_casts。可以使用virtual的继承去实现-_1!
  4) @^@如果可以将转型放在函数背后,客户可以调用该函数,而不需要进行转型操作。
  5) 宁可使用新式(C++-style)转型,不要使用旧式转型。前者更明确,更容易查找。
 

28. 避免返回handles指向对象内部成分

  1) 避免返回handles(包括指针、reference、迭代器)指向对象内部。这可以增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码”的可能性降至最低。
  

29. 为“异常安全”而努力是值得的

  1) 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  2) “强烈保证”往往能够以copy-and-swap实现,但要考虑资源消耗和效率问题,不是所有情况都有必要的。
  3) 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
 

你可能感兴趣的:(学习《Effective C++》)