美文网首页
《Effective C++》学习笔记(1)

《Effective C++》学习笔记(1)

作者: 暗夜望月 | 来源:发表于2017-03-29 09:36 被阅读0次

    1 让自己习惯 C++

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

    将C++视为一个由相关语言组成的联邦而非单一语言。在某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。

    • C:说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
    • Objective-Oriented C++:C with Classes所诉求的。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
    • Template C++:C++的泛型编程部分
    • STL:template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...

    ** note: **
    C++高效编程守则视状况而改变,取决于你使用C++的哪一部分。


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

    C++ 编译过程:预处理 --> 编译 --> 链接
    预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

    “宁可以编译器替换预处理器”。就是尽量少用预处理。

    • 预处理器#define ASPECT_RATIO 1.653将所有出现ASPECT_RATIO的地方替换为1.653,ASPECT_RATIO可能并未进入记号表(symbol table)。因此,当出现错误时报的是1.653而不是ASPECT_RATIO,导致目标定位有问题,问题追踪有困难。如果使用变量,则可轻易地判断。
      此外,盲目地把ASPECT_RATIO替换为1.653可能会在目标码中出现多份1.653,改用常量绝不会出现相同情况。所以尽量定义为常量,const double ASPECT_RATIO = 1.653

    • 如果在数组初始化的时候,编译器需要知道数组的大小,且编译器(错误地)不允许使用“static整数型class常量”进行数组初始化,这时可以使用枚举类型enum来替代define。

    class GamePlays{
    private:
      static const int NumTurns = 5;      // static整数型class常量
      enum { NumTurns = 5 };             // 枚举
      int scores[NumTurns];
    ... ...
    }
    
    • 宏看起来像函数,但不会招致函数调用带来的额外开销。如果你想获得高效,建议使用inline内联函数。

    有了consts 、enums 和inlines,我们对预处理器(特别是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而 #ifdef / #ifndef 也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但我们要尽量限制预处理器的使用。

    ** note: **

    1. 对于单纯常量,最好以const对象或enum替换#define。
    2. 对于形似函数的宏,最好改用inline函数替换#define。

    条款03:尽可能使用 const

    const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。如果const修饰变量,则表示这个变量不可变;如果const修饰指针,表示指针指向的位置不可改变。

    • 和指针有关的const判断:
    1. 如果关键字const出现在星号左边,表示被指物事常量。const char *pchar const *p两种写法意义一样,都说明所致对象为常量。
    2. 如果关键字const出现在星号右边,表示指针自身是常量。
    const char * p = "hello"; // *p的hello不可变, 与char const * p = "hello"等价
    char * const p = "hello"; // 表示p的值不可变,即p不能指向其它位置
    
    • STL迭代器的const:
    1. 声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值可以改变。
    2. 如果想要迭代器所指的东西不可改变(即模拟一个const T*指针),使用const_iterator。
    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
    
    • 令函数返回一个常量值,可以避免意外错误。
      如下代码,错把==写成=,一般程序对*号之后进行赋值会报错,但在自定义操作符面前不会(因为自定义*号后返回的是Rational对象实例的引用,可以拿来赋值,不会报错)。如果*不写成const,则下面的程序完全可以通过,但写成const之后,再对const进行赋值就出现问题了。函数的参数,如果无需改变其值,尽量使用const,这样可以避免函数中错误地将==等于符号误写为=赋值符号,而无法察觉。
    class Rational { ... };
    const Rational operator* (const Rational& lhs, const Rational& rhs);
    ...
    Rational a, b, c;
    if(a * b = c)...  //把==错写成=,比较变成了赋值
    
    • const作用于成员函数,有两个作用:
    1. 可以知道哪些函数可以改变对象内容,哪些函数不可以。
    2. 改善C++效率,通过pass by reference_to_const(const对象的引用)方式传递对象可改善C++效率。
      下面是常量函数与非常量函数的形式:
    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对象
    

    在C++中,只有被声明为const的成员函数才能被一个const类对象调用。

    • 成员函数是const意味着什么?
    1. bitwise const主张const成员函数不可以改变对象内任何non-static成员变量。但一个更改了“指针所指物”的成员函数虽然不能算const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。
    2. logical const主张成员函数可以修改它所处理的对象内的某些bits,但要在客户端侦测不出的情况下才得如此。
      编译器默认执行bitwise。如果想要在const函数中修改non-static变量,需将变量声明为mutable(可变的)。
    class TextBlock{
        private:
            char* pText;
            mutable std::size_t textLength;    // 即使在const成员函数内,
            mutable bool lengthIsValid;        // 这些成员变量也可能会被更改。
        public:
            ...
            std::size_t length() const; 
    };
    std::size_t TextBlock::length() const{
        if (!lengthIsValid){
            textLength = std::strlen(pText);  //加上mutable修饰后,便可以修改其值
            lengthIsValid = true;
        }
    }
    
    • 避免const和non-const成员函数重复
      如果const和non-const成员函数功能相当、代码重复,编译时间、维护等会是一个大问题,这时就用non-const函数去调用const函数,但不能反过来。这是因为non-const函数可能会改变对象,const函数承诺不改变对象,const调用non-const就不安全了
    class TextBlock{
        public:
            const char& operator[](std:size_t position) const
                ...
                return text[position];
            }
    
            char& operator[] (std:size_t position){
                return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
            } 
    };
    

    上面代码进行了两次转型:

    1. 第一次用static_cast来为*this添加const,这使接下来调用operator[ ]时得以调用const版本;
    2. 第二次则是用const_cast从const operator[]的返回值转除const,以符合non-const返回值类型。

    ** note: **

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

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

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

    • 内置类型以外的类型,初始化要靠构造函数,要确保每一个构造函数都将对象的每一个成员初始化。
      类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操作,这样通常效率更高。因为赋值的版本其实是先进行初始化再进行赋值,而成员初值列版本是直接进行初始化,这对于非内置类型(std::string等)来说显然后者效率更高。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。对于const和reference类型必须是初始化,赋值操作是不允许的。

    • base classes更早于derived classes被初始化,class的初值列成员变量的排列顺序与其声明顺序相同。

    • “不同编译单元内定义之non-local-static对象”的初始化次序。
      static对象,其寿命从被构造出来直到程序结束为止,包括global对象,定义于namespace作用域内的对象,在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象被称为local static对象(因为它们对函数而言是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();
    }
    

    C++对“定义于不同编译单元内的non-local static对象”的初始化相对次序并无明确定义,因此,为防止A的初始化需要B,但B尚未初始化的错误,将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static),然后用户调用这些函数,而不直接涉及这些对象。

    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 对象会在"该函数被调用期间","首次遇上该对象之定义式"时被初始化,这样就保证了对象被初始化。使用函数返回的“指向static对象”的reference,而不再使用static对象本身。这样做的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。

    ** note: **

    1. 为内置对象进行手工初始化,因为C++不保证初始化它们;
    2. 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
    3. 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

    相关文章

      网友评论

          本文标题:《Effective C++》学习笔记(1)

          本文链接:https://www.haomeiwen.com/subject/ziusottx.html