zoukankan      html  css  js  c++  java
  • Effective C++读书笔记~5 实现

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

    Postpone variable definitions as long as possible.

    为什么要延后变量定义?

    因为定义一个变量会有构造成本和析构成本,而该变量可能从始至终并未使用过。因此,如果能尽量避免,就能减少这些成本。

    如何延后变量定义?

    1)不要过早定义变量,即将使用的时候定义

    // 过早定义变量encrypted
    string encrypPassword(const string& password)
    {
        using namespace std;
        string encrypted; // 提前定义变量 encrypted, 保存加密后的密码
        if (password.length() < MinimumPasswordLength) { // 如果抛出异常, 就不会使用变量encrypted
            throw logic_error("Password is too short");
        }
        ...
        return encrypted;
    }
    
    ==> 改进
    // 延后定义变量encrypted
    string encrypPassword(const string& password)
    {
        using namespace std;
        
        if (password.length() < MinimumPasswordLength) { // 可能抛出异常
            throw logic_error("Password is too short");
        }
        string encrypted; // 延后定义变量 encrypted, 保存加密后的密码
        ...
        return encrypted;
    }
    

    2)定义对象时,提供构造参数
    如果不提供构造参数,意味着调用default构造函数。条款4解释了“通过default构造函数构造对象,然后赋值”的效率比“直接在构造时指定初值”更低。

    void encrpyt(string& s) // 对s加密
    {
        ...
    }
    
    // default构造对象
    string encryptPassword(const string& password)
    {
        ...
        string encrypted; // 已延后定义encrypted. default构造函数 构造encrypted
        encrypted = password; // 赋值给encrypted
        encrpyt(encrypted); 
        return encrypted;
    }
    
    ==> 改进
    // 带参数构造对象
    string encryptPassword(const string& password)
    {
        ...
        string encrypted(password); // 已延后定义encrypted. 参数构造encrypted (copy 构造)
        encrpyt(encrypted);
        return encrypted;
    }
    

    3)循环体内 or 外定义变量?
    对于循环,变量是定义在循环体外(方法A),还是循环体内(方法B)?
    先看下面的例子,

    // 方法A:定义于循环外
    Widget w;
    for (int i = 0; i < n; ++i) {
        w = 取决于i的某个值;
        ...
    }
    
    // 方法B:定义于循环体内
    for (int i = 0; i < n; ++i) {
        Widget w(取决于i的某个值);
        ...
    }
    

    Widget函数内部定义变量的开销:
    方法A:1个构造函数 + 1个析构函数 + n个赋值操作;
    方法B:n个构造函数 + n个析构函数;

    当class的赋值成本明显低于构造 + 析构成本时,特别n较大时,A方法较好;
    否则,B方法较好,因为B的程序更容易理解。

    也就是说,当1)明确知道赋值成本比“构造+析构”成本低,2)正在处理代码的效率高度敏感时,选择方法A;否则选择方法B。

    小结

    1)尽可能延后变了定义式的出现,因为可以增加程序的清晰度并改善程序效率。

    [======]

    条款27:尽量少做转型动作

    Minimize casting.

    为什么要减少转型动作?

    转型(casts)破坏了类型系统(type system),可能导致任何种类的麻烦,难以辨识。

    转型语法

    旧式转型:
    1)C风格转型

    (T)expression // 将expression转型为T
    

    2)函数风格转型

    T(expression) // 将expression转型为T
    

    C++新式转型(称为new-style或C++-style casts):
    4种

    const_cast<T>(expression)
    dynamic_cast<T>(expression)
    reinterpret_cast<T>(expression)
    static_cast<T>(expression)
    

    各个转型目的:
    1)const_cast 通常被用来将对象的常量性移除(cast away the constness)。唯一有此能力的C++-style转型操作符。
    non-const member函数调用const member时,const_cast可用于去掉常量性。

    2)dynamic_cast 主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
    一般情况下,不允许基类指针转型为派生类指针或引用,因为编译器并不知道运行时指针所指向的实际对象,是否为派生类对象。而dynamic_cast允许基类指针在运行时安全地转型为派生类指针。

    3)reinterpret_cast 意图执行低级转型,实际动作(及结果)可能取决于编译器,意味着它不可移植。
    慎用。

    int *ip;
    char *pc = reinterpret_cast<char *>(ip); // pointer from int* to char*
    
    string str(pc); // 可能导致程序异常
    

    4)static_cast 用来强迫隐式转换,例如将non-const对象转为const对象(条款3),或将int转为double等。可以完成1)~3)的多种反向转换。但无法将const转为non-const(去掉常量性) -- 因为这是只有const_cast才办得到。

    旧式转型与新式转型的选择

    旧式转型仍然合法,但新式转型更受欢迎。原因在于:
    1)容易在代码中被辨识(人眼或工具如grep);
    2)各转型动作目标越窄化,编译器越可能诊断出错误的应用;

    除了explicit构造函数转型,其他情况推荐使用新式转型。

    去掉转型动作

    有时,容易写出某些似是而非的代码,在其他语言可能正确,但在C++则不正确。

    1)去掉static_cast案例

    class Windows { // base class
    public:
        virtual void onResize() {...} // base onResize实现
        ...
    };
    
    class SpecialWindow: public Window { // derived class
    public:
        virtual void onResize() { // derived onResize实现
            static_cast<Window>(*this).onResize(); // 错误: 将*this转型为Window(副本), 然后调用其onResize
            
            ... // SpecialWindow专属行为, 作用于*this对象
        }
    };
    

    乍一看,并没有什么问题。然而,仔细分析derived class的onResize中转型行为,存在很大问题。"static_cast(this).onResize()"首先将 "this 对象的base class"拷贝构造一个临时副本,然后调用临时副本的onResize函数。调用的onResize函数既不是当前对象上的函数,也不是当前对象基类那部分的函数,而是基类对象的副本的函数。也就是说,如果onResize如果修改了基类数据(毕竟没有const限定),而下面的SpecialWindow也修改了对象,就造成了基类和派生类数据的不一致。
    解决办法:去掉转型动作,代之以你真正想说的话。
    不要哄骗编译器将*this视为一个base class对象,只是想调用base class版本的onResize函数,令其作用于当前对象身上。可以这么写:

    class SpecialWindow: public Window {
    public:
        virtual void onResize() {
            Window::onResize(); // 调用Window::onResize作用于*this身上
            ...
        }
    };
    

    2)去掉dynamic_cast案例
    dynamic_cast的需要实现版本执行速度相当慢,尽量减少使用次数。
    为什么需要dynamic_cast?
    通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你手上只有一个“指向base”的pointer或reference,你只能靠它们来处理对象。有2个一般性做法可以避免该问题:
    (1)使用容器,并在其中存储直接指向derived class对象的指针(通常是智能指针,条款13),以便消除“通过base接口处理对象”的需要。
    假设之前的Windows/SpecialWindow继承体系的例子中,只有SpecialWindow才支持闪烁效果。

    // 使用dynamic_cast效率低下做法
    class Window { ... };
    class SpecialWindow: public Window {
    public:
        void blink();
    };
    
    typedef vector<shared_ptr<Window>> VPW; // vector存放Window对象的智能指针
    VPW winPtrs;
    ... // 往vector装数据等操作
    for (VPW::iterator iter = winPtrs.begin(); iter != winPtr.end(); ++iter) {
        if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()) // 使用了dynamic_cast
            psw->blink();
    }
    
    ==> 应该改成
    // 去掉了dynamic_cast转型, 更高效
    typedef vector<shared_ptr<SpecialWindow>> VPW; // vector存放SpecialWindow对象的智能指针
    VPSW winPtrs;
    ... // 往vector装数据等操作
    for(VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) // 不使用了dynamic_cast
        (*iter)->blink();
    

    这种改动方式使用简单,而且安全,但有一个缺点:无法在同一容器内 存储指针以指向所有可能的各种Window派生类。如果要处理多种窗口类型(Window派生类),可能需要多个容器,并且它们都必须具备类型安全性(type-safe)。

    (2)在base class内提供virtual函数,做想对各个Window派生类做的事情。因为指针指向的对象会在运行时调用virtual函数,以实现各自想做的事情。

    // 为base class添加virtual函数
    class Window {
    public:
        virtual void blink() { } // 缺省代码“什么都没做”,可能是个糟糕的主意, 见条款34. 这里是为了演示
        ...
    };
    
    class SpecialWindow: public Window {
    public:
        virtual void blink() { ... } // 在该class内, blink做符合当前class应该做的事情
    };
    
    typedef vector<shared_ptr<Window>> VPW; // vector元素还是使用指向base class Window对象的智能指针
    VPW winPtrs;
    ...// 往vector装数据等操作
    for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
        (*iter)->blink(); // 这里没有dynamic_cast, 运行时会自动调用derived class对应virtual函数
    

    小结

    1)如果可以,尽量避免转型,特别是注重效率的代码中,应极力避免dynamic_casts。如果有个设计需要转型动作,尝试发展无需转型的替代设计。
    2)如果转型是必要的,试着将它隐藏在某个函数背后。客户调用该函数即可,而不需要将转型放进他们自己代码中。(隔离转型动作)
    3)宁可使用C++-style(新式)转型,不要使用旧式转型。

    [======]

    条款28:避免返回handles指向对象内部成分

    Avoid returning "handles" to object internals.

    为什么要避免返回handles指向对象内部成分?
    先来看一个例子。假设程序涉及矩形,每个矩形(Rectangle)由左上角、右下角表示。为了让Rectangle对象尽可能小,可能会决定不把定义这些矩形的点存放在Rectangle对象内,而是放在一个辅助的struct内,让再Rectangle去指它:

    class Point {
    public:
        Point(int x, int y);
        ...
        void setX(int newValue);
        void setY(int newValue);
        ...
    };
    
    struct RectData { // 点数据用来表现一个矩形
        Point ulhc; // upper left-hand corner 左上角
        Point urhc; // lower right-hand corner 右下角
    };
    class Rectangle {
        ...
    private:
        shared_ptr<RectData> pData; // 智能指针指向RectData对象
    };
    

    返回reference指向对象内部成分的缺陷

    Rectangle的客户需要计算Rectangle的(四个顶点)范围,所以该class提供upperLeft函数、lowerRight函数。Point是用户自定义类型,

    // 可以通过编译, 但设计是错误的
    class Rectangle {
    public:
        ...
        Point& upperLeft() const { return pData->ulhc; } // 左上顶点, by reference方式返回用户自定义类型(根据条款20)
        Point& lowerRight() const { return pData->urhc; } // 右下顶点, by reference方式返回用户自定义类型(根据条款20)
        ...
    };
    

    上面的设计可以通过编译,但却是错误的。因为它是自我矛盾的。一方面,upperLeft, lowerRight声明为const函数,表明提供Rectangle坐标点信息,而不是让客户修改Rectangle (条款3);另一方面,2个函数都返回reference指向了private内部数据,调用者可以通过这些reference修改内部private数据。

    Point coord1(0, 0);
    Point coord2(100, 100);
    const Rectangle rec(coord1, coord2); // rec是const 矩形, 左上角(0,0) 右下角(100,100)
    
    rec.upperLeft().setX(50); // rec现在变成了左上角(50,0)右下角(100,100)
    // 也就是说, rec的private数据发生了改变, const 矩形不再是不可变的(const).
    

    2个教训:

    1)成员变量的封装性最多只等于“返回其reference”的函数的访问级别,而不再是简单的声明变量时的访问级别。
    比如,pData虽然是class Rectangle的private成员变量,但是class也提供了public member返回了其引用(upperLeft, lowerRight),这样,pData的封装性不再是private,而是public。

    2)如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据(reference所指数据)。
    比如,const成员函数upperLeft和lowerRight各传出一个reference,而 reference所指数据是左上、右下顶点,与Rectangle对象自身有关联,而且存储在Rectangle对象外的 Point对象,那么upperLeft和lowerRight的调用者可以修改reference所指的数据(Point对象)了。

    返回handles与返回references

    reference、指针、迭代器统统都是所谓handles(句柄,用来取得某个对象),而返回一个“代表对象内部数据”的handle,这样就会面临“降低对象封装性”的风险,同时可能导致“const成员函数却造成对象状态被修改”。

    通常,对象的“内部”是指它的成员变量,不过,不被公开使用的成员函数(private或protected),也是对象“内部”的一部分。因此,也要留心不要返回它们的handles。如果确实这么做了,它的实际访问级别就会提高,因为客户可以通过指针直接调用。

    如何解决返回reference,被客户修改的问题?

    可以对返回类型加上const,这样客户可以读取矩形的Points,但不能修改。也就意味着,客户不能改变对象状态。

    class Rectangle {
    public:
        ...
        const Point& upperLeft() const { return pData->ulhc; } // 返回const对象, 客户无法修改
        const Point& lowerRIght() const { return pData->urhc; } // 返回const对象, 客户无法修改
        ...
    };
    

    上述方案也不是完美的,存在空悬指针的问题。因为pData所指的对象可能会不存在,如果还使用该指针,就会产生“野指针”问题(内存已释放)。
    例如,boundingBox函数返回GUI对象的外框(bouding box),

    // 外框采用矩形形式
    // GUI对象
    class GUIObject { ... }; 
    // 以by value方式返回一个矩形
    const Rectangle boundingBox(const GUIObject& obj); // 条款3谈过为什么返回类型是const
    
    // 客户可能这样使用boundingBox函数
    GUIObject* pgo;
    ... // 设置pgo指向的GUI对象
    // 取得一个指针指向外框的左上角顶点
    const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft()); // 错误: boundingBox(*pgo)返回的是一个临时对象, 语句结束后释放对象, pUpperLeft成为空悬指针
    

    boundingBox(*pgo)返回的将是一个Rectangle临时对象,而在这行语句结束后,就会释放该临时对象。而pUpperLeft却指向了其内部pData所指向的ulhc对象(Point),该对象是会随临时对象的析构而释放的,因此pUpperLeft将成为空悬指针。

    虽然member函数封handle可能产生风险,但有时必须这么做,比如operator[],返回的就是对象的reference,指向内部数据。

    小结

    1)避免返回handle(包括reference、指针、迭代器)指向对象内部。遵守该条款可以增加封装性,帮助const member函数的行为像const,并将“空悬指针”的可能性降到最低。

    [======]

    条款30:透彻了解inlining的里里外外

    Understand the ins and outs of inlining.

    inline函数

    理念:将函数的每个调用,都以函数本体替换之。

    inline只是对编译器的一个申请,不是强制命令。申请分为两种方式:隐喻提出,明确提出。

    • 隐喻提出:将函数定义于class声明中。
    • 明确提出:在函数定义式前,加上关键字inline。

    大部分编译器拒绝将太过复杂(如带有循环或递归)的函数inlining(内联),而所有堆virtual函数的调用也都会使inlining落空,因为virtual意味着“等待,直到运行期才确定调用哪个函数”,而inline意味着“执行前,先将调用动作替换为被调用函数的本体”。
    编译器通常不对“通过函数指针进行的调用 ”实施inlining。

    程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库升级而升级。也就是说,如果f是程序库内的inline函数,客户将函数f编进程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。而如果f是non-inline函数,只需要重新链接就好;如果是动态链接,升级后的函数可以直接被使用。

    另外一个现实问题,大多数调速器对inline函数束手无策,因为无法为一个并不存在的函数设立断点。

    建议:一开始不要将任何函数声明为inline,或者实行有限范围内的函数称为inline。待到有优化需求时,再改成inline。

    小结

    1)将大多数inlining限制在小型、被频繁调用的函数身上。可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升计划最大化;
    2)不要只因为function templates出现在头文件,就将它们声明为inline;

    [======]

    条款31:将文件间的编译依存关系降到最低

    Minimize compilation dependencies between files.

    问题引出

    考虑C++ class:

    // Person定义
    /* 头文件 */
    #include <string>
    #include "date.h"
    #include "address.h"
    
    class Person {
    public:
        Person(const std::string& name, const Date& birthday, const Address& addr);
        std::string name() const;
        std::string birthDate() const;
        std::string address() const;
        ...
    private:
        std::string theName;
        Date theBirthDate;
        Address theAddress;
    };
    

    Person定义文件include了其他文件之间形成了一种编译依存关系(compliation dependency)。如果这些头文件中有一个被改变,或者这些头文件所依赖的其他头文件有改变,那么每个include Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。这样一连串的编译依存关系会对许多项目造成难以形容的灾难。

    那么,我们如何解决这个问题呢?

    方法一:Handle class

    可以把Person分割为2个class:一个只提供接口(Person),另一个负责实现该接口(PersonImpl)。
    持有实现接口(PersonImpl对象)指针的类Person,称为Handle class。

    // 接口那部分
    #include <string>
    #include <memory>
    
    /* 前置声明, 前提是不需要知道其定义 */
    // 声明式 (Person定义需要用到)
    class PersonImpl; // Person实现类前置声明
    class Date; // Person接口用到的class的前置声明
    class Address;
    
    // Person定义(式)
    class Person { // Handle class
    public:
        Person(const std::string& name, const Date& birthday, const Address& addr);
        std::string name() const; // string并不是class, 而是basic_string<char>的typedef
        std::string birthDate() const;
        std::string address() const;
        // ...
    private:
        // pimpl idiom (pointer to implementation)
        std::tr1::shared_ptr<PersonImpl> pImpl; // 智能指针, 指向实现类(PersonImpl)(shared_ptr见条款13)
    };
    

    分离的关键在于以“声明的依存性”替换“定义的依存性”,也就是分离 声明所需要的东西 跟 定义所需要的东西。这也是编译依存性最小化本质。现实中,让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(class XXX;)而非定义式(class XXX{ };)相依。其他每件事源于这个简单的设计策略:

    • 如果使用object references或object pointers可以完成任务,就不要使用objects
      因为如果定义某个类型的object,就要用到该类型的定义式。而reference和pointer只需要该类型的声明式即可。

    • 如果能够,尽量以class声明式替换class定义式
      也就是说,如果只是声明,可以不需要class定义式,用class声明式即可。而定义里面,能用class声明式则尽量用。

    • 为声明式和定义式提供不同的头文件
      如,声明式放到Person.h,实现式放到PersonImpl.h。

    方法二:Interface class

    令Person称为一种特殊的abstract base class(抽象基类),称为Interface class。类似于Java里面的interface,专门提供derived class的接口,因此不带成员变量,也没有构造函数,只有一个virtual析构函数 + 一组pure virtual函数(纯虚函数),用来描述整个接口。不过,C++并不禁止interface内实现成员变量或成员函数。

    // interface class 
    // 没有构造函数
    class Person {
    public:
        virtual ~Person(); // virtual析构函数
        virtual std::string name() const = 0; // pure virtual函数
        virtual std::string birthDate() const = 0;
        virtual std::string address() const = 0;
        // ...
        // factory函数, 创建新对象. 其实现不一定得在具象类中, 也可以在客户端
        static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr);
    };
    

    思考:为什么要有factory函数?
    答:因为Interface class的客户必须有办法为这种class创建新对象,他们通常调用一个特殊函数,此函数扮演那个derived class的构造函数的角色。客户持有的,通常是Interface class Person类指针,而非derived class,否则程序的设计不具有复用性。

    注意:factory函数不一定非得是在具象类RealPerson中实现,可以在专门的工厂中,或者客户端。关键在于,提供方法构建具象类,而绑定的指针却是接口类。

    如果有了factory函数(create),客户可以这样使用:

    string name;
    Date dateOfBirth;
    Address address;
    ...
    // 创建一个对象, 支持Person接口
    shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
    ...
    cout << pp->name()
         << " was born on "
         << " and now lives at "
         << pp->address();
    ...
    

    客户真正使用的,是实现Interface class接口的那个具象类(concrete class),因此该类必须被定义出来,而且构造函数必须被调用。
    注意:interface class并没有构造函数。

    具象类实现:

    // 实现interface class的具象类
    class RealPerson: public Person {
    public:
        RealPerson(const std::string&name, const Date& birthday, const Address& addr)
          : theName(name), theBirthDate(birthday), theAddress(addr)
        { }
        virtual ~RealPerson();
        std::string name() const;
        std::string birthDate() const;
        std::string address() const;
    private:
        std::string theName;
        Date theBirthDate;
        Address theAddress;
    };
    
    // 一个具体的factory函数实现
    shared_ptr<Person> Person::create(const string&name, const Date& birthday, const Address& addr)
    {
        return shared_ptr<Person>(new RealPerson(name, birthday, addr));
    }
    

    Handle class与interface class

    在handle class的方案中,handle class和实现类是依赖关系(use-a):handle class依赖于实现类,持有指向实现类指针;
    在interface class的方案中,interface class和实现类是实现关系:实现类实现了interface class的接口。

    什么时候考虑使用handle class和interface class?
    当想要实现代码变化时,对客户带来最小冲击,就应当考虑使用handle class和interface class解耦声明与实现,以使编译依存性最小。

    小结

    1)支持“编译依存性最小化”的一般构想:相依于声明,不要相依于定义式。基于此构想的2个手段是Handle class和interface class。
    2)程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论设计template都适用。

    [======]

  • 相关阅读:
    Office如何加密解密
    各个刷流量软件总结对比
    西游释厄传2游戏技巧
    三国战记合集模拟器下载和通关技巧
    [Webpack] Create Separate webpack Configs for Development and Production with webpack-merge
    [Algorithm] Reverse array of Chars by word
    [Angular] Angular Elements Intro
    [React + Functional Programming ADT] Create Redux Middleware to Dispatch Actions with the Async ADT
    [Algorithm] Fibonacci problem by using Dynamic programming
    [HTML5] Track First Contentful Paint with PerformanceObserver and Google Analytics
  • 原文地址:https://www.cnblogs.com/fortunely/p/15582417.html
Copyright © 2011-2022 走看看