zoukankan      html  css  js  c++  java
  • 【C++】《Effective C++》第四章

    第四章 设计与声明

    条款18:让接口容易被正确使用,不易被误用

    请记住

    • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达到这些性质。
    • "促进正确使用"的办法包括接口的一致性,以及与内置类型的行为兼容
    • "阻止误用"的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
    • shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes)等等。

    条款19:设计class犹如type

    C++中,当定义一个新的class,也就是定义了一个新type。
    那么,如果设计高效的classes呢?以下是需要面对的问题:

    • type的对象应该如何被创建和销毁?
      • 这会影响到你的class的构造函数和析构函数以及内存分配函数的释放函数(operator newoperator new []operator deleteoperator [] delete)等的设计,不过前提是如果你自定义它们。
    • 对象的初始化和对象的赋值该有什么样的差别?
      • 这会决定你的构造函数和赋值操作符的行为以及其间的差异。很重要的是别混淆了"初始化"和"赋值",因为它们对应于不同的函数调用。
    • type的对象如果被passed-by-value(以值传递),意味这什么?
      • 这会让你思考copy构造函数用来定义一个typepass-by-value该如何实现。
    • 什么是新type的"合法值"?
      • class的成员变量而言,通常只有某些数值集是有效的。那些数值集决定了你的class必须维护的约束条件,也就决定了你的成员函数必须进行的错误检查工作,它也影响函数抛出的异常、以及函数异常明细。
    • 你的新type需要配合某个继承体系吗?
      • 如果你的类继承自某些既有的类,那么你就受到那些classes的设计的束缚,特别是受到"它们的函数是virtual还是non-virtual"的影响。如果你的类允许其他classes继承,那会影响你所声明的函数,尤其是析构函数是否为virtual
    • 你的新type需要什么样的转换?
      • 如果你允许类型T1被隐式转换为T2,那么就必须在class T1内写一个类型转换函数(operator T2)或者在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。如果你允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不能为类型转换操作符或on-explicit-one-argument构造函数
    • 什么样的操作符和函数对此新type而言是合理的?
      • 这会决定你将为你的class声明哪些函数,其中某些该是member函数,某些则不是。
    • 什么样的标准函数应该被驳回?
      • 这会决定你必须声明为private的函数。
    • 谁该取用新type的成员?
      • 这会决定你哪个成员为public,哪个为protected,哪个为private。也会决定你哪一个classesfunctions应该是friends,以及将它们嵌套于另一个之内是否合理。
    • 什么是新type的"未声明接口"?
      • 这会决定你对效率、异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证?你在这些方面提供的保证将为你的class实现代码加上相应的约束条件。
    • 你的新type有多么一般化?
      • 或许你其实并非定义一个新type,而是定义一整个types家族。果真如此你就不该定义一个新class,而是应该定义一个新的class template
    • 你真的需要一个新type吗?
      • 如果只是定义新的派生类以便为既有的添加机能,那么说不定单纯定义一个或多个non-member函数或者templates更能达到目标。

    请记住

    • class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。

    条款20:宁以pass-by-reference-to-const替换pass-by-value

    缺省情况下C++by value方式传递对象至函数,传递过程中副本由对象的copy构造函数产出,这可能使得pass-by-value成为昂贵的操作。

    class Person {
    public:
        Person();
        virtual ~Person();
    private:
        std::string name;
        std::string address;
    };
    
    class Student: public Person {
    public:
        Student();
        ~Student();
    private:
        std::string schoolName;
        std::string schoolAddress;
    };
    
    // 使用
    bool validateStudent(Student s);
    Student plato;
    bool platoIsOk = validateStudent(plato);
    

    分析上述代码,以by value方式传递一个Student对象会导致调用一次Student copy构造函数、一次Person copy构造函数、四次string copy构造函数,并且当函数内的那个Student副本被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作,这就是昂贵的操作了。并且当参数接受一个基类对象但是传入一个子类对象时,传入的子类对象会被切割,只保有基类对象的部分,从而无法表现出多态。

    这两个问题,通常可以通过pass-by-reference-to-const解决。因为reference往往以指针实现出来,因此它通常意味真正传递的是指针。

    // 使用
    bool validateStudent(const Student &s);
    

    请记住

    • 尽量以pss-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
    • 以上规则并不适用与内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较恰当。但是不是所有小型对象都是pass-by-value的合格候选者。

    条款21:必须返回对象时,别妄想返回其reference

    必须返回对象的最常见是运算符函数:

    const Rational operator*(const Rational&lhs, const Rational&rhs);
    

    在必须返回对象时,不要企图放回reference,不然就会造成下面的情况:

    • 使用stack构造一个局部对象,返回局部函数的reference
    const Rational& operator*(const Rational&lhs, const Rational&rhs) {
        Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
        return result;
    }
    

    这样做的问题:使用reference的本意是避免构造新对象,但是一个新的对象result还是经由构造函数构造。更严重的是,这个局部对象在函数调用完成后就被销毁了,reference将指向一个被销毁的对象。

    • 使用heap构造一个局部对象,返回局部函数的reference
    const Rational& operator*(const Rational&lhs, const Rational&rhs) {
        Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
        return *result;
    }
    
    Rational w, x, y, z;
    w = x * y * z;
    

    这样做的问题:虽然不再引用一个被销毁的对象,但是多了动态内存分配的开销。而且,谁该为delete负责也成为问题。并且当多次动态分配内存时只返回最后一个的指针,这就造成了资源泄漏,比如上面的连乘操作。

    • 构造一个static局部对象,每次计算结果保存在这个对象中,返回其reference
    const Rational& operator*(const Rational&lhs, const Rational&rhs) {
        static Rational result;
        result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
        return result;
    }
    
    Rational w, x, y, z;
    if((w * x) == (y * z)) {
        // ...
    }
    
    

    这样做的问题:显而易见的问题是这个函数在多线程情况是不安全的,多个线程会修改相同的static对象。并且,在上面的判断语句中,不管传入的w, x, y, z是什么,由于operator*返回的reference都指向同一个static对象,因此上面的判断永远为真。

    请记住

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

    条款22:将成员变量声明为private

    为什么不能是public

    • 语法一致性:如果成员函数和成员变量一样,都是public,那么调用时会困惑于该不该使用括号,比如想获取大小时使用size,但是这到底是一个成员变量还是一个成员函数呢?
    • 更精确的访问控制:通过将成员变量声明为private,通过成员函数提供访问,可以实现更精确的访问控制。
    • 封装特性(主要):如果通过public暴露,在需要改成员变量的大量实现代码中,会直接使用当这个成员变量被修改或删除时,这样所有直接访问该成员的代码可能将会变得不可用。

    为什么不能是protected

    • 理由同上面的三个。

    请记住

    • 切记应该讲成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
    • protected并不public更具封装性。

    条款23:宁以non-member、non-friend替换member函数

    假设有个浏览器类,包含一些功能用来清除下载元素高速缓存区、清除访问过的历史记录、以及移除系统中所有的cookies

    class WebBrowser {
    public:
        // ...
        void clearCache();
        void clearCookies();
        void clearHistory();
        // ...
    };
    

    此时,如果想整个执行所有这些动作,那么有两个选择,一种实现成member函数,一种实现成non-member函数。

    class WebBrowser {
    public:
        // ...
        void clearCache();
        void clearCookies();
        void clearHistory();
    
        // 实现成member函数,可以访问private成员
        void clearEverything() {
            clearCache();
            clearCookies();
            clearHistory();
        }
        // ...
    };
    
    // 实现成non-member函数,不可以访问private成员
    void clearEverything(WebBrowser& wb) {
            wb.clearCache();
            wb.clearCookies();
            wb.clearHistory();
        }
    

    关于这两种选择的抉择在于封装性

    对于对象内的代码,越少代码可以看到数据(也就是访问它),越多的数据可被封装,也就越能自由地改变对象数据。作为一种粗糙的测量,越多的函数也可访问它,数据的封装性就越低。

    请记住

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

    条款24:如所有参数皆需类型转换,请为此采用non-member函数

    为类支持隐式类型转换不是个好主意,但是在数值类型之间颇为合理。考虑有理数和内置类型之间的相乘运算。

    具有如下有理数:

    class Rational {
    public:
        Rational(int n = 0, int d = 0); // 构造函数可以不为explicit,提供了int-to-Rational的隐式转换
    
        int numerator() const;  // 分子的访问函数
        int denominator() const; // 分母的访问函数
    private:
        // ...
    };
    

    现在提供了隐式转换方式,那么operator*应该实现成member还是non-member呢?

    class Rational {
    public:
        // member
        const Rational operator*(const Rational&rhs) const;
    }
    
    // non-member
    const Rational operator*(const Rational&lhs, const Rational&rhs);
    

    区别在于混合运算上,如果是member,那么下面的混合运算只有一半行得通:

    result = oneHalf * 2;   // 等价于oneHalf.operator*(2)  Success
    result = 2 * result;   // 等价于2.operator*(oneHalf)  Error
    

    因为内置类型没有相应的class,也就没有operator*成员函数,所以会错误。但是当实现为non-member时,具有两个参数,都能通过int-to-Rational,所以会正常。

    请记住

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

    条款25:考虑写出一个不抛异常的swap函数

    swap原先只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自赋值的常见机制,可以参考条款12

    • STL的swap实现
    namespace std{
        template<typename T>
        void swap(T& a,T& b){
            T temp(a);
            a=b;
            b=temp;
        }
    }
    

    只要类型T支持copying操作,上述实现就没问题,但是对于某些类型而言,其copying行为是不必要的,降低了程序运行的性能。

    • 最好的方案
    class Widget{
    public:
        void swap(Widget& other){
            using std::swap;    // 必须声明
            swap(pImpl, other.pImpl);
        }
    
    private:
        Widget *pImpl;
    };
    
    namespace std{
        template<>
        void swap<Widget>(Widget& a,Widget& b){
            a.swap(b);
        }
    }
    

    这种实现不仅高效还能与STL容器兼容,因为STL容器也提供了public swap成员函数与std::swap特例化版本。

    • template下的swap实现
    namespace WidgetStuff{
        template<typename T>
        class Widget { }
        template<typename T>
        void swap(Widget<T>& a,Widget<T> &b){
            a.swap(b);
        }
    }
    

    请记住

    • std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
    • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap
    • 调用swap时应该针对std::swap使用using声明式,然后调用swap并且不带任何"命名空间资格修饰"。
    • 为"用户定义类型"进行std templates全特化是好的,但不要尝试在std内加入某些对std而言全新的东西。
  • 相关阅读:
    状压DP【p1879】[USACO06NOV]玉米田Corn Fields
    Tarjan缩点+Spfa最长路【p3627】[APIO2009] 抢掠计划
    Tarjan缩点【p1726】上白泽慧音
    分层图【p4822】[BJWC2012]冻结
    Tarjan缩点+LCA【p2783】有机化学之神偶尔会做作弊
    线段树【p1607】[USACO09FEB]庙会班车Fair Shuttle
    better-scroll踩坑合集
    在浏览器上安装 Vue Devtools工具
    无法执行vue初始化命令
    vue-cli创建第一个项目(用git bash解决上下键移动选择问题)
  • 原文地址:https://www.cnblogs.com/parzulpan/p/13512257.html
Copyright © 2011-2022 走看看