• Effective C++读书笔记~01 让自己习惯C++


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

    View C++ as a federation of languages.

    如何理解C++?

    将C++视为一个由相关语言组成的联邦而非单一语言。主要的4个次语言(sublanguage):

    • C, C++的基础
    • Object-Oriented C++, C with Classes:构造函数、析构函数,封装,继承,多态,虚函数等。
    • Template C++,C++泛型编程部分
    • STL,template程序库,对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调。

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

    [======]

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

    Prefer consts, enums, and inlines to #defines.

    另一种理解:“编译器替换预处理器”

    const等类型可以有编译器进行检查,而#define对编译器是透明的,无法利用编译器检查。

    两种替换#define特殊情况:

    1)定义常量指针(constant pointers)
    如果是用char *定义常量指针,用于替换#define,必须写const两次。

    const char *const authorName = "Scott Meyers";
    

    如果是用string(通常比char *要合适)

    const std::string authorName("Scott Meyers");
    

    2)class专属常量
    为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member)。而为确保此常量至多只有一份实体,必须让它成为static成员:

    class GamePlayer {
    private:
        static const int NumTurns = 5; // 常量声明式
        int scores[NumTurns]; // 使用该常量
    };
    
    const int GamePlayer::NumTerns; // NumTerns定义
    // 为什么没有赋值?
    

    上面定义NumTerns时,为什么没赋值?
    因为经常把定义的式子放到实现文件(.cpp, .cxx)中,由于class常量已经在声明时获得初值(5),因此定义时不可再设初值。
    当然也可以把初值放在定义中,而声明中不指定初值。

    PS:#define无法创建一个class专属常量,因为宏一旦被定义,在其后的编译过程中有效(除非被#undef)

    enum hack

    1)替换static const
    如果编译器必须在编译器就知道数组大小,static const方式无法做到,可以改成使用enum

    class GamePlayer {
    private:
        enum{ NumTurns = 5 }; // enum hack: NumTurns成为常量5的一个记号名称
        int scores[NumTurns]; // 使用该常量
    };
    

    2)实用主义
    很多代码用了它,必须认识。也是模板元编程的基础技术。

    用template inline替换宏函数

    可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全(type safety)
    比如,用template inline函数callWithMax,替换宏函数,以a,b中较大者调用f。

    // 不安全的宏函数
    // 以a和b的较大者调用f
    #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
    
    // 兼顾安全与效率的template inline函数
    template<typename T>
    inline void callWithMax(cosnt T& a, const T& b)
    {
        f(a > b? a : b);
    }
    

    PS:
    1)对于单纯常量,最好以const 对象或enum替换#define;
    2)对于形似函数的宏(macro),最好改用inline函数替换#define;

    [======]

    条款03:尽可能使用const

    Use const whenever possible.

    顶层const:代表指针变量自身无法修改;
    底层const:代表指针所指对象无法修改。

    char* const p = "hello"; // 顶层const, const pointer, non-const data
    const char *p = "hello"; // 底层const, non-const pointer, cosnt data
    

    const与迭代器

    迭代器的作用像T指针。
    如果希望迭代器本身无法改变,而指向的内容可以改变,可以在迭代器前面加上const,即T
    const。
    如果希望迭代器本身可以改变,而指向的内容不可改变,可以使用const_iterator,即const T*。

    std::vector<int> vec;
    ...
    const std::vector<int>::iterator iter = vec.begin(); // <=> T* const
    *iter = 10; // OK
    ++iter; // error: iter 是const
    
    std::vector<int>::const_iterator cIter = vec.begin(); // <=> const T*
    *cIter = 10; // error: *cIter是const
    ++cIter; // OK
    

    const成员函数

    const成员函数不允许更改对象的任何变量(static除外)。目的在于确认该成员函数可作用于const对象身上。const成员函数很重要,原因:
    1)使class接口比较容易被理解。因为很容易得知,哪个函数可以改动对象内容,而哪个函数不行。
    2)使“操作const对象”成为可能。对编写高效代码是关键:以pass by reference-to-const方式传递对象(条款20)。??

    2个成员函数,如果只是常量性(constness)不同,可以被重载。non-const对象调用non-const function for non-const object, const 对象调用const function for const object。

    class TextBlock
    {
    public:
           TextBlock(const char* s) : text(s) {}
           const char& operator[] (size_t position) const /* operator[] for const 对象 */
           {
                  return text[position];
           }
           char& operator[] (size_t position) /* operator[] for non-const 对象 */
           {
                  return text[position];
           }
    private:
           string text;
    };
    

    non-const与const成员函数,在使用上的差异:

    TextBlock tb("hello"); // non-const 对象
    tb[0] = 'x'; // OK: 写一个non-const TextBlock
    cout << tb[0] << endl; // OK: 调用non-const TextBlock::operator[]
    
    const TextBlock ctb("hello"); // const 对象
    ctb[0] = 'y';    // error: 写一个const TextBlock
    cout << ctb[0] << endl; // OK: 调用const TextBlock::operator[]
    

    注意:要修改string text,operator[] 返回值必须是引用类型(char &)或指针类型,不能是值。如果是值, 那么调用者修改的是tb[0]副本,而不是tb[0]本身。
    比如,operator[] 返回char,下面句子无法通过编译:

    tb[0] = 'x';
    

    成员函数如果是const意味着什么?

    这里有2个流行概念:bitwise constness(又称physical constness),logical constness。

    1)bitwise constness
    成员函数只有在不更改对象任何成员变量(static除外)时,才能说是const。i.e. 它不更改对象内的任何一个bit。编译器只需要寻找成员变量的赋值动作即可。bitwise constness是C++对常量性(constness)的定义,因此const成员函数不能更改对象内任何non-static成员变量。
    但是,bitwise测试存在缺陷:如果类对象持有的是指针,bitwise测试能确保const成员函数不修改指针本身,但无法确保指针指向的内容不被修改。

    class CTextBlock
    {
    public:
           CTextBlock(const char* s) { pText = new char[strlen(s) + 1]; strcpy(pText,  s); }
           ~CTextBlock() { delete[] pText; }
           char& operator[] (size_t position) const /* bitwise const声明, 确保operator[] 不修改pText */
           {
                  return pText[position];
           }
           ...
    private:
           char *pText;
    };
    
    // 以下内容能通过编译器
    // 可以通过operator[]返回的指针, 修改pText指向的内容
    const CTextBlock cctb("Hello"); // 声明一个常量对象
    char *pc = &cctb[0];  // 调用const operator[]取得一个指针, 指向cctb第0个数据
    *pc = 'J'; // 更新了cctb[0]内容
    

    2)logical constness
    由于bitwise constness并不能完全避免在const成员函数内修改处理对象内的bits,logical constness主张认为一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。
    例如,CTextBlock class有可能高速缓存(cache)文本区块的长度,以便应付查询:

    class CTextBlock
    {
    public:
           // ...
           size_t length() const;
    private:
           char *pText;
           size_t textLength;   // 最近一次计算的文本区块长度
           bool lengthIsValid;  // 目前的长度是否有效
    };
    
    size_t CTextBlock::length() const
    {
           if (!lengthIsValid) {
                  textLength = strlen(pText); // 错误:不能在const成员函数内修改对象属性textLength和lengthIsValid
                  lengthIsValid = true;
           }
           return textLength;
    }
    // ...
    

    上面length()的实现不是bitwise const,因为textLength和lengthIsValid都可能被修改,因此在用length()取得pText字符串最新长度时,需要重新计算、更新textLength和lengthIsValid的值。
    而要在const成员函数内修改对象属性,可以用C++与const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束:

    class CTextBlock
    {
    public:
           // ...
           size_t length() const;
    private:
           char *pText;
           mutable size_t textLength;   // mutable表示这些成员变量可能总是会被更改, 即使是const成员函数内
           mutable bool lengthIsValid; 
    };
    
    size_t CTextBlock::length() const
    {
           if (!lengthIsValid) {
                  textLength = strlen(pText); // OK
                  lengthIsValid = true; // OK
           }
           return textLength;
    }
    // ...
    

    在const和non-const成员函数中避免重复

    虽然上面的例子中,用mutable可以解决在const成员函数中修改对象属性,但不能解决代码重复的问题。比如,const 成员函数和non-const成员函数,operator[] 都可能有边界检验、日志记录数据访问、检验数据完整性这相同的3步。

    class TextBlock
    {
    public:
           TextBlock(const char* s) : text(s) {}
           const char operator[] (size_t position) const
           {
                  // 边界检验 (bounds checking)
                  // 日志记录数据访问(log access data)
                  // 检验数据完整性(verify data integrity)
    
                  return text[position];
           }
           char operator[] (size_t position)
           {
                  // 边界检验 (bounds checking)
                  // 日志记录数据访问(log access data)
                  // 检验数据完整性(verify data integrity)
    
                  return text[position];
           }
    private:
           string text;
    };
    

    能否把这些重复的代码,利用起来呢?编写一次,使用2次。
    答案是可以的,可以将常量性转除(casting away constness):利用non-const函数调用cast函数,首先需要转型,然后去掉const修饰:

    class TextBlock
    {
    public:
           TextBlock(const char* s) : text(s) {}
           const char& operator[] (size_t position) const
           {
                  // 边界检验 (bounds checking)
                  // 日志记录数据访问(log access data)
                  // 检验数据完整性(verify data integrity)
    
                  return text[position];
           }
           char& operator[] (size_t position)
           {
                  // 边界检验 (bounds checking)
                  // 日志记录数据访问(log access data)
                  // 检验数据完整性(verify data integrity)
    
                  return const_cast<char &>(static_cast<const  TextBlock&>(*this)[position]);
           }
    private:
           string text;
    };
    

    这里有2个转型动作:
    1)让non-const operator[]调用其兄弟const operator[],通过static_cast将*this由原始类型TextBlock& 安全转型为const TextBlock&;
    2)再从const operator[]的返回值中,通过const_cast移除const。该步当然也可以选择用C风格的强制类型转换,不过并不推荐;

    为避免重复,为什么只能是令non-const版本调用const的operator[],而不能是const版本调用non-const的?
    因为const成员函数承诺绝不改变其对象的逻辑状态(logical state),non-const成员函数没有这样的承诺。如果在const函数中调用了non-const函数,就是冒了这样的风险。而non-const函数中调用const函数却不会带来风险。

    小结

    • 将某些东西声明为const,可以帮助编译器检查出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数;
    • 编译器强制实施bitwise constness(const成员函数要求不能修改任何对象属性),编写代码时应该使用“概念上的常量性”(conceptual constness)(用mutable在const成员函数修改对象属性);
    • 当const和non-const成员函数有着实质等价实现时,可以令non-const版本调用const版本避免代码重复。

    [======]

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

    Make sure thath objects are initialized before they're used.

    对于内置型对象

    内置类型对象x,在某些语境下保证被初始化为0,不过其他语境并不能保证为0。例如,

    // 作为全局变量
    int x; // 0
    
    // 作为自动变量
    void fun() {
        int x; // x不能保证为0
    }
    
    // 作为类对象变量的成员
    class Point {
        int x, y; 
    };
    
    Point p; // 全局变量p确保x为0
    

    最佳解决办法:手工在使用对象之前现将其初始化。

    int x = 0; // 对int手工初始化
    const char* text = "A C-style string"; // 对指针手工初始化
    
    double d;
    std::cin >> d; // 以读取input stream方式完成初始化
    

    对于类对象

    使用构造函数(constructor):确保构造函数都将对象的每个成员初始化。
    需要注意:在构造函数中使用member initialization list(成员初值列),才是初始化成员;而构造函数体内使用赋值操作,是赋值(assignment)而非初始化(initialization)。

    class PhoneNumber
    {
    public:
           string number;
    };
    class ABEntry
    {
    public:
           // case1 构造函数体内对class成员进行赋值
           ABEntry(const string& name, const string& address, const list<PhoneNumber>&  phone)
           {
                  /* 这些都是赋值(assignment), 而非初始化 */
                  theName = name; // 如果是类对象, 赋值(=)操作会调用拷贝构造函数; 如果是基本类型, 则直接赋值
                  theAddress = address;
                  thePhone = phone;
                  numTiemsConsulted = 0;
           }
    
           //  case2 使用member initialization list构造ABEntry对象
           ABEntry(const string& name, const string& address, const list<PhoneNumber>&  phone)
                  : theName(name), // 调用theName的default构造函数, 从而构造成员对象
                  theAddress(address),
                  thePhone(phone),
                  numTimesConsulted(0)
           {}
    private:
           string theName;
           string theAddress;
           list<PhoneNumber> thePhone;
           int numTimesConsulted;
    };
    

    1)case1 构造函数体内对class成员进行赋值,实际上在进入构造函数体之前,会调用构造函数对各对象成员进行初始化。构造函数体内是赋值操作,而非初始化。
    2)case2 是推荐的做法,在进入构造函数前,就进行了成员初始化。

    因此,case1 相当于下面的代码,可以看出对成员相当于调用了2次赋值操作:

    ...
    // case1 构造函数体内对class成员进行赋值
    ABEntry(const string& name, const string& address, const list<PhoneNumber>&  phone)
    	: theName(), // 构造成员对象
    	theAddress(),
    	thePhone(),
    	numTimesConsulted(0)
    {
    	/* 这些都是赋值(assignment), 而非初始化 */
    	theName = name; // 如果是类对象, 赋值(=)操作会调用拷贝构造函数; 如果是基本类型, 则直接赋值
    	theAddress = address;
    	thePhone = phone;
    	numTiemsConsulted = 0;
    }
    ...
    

    注意:如果成员过多,或者无需初值,可以在成员初值列中忽略该成员,这样它就没有初值。

    关于成员初始化次序:
    member initialization list中对象成员初始化次序,以防漏掉,或者导致检阅者迷惑,建议与声明的顺序保持一致。

    不同编译单元内定义的non-local static对象的初始化次序

    static对象,其寿命从被构造出来直到程序结束为止。因此stack和heap-based对象都不是static对象。static对象包括:global对象、定义于namespace作用域内的对象,在class内、函数内、file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他static对象都称为non-local static对象。
    编译单元(transitation unit):指产出单一目标文件(sigle object file)的那些源码。通常是单一源码文件(.c, .cpp, .cxx)加上所包含的头文件(#include files)。

    问题:如果编译单元A内的某个non-local static对象的初始化动作,使用了另一个编译单元B内的某个non-local static对象,A初始化时,B可能没有被初始化。而C++对定义于不同编译单元内的non-local static对象的初始化次序,并没有明确的规定(事实上也很难做到)。

    // FileSystem.h
    class FileSystem {
    public:
        ...
        size_t numDisks() const;
        ...
    };
    
    extern FileSystem tfs; // 声明global 对象tfs, 属于non-local static对象
    
    // Directory.h
    class Directory {
    public:
        Director(params);
        ...
    };
    extern Directory tempDir;
    
    // Directory.cpp
    Director::Director(params)
    {
        ...
        size_t disks = tfs.numDisks();// 错误:使用tfs对象, 但C++无法保证位于编译单元FileSystem.o中的FileSystem tfs已经初始化完成
    }
    

    如何解决这个问题?
    可以将B的non-local static对象通过函数包装起来,替换为local static对象,A只需要通过这个包装函数访问B的local static对象即可。类似于单例模式。

    
    
    // FileSystem.h
    class FileSystem { ... };
    // reference-returning函数, 返回local static对象引用
    FileSystem& tfs() // 用于替换tfs对象, 将non-local static对象tfs替换为local static对象, 并返回其reference
    {
        static FileSystem fs; // 这里会调用FileSystem构造函数, 确保fs初始化完成
        return fs;
    }
    // Directory.h
    class Directory { ... };
    // Directory.cpp
    Directory::Directory(params)
    {
        ...
        size_t disks = tfs().numDisks();
        ...
    }
    // reference-returning函数, 返回local static对象引用
    Directory& tempDir() // 用于替换tempDir对象
    {
        static Directory td;
        return td;
    }
    

    如果这种reference-returning函数,被频繁调用,还可以声明为inline函数。

    缺点:
    由于函数内含static对象(带有状态),在多线程系统中带有不确定性。解决办法是在程序单线程启动阶段,手工调用所有reference-returning函数,可消除与初始化有关的“竞争条件(race conditions)”。

    注意:
    绝对要避免这种尴尬状况:对象A初始化依赖于对象B,而对象B的初始化又要依赖于A。

    小结

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

    [======]

  • 相关阅读:
    搭建woocomerce网站
    Cozmo 机器人编程环境搭建
    DevExpress Wizard的控件使用方法
    DevExpress 地图的控件使用方法
    DevExpress 摄像机的控件使用方法
    大疆第一人称视角眼镜goggle激活
    iis支持asp.net4.0的注册命令使用方法
    【转】PowerDesigner删除外键关系,而不删除外键列
    【转】ABP webapi三种方式
    【转】OAuth2.0的refresh token
  • 原文地址:https://www.cnblogs.com/fortunely/p/15558963.html
Copyright © 2020-2023  润新知