• 《Effective C++》第5章 实现-读书笔记


    章节回顾:

    《Effective C++》第1章 让自己习惯C++-读书笔记

    《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

    《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

    《Effective C++》第3章 资源管理(1)-读书笔记

    《Effective C++》第3章 资源管理(2)-读书笔记

    《Effective C++》第4章 设计与声明(1)-读书笔记

    《Effective C++》第4章 设计与声明(2)-读书笔记

    《Effective C++》第5章 实现-读书笔记

    《Effective C++》第8章 定制new和delete-读书笔记


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

    你定义了一个类类型的变量,那么就要耗费一个构造函数和析构函数。如果你最终不使用这个变量,就应该避免这些耗费。

    你可能会怀疑:怎么可能定义一个变量而不去使用呢?考虑下面的代码:

    std::string encryptPassword(const std::string& password)
    {
        using namespace std;
        string encrypted;
    
        if (password.length() < MinimumPasswordLength) 
        {
            throw logic_error("Password is too short")
        }
        
        ...
        return encrypted;
    }

    先不去考虑代码具体含义。如果if语句为true,就会抛出异常,这个encrypted对象仍然需要耗费一个构造函数和一个析构函数。所以最好延后encrypted的定义式,直到确实需要它。

    std::string encryptPassword(const std::string& password)
    {
        using namespace std;
        
        if (password.length() < MinimumPasswordLength) 
        {
            throw logic_error("Password is too short")
        }
        
        string encrypted;            //放在了后面
        ...
        return encrypted;
    }

    这段代码不够秾纤合度(不懂这个词)。因为encrypted对象调用的是默认构造函数,后面几乎一定会对它重新赋值。举例如下:

    void encrypt(std::string& s);
    std::string encryptPassword(const std::string& password)
    {
        ...
        string encrypted;            //放在了后面,考虑到使用时才定义
        
        encrypted = password;        //重新赋值
    
        encrypt(encrypted);
    
        return encrypted;
    }

    更好的做法是跳过无意义的default构造函数:

    std::string encryptPassword(const std::string& password)
    {
        ...
        string encrypted(password);            //拷贝构造函数
    
        encrypt(encrypted);
    
        return encrypted;
    }

    所以,“尽可能延后”的真正意义是:你不仅要尽可能延后变量的定义直到要使用它,还应该延后这个变量的定义直到给它初值。这样可以避免没有必要的构造和析构对象以及没有意义的default构造函数。

    还有一种情形出现在循环里面,分下下面两种做法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的某个值;    
    }

    做法A的成本:1个构造函数、1个析构函数和n个赋值;做法B的成本:n个构造函数和n个析构函数。

    如果赋值成本低于1个构造+1个析构,则做法A效率高一点,否则B的做法好。另外做法A造成Widget对象作用域扩大。所以,给出的建议是:除非你明确知道几个操作的成本,否则做法B是比较好的。

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


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

    C风格的转型(旧式转型)如下:

    (T) expression;        //两者含义相同
    T(expression)

    C++提供的4种新式转型如下:

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

    一般来说新式转型比较好。可能旧式转型比较常用的地方是调用explicit构造函数传递一个对象给函数时。举例如下:

    class Widget
    {
    public:
        explicit Widget(int size);
    };
    void doSomeWork(const Widget& w);
    
    doSomeWork(Widget(15));                    //C的函数风格
    
    doSomeWork(static_cast<Widget>(15));    //C++新风格

    任何一个类型转换,无论是通过转型操作进行的显式转换或通过编译器进行的隐式转换,往往会导致编译器产生运行期执行的代码。

    下面有个转型代码比较有迷惑:

    class Window
    {
    public:
        Window(int n = 0) : m(n) {}
    
        virtual void onResize()
        {
            m = 10;
        }
    
        int m;
    };
    
    class SpecialWindow : public Window
    {
    public:
        virtual void onResize()
        {
            static_cast<Window>(*this).onResize();
        }
    };
    
    int main()
    {
        SpecialWindow w1;
        cout << w1.m << endl;        //输出0
    
        w1.onResize();
        cout << w1.m << endl;        //输出0
    
        return 0;
    }

    两份输出都是0。不要怀疑,static_cast<Window>(*this).onResize();确实调用了class Window的onResize()函数,但关键是转型的结果是(*this)的一个副本,而不是对象本身。

    如果你仍然需要调用class Window版本的onResize()函数,就要拿掉转型。

    class SpecialWindow : public Window
    {
    public:
        virtual void onResize()
        {
            Window::onResize();
        }
    };

    dynamic_cast的成本很高,之所以需要它的一个原因是:在一个你认定为derived class对象身上执行derived class函数,但你只有一个指向base的指针或引用。

    请记住:

    (1)如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

    (2)如果转型是必要的,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需将转型放在自己的代码内。

    (3)优先使用C++风格的转型,因为它很容易被辨识出来并且有分类。


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

    handles包括指针、引用和迭代器。直接用例子说明:

    class Point                    //表示一个“点”
    {
    public:
        Point(int x, int y);
    ...
        void setX(int newVal);
        void setY(int newVal);
    }
    
    struct RectData
    {
        Point ulhc;            //表示左上角坐标
        Point lrhc;            //表示右下角坐标
    };
    
    class Rectangle
    {
    public:
        Point& upperLeft() const { return pData->ulhc; }            //返回左上角坐标
        Point& lowerRight() const { return pData->lrhc; }            //返回右下角坐标
    };

    Rectangle类设计两个成员函数upperLeft(),lowerRight()返回左上角和右下角坐标是必要的。但这两个函数都是const的,说明它的目的只是给用户查看,并不是让用户去修改这些坐标。但是客户这样做:

    Point coord1(0, 0);
    Point coord2(100, 100);
    
    const Rectangle rec(coord1, coord2);
    rec.upperLeft().setX(50);                //左上角坐标变为(50,0)

    确实改变了坐标值,尽管point还是private数据。这给我们的启示是:成员变量的封装性最多只等于返回其reference函数的访问级别。虽然point是private的,但实际效果却是public的。

    修改版本也很简单:

    class Rectangle
    {
    public:
        const Point& upperLeft() const { return pData->ulhc; }            //返回左上角坐标的const
        const Point& lowerRight() const { return pData->lrhc; }            //返回右下角坐标的const
    };

    另外一点handles指向的东西返回后可能不再存在。举例说明:

    class GUIObject { ... };
    const Rectangle boundingBox(const GUIObject& obj);
    
    //客户如下这样调用
    GUIObject *pgo;
    const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());

    boundingBox(*pgo)的调用将产生一个临时的Rectangle对象,在此对象上调用upperLeft()返回的是临时对象的左上角坐标,然后这个临时对象析构,这样pUpperLeft就是指向一个不存在的东西。

    请记住:避免返回handles(包括引用、指针和迭代器)指向内部对象。遵守这个条款可增加封装性、帮助const成员函数的行为像个const,并降低发生handles指向不存在东西的可能性。

  • 相关阅读:
    牛客小白月赛2 D 虚虚实实 【欧拉图】【连通图】
    牛客小白月赛2 A 数字方阵【随机】【找规律】
    牛客小白月赛1 J おみやげをまらいました 【MAP】
    牛客小白月赛1 I あなたの蛙が帰っています 【卡特兰数】
    欧拉函数
    乘法逆元
    扩展欧几里得
    快速乘法
    JPEG图像压缩出现资源不足问题的解决
    如何避免关键程序被意外关闭?
  • 原文地址:https://www.cnblogs.com/mengwang024/p/4456795.html
Copyright © 2020-2023  润新知