• 【读书笔记】【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*成员函数
      
      • 1
      • 2
      • 3
      • 4
    • 第一次调用(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特化,特化版本会被编译器挑中。
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
    • 本条款对 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 而言全新的东西。
  • 相关阅读:
    Vue框架(三)------quasar组件使用及文件上传
    呼叫中心和电话营销系统相关知识--中继线路
    Python运算符、函数与模块和程序控制结构
    常用的排序算法
    ORB-SLAM2 ---- Tracking::TrackWithMotionModel函数
    JSON 配置文件
    策略模式和观察者模式
    微服务框架 案例
    安装free IPA与CDH6.3.2结合
    磁盘有空间但无法创建文件
  • 原文地址:https://blog.csdn.net/weixin_44705592/article/details/126844658