【读书笔记】【Effective C++】设计与声明

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

  • 好的接口应该容易使用,不易被勿用,在设计接口时应该努力达到这点。
  • 要保持接口的一致性,以及与内置类型的行为相同。
  • 阻止误用的措施有:
    • 建立新类型(例如给同为 int 的多个参数分别外覆一个类型,加以区分);
    • 限制类型上的操作(用 get 函数调用取代直接取参数);
    • 束缚对象值;
    • 消除客户的资源管理责任(存入智能指针中)。
  • trl::shared_ptr 支持定制性删除器,这能解决 cross-DLL 问题,即对象在一个动态链接库(DLL)中被 new 创建,又在另一个 DLL 中被 delete。【多使用 shared_ptr

条款 19:设计 class 犹如设计 type

  • 设计一个 class 应该考虑以下问题:
    1. class 如何被创建和销毁:构造函数和析构函数以及重载 new、delete 的设计。
    2. 对象的初始化和对象的赋值之间的差别:构造函数和赋值操作符的行为以及差异,重点是区分初始化和赋值行为。
    3. 对象以 pass-by-value 意味着什么:意味着拷贝构造函数是传值。
    4. 什么是新 type 的合法值:即用特定的标志来维护对象的正确性(约束),这决定了错误检查机制,影响函数抛出的异常。
    5. 新 type 需要配合某个继承图系吗:是否为基类或子类,这会影响虚函数机制。
    6. 新 type 需要什么样的转换:考虑添加类型转换函数。
    7. 什么样的操作符和函数对该类是合理的:考虑添加多少成员函数。
    8. 什么样的标准函数应该驳回:即哪些部分必须设置为 private。
    9. 谁该取用新 type 的成员:即如何区分 public 和 protected 以及 private 权限。
    10. 新 type 的未声明接口:对效率、异常安全性以及资源运用的保证。
    11. 新 type 的一般性:考虑是否写成模板类。
    12. 是否真的需要一个新 type:用别的手法代替(函数、模板)。

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

  • 默认情况下,C++ 采用 pass-by-value 来进行函数参数的传递。
  • pass-by-value 由于有对象的赋值以及构造函数、析构函数的调用,会造成内存的增大,效率的下降,同时在将派生类作为基类函数参数传递时,会造成派生类数据截断。(之所以会造成 slicing 问题,是因为传值的时候被视为 base 类,所以传指针才能保证多态性质)
  • 本条建议不适用于内置类型、STL 迭代器和函数对象,它们一般采用 pass-by-value 的写法,因为相比于传引用(底层实现就是指针),传值效率更快。

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

  • 如果在函数内定义对象,即在 stack 上创建对象,然后再返回改对象的引用,这样可以避免返回参数时候的构造开销,但问题是随着函数的执行完成,对象的生命周期结束,对象也就自动销毁了,返回的引用也就指向了一个销毁了的对象,会产生不明确行为。
  • 如果在 heap 上创建了对象,而我们选择返回指针,这样一种策略会导致 new 和 delete 分属不同的使用者,极容易导致内存泄露。
  • 定义静态对象,返回引用,这样一种策略也会导致隐患。
    • 比如说一个重载乘法的操作符返回静态对象的引用,当执行下面语句时 if(a*b==c*d) 就会产生不符合预期的效果。
    • 这里是因为重载乘法操作符,返回了引用,而引用的其实函数内的 static 变量,意思就是无论 ab 的乘积,还是 cd 的乘积,最后都是返回函数内同一个 static 变量的结果,所以判断只能为 true
    • 其实对于多线程也会产生不符合预期的结果。
  • 采用返回对象的方式会有对象构造的开销,但相对于上述的不安全行为,这样一种开销是值得的。

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

  • 把成员变量声明为 private,再设置 getset 函数来获取、设置该成员变量。
    • 好处就是加强封装,只使用接口,不直接改变变量。
    • 比如程序员对一个 public 成员变量进行改变时,会影响到所用使用该类的客服端代码;对于 protected 成员变量,会影响到所有派生类代码。
    • protected 并不比 public 更具封装性。

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

  • 如果用 non-member、non-friend 函数作为接口,那么直接接触 private 变量的机会会越来越少。
  • 通过使用 namespace 可以降低编译依赖以及功能进行扩充。

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

  • 假设在 Rational 类中重载了乘法操作符,且其中一个参数是同类;且 Rational 构造函数不添加 explicit,即允许 int 转 Rational
    Rational A;
    //如果Rational的构造函数是explicit的,则下面两个式子都会错误
    result = A * 2;// 是允许的,因为2是int,会转成Rational
    result = 2 * A;// 是错误的,因为2是没有相应的class,没有operator*成员函数
    
  • 第一次调用(A*2)的时候,int 在 Rational 构造函数的参数类型内,所以成功转换。
  • 而想要让乘法操作符针对 Rational 能实现混合运算(即 A*2=2*A),则需要将成员函数中的 operator* 改成非成员函数。
  • 改成 const Rational operator*(Rational& A, Rational& B) 即可。
    • 这样无论乘法运算符左右哪一个是 int,都能实现隐式转换。

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

  • swap 函数原本只是 STL 的一部分,而后成为异常安全性编程(exception-safe programming)的脊柱,以及用来处理自我赋值可能性的一个常见机制。

  • 通过代码进行分析 swap 函数实现的不同:

    // 一、最基础的swap函数(默认)
    namespace std  {
        template<typename T>    // std::swap的典型实现
        void swap( T& a, T& b )    // 置换a和b
        {
            // 只要类型T支持copying,swap实现函数就会交换swap
            // 但是进行了很多复制,整个swap太慢了
            T temp(a);
            a = b;
            b = temp;
        }
    }
    
    // 二、pimpl手法(pointer to implementation的缩写)
      // 以指针指向一个对象,内含真正数据。
    class WidgetImpl  {    // 针对Widget数据而设计的class
    public:
        ...
    private:
        int a,b,c;
        std::vector<double> v;    // 真正的数据有vector数组,意味着复制时间更长
        ...
    };
    class Widget  {    // 这个class使用pimpl手法
    public:
        Widget( const Widget& rhs );
        Widget& operator=( const Widget& rhs )    // 复制Widget时,令它复制其WidgetImpl对象
        {
            ...
            *pImpl = *(rhs.pImpl);
            ...
        }
        ...
    private:
        WidgetImpl* pImpl;// 注意是private的
    };
      // 当Widgets被置换时,真正该做的是置换其内部的pImpl指针。
      // 所以需要将std::swap对Widgets对象进行特化:
    namespace std  {
        template<>
        void swap<Widget>( Widget& a, Widget& b )
        {
            swap(a.pImpl,b.pImpl);    // 只要置换它们指针即可
        }
    }
      // 但目前这个全特化版本是无法通过编译的,因为它试图调用private的成员函数
      // 所以有两个解决方法:
        // 可以将这个特化版本声明为friend;
        // 可以令Widget声明为一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数:
    class Widget  {    // 与之前相同,唯一差别是增加swap函数
    public:
        ...
        void swap( Widget& other )
        {
            using std::swap;    // 这个声明很必要
            swap(pImpl,other.pImpl);    // 若要置换Widgets就置换其pImpl指针
        }
        ...
    };
      
    namespace std  {
        template<>    // 修订后的std::swap特化版本
        void swap<Widget>( Widget& a,Widget& b )
        {
            a.swap(b);
        }
    }
    
    // 三、还有一种情况:假设Widget和WidgetImpl 都是class templates 而非 classes
      // 我们可以试试将WidgetImpl 内的数据类型加以参数化:
    template<typename T>
    class WidgetImpl  {  ...  };
    template<typename T>
    class Widget  {  ...  };
      // 在Widget内(以及WidgetImpl内,如果需要的话)放个swap成员函数就像以往一样简单
      // 但我们却在特化std::swap时遇上乱流,因为C++只允许对class template进行偏特化:
    namespace std  {
        template<typename T>
        void swap<Widget T>(Widget<T>& a,Widget<T>& b)    // 可惜,不合法,是错误的
        {  a.swap(b);  }
    }
      // 因此我们只能考虑重载function template,但这又有新的问题,
      // 客户可以全特化std内的templates,
      // 但不可以添加新的templates(或classes或functions或其他任何东西)到std里头。
    namespace std  {
        template<typename T>
        void swap(Widget<T>& a,Widget<T>& b)
        {  a.swap(b);  }
    }
    
    // 四、在这个情况下,我们再次考虑non-member函数
      // 假设Widget的所有相关机能都被置于命名空间WidgetStuff内:
    namespace WidgetStuff  {
        ...
        template<typename T>
        class Widget  {  ...  };
        ...
        template<typename T>
        void swap( Widget<T>& a,Widget<T>& b )    // 这里并不属于std命名空间
        {
            a.swap(b);
        }
    }
      // 补充说明:
      // C++的名称查找法则(name lookup rules )确保将找到global作用域或T所在之命名空间内的任何T专属的swap。
      // 如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”找出WidgetStuff内的swap。
      // 如果没有T专属之swap存在,编译器就会使用std内的swap,这需要感谢using声明式让std::swap在函数内曝光。
      // 然而即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,
      // 所以如果你已针对T将std::swap特化,特化版本会被编译器挑中。
    
  • 本条款对 default swap、member swap、non-member swapstd::swap 特化版本以及对 swap 的调用进行了讨论。

    • 如果 swap 的缺省实现码对你的 class 或 class template 提供可接受的效率,你不需要额外做任何事。【任何尝试置换那种对象的人都会取得缺省版本,而那将有良好的运作】
    • 如果 swap 缺省实现版的效率不足(那几乎总是意味你的 class 或 template 使用了某种 pimpl 手法),试着做下面这些事:
      1. 提供一个 public swap 成员函数,让它高效地置换你的类型的两个对象值。
      2. 在你的 class 或 template 所在的命名空间内提供一个 non-member swap,并令它调用上述 swap 成员函数。
      3. 如果你正编写一个 class(而非 class template),为你的 class 特化 std::swap,并令它调用你的 swap 成员函数。
    • 如果你调用 swap,请确定包含一个 using 声明式,以便让 std::swap 在你的函数内曝光可见,然后不加任何 namespace 修饰符,赤裸裸的调用 swap
    • 还有一点需要强调:成员版 swap 决不可抛出异常。
  • 总结:

    • std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
    • 如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者;对于 classes(而非 templates),也请特化 std::swap
    • 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何命名空间资格修饰。
    • 为用户定义类型进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值