4. 设计与声明

32 阅读4分钟

cpp中存在class接口、function接口以及template接口,对于接口的设计,要站在客户的角度,让接口易用且保证正确性、高效性、封装性、维护性、延展性以及一致性

小结

  1. 本章针对的是接口的设计与声明,这部分是写代码最耗费时间的
  2. 接口需要站在用户角度而言方便、高效使用,这里就涉及到条款18、19、20、21、25
  3. 在涉设计类时,注意封装性,条款22、23、24

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

  1. 首先要识别到哪些情况下容易被误用:
  • 传参顺序
  • 参数限制(如无效的月份)
  1. 解决方案:
  • 引入新类型,比如年月日不使用int,而是去单独定义类型,这样传入顺序就固定了,且可以通过函数(而不是non-local static变量,因为存在初始化顺序问题)来解决参数限制问题 image.png
  1. 容易被正确使用:
  • 一致性,如stl容器都提供size()
  • 对于引入的新类型,行为与内置types(如int)一致,如operator *返回的是const对象
  • 不能要求用户必须做某些事情,比如工厂函数返回裸指针,要求用户必须后续delete,应该直接返回智能指针

19 设计class犹如设计type

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

对于对象的copy,如果成员还有其他对象如string,则拷贝析构不止是多调用一次,内部成员也会调用拷贝析构

  1. 支持pass-by-value的是内置类型、stl迭代器以及函数对象
  2. 其他的都是reference更加高效,包括小对象

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

  1. 如果是栈对象,出作用域就析构了,肯定不能返回引用
  2. 如果是静态局部变量,如果返回引用,每次调用函数获取的返回其实是一个对象,如test() == test()永远为true
  3. 如果是堆对象,返回引用之后,需要用户delete,很容易就引发内存泄漏,比如连续调用两次w = x * y * z,这里x*y就会导致内存泄漏

22 将成员变量声明为private

  1. 好处:
  • 客户不需要纠结究竟是访问数据成员还是成员方法,而是同意访问方法,比如stl容器中的size()
  • 能对成员的读写权限更加精确的控制,提供相应的方法即可
  • 封装,比如对于速度收集器,需要平均速度时,可以选择存储在内存,也可以选择调用函数时计算后返回
  • 允诺约束条件获得保证,写入函数能先判断合理性
  1. protected并不比public更具封装性

23 宁以non-menber、non-friend替换menber函数

  1. 成员封装性可以与访问该成员的函数数量成反比,越多函数可访问它,数据的封装性就越低
  2. 如果成员不是private,则其毫无封装性
  3. 能访问private的,只有member函数以及friend函数

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

虽然隐式转换大多数情况下都应该被禁用,但是对于int而言,自动与转换成有理数等场景,是很有必要的

  1. 只有参数被列于参数列内,才是隐式类型转换的合格参与者,所以对于成员函数而言,this指针是无法隐式转换的

image.png

image.png 2. 让operator*成为一个non-member函数,也就允许编译器在每个实参身上执行隐式类型转换,至于其是否应该是friend,取决于函数内是否访问了private成员

image.png

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

  1. std::swap是用的栈对象+拷贝构造函数来实现的,会调用一次copy两次assignment函数,如果有指针,会复制指针指向的内容,效率低下,因为明明只需要交换两个指针值即可
  2. 为了提高swap速度
  • 在自定义types中写swap成员函数,并保证这个函数不会抛出异常(比如调用std::swap来交换内置类型,比如指针)

    // 成员 swap 函数(高效交换内部资源)
    void swap(MyClass& other) noexcept {
        std::swap(data, other.data); // 交换指针,O(1) 操作
    }
    
  • 在所处命名空间中写swap函数,调用swap成员变量

    cpp11之后,有了ADL规定:当调用一个函数时,编译器不仅会在当前作用域查找该函数,还会在函数参数所属的命名空间中查找。,cpp11之前,则需要涉及到模板特例化,比较麻烦,而且会侵入std空间

    // 非成员 swap,支持 ADL 
    void swap(MyClass& a, MyClass& b) noexcept { 
        a.swap(b); // 调用成员函数 
    }
    
  • 泛型代码中使用 using std::swap,并直接调用 swap

这里如果不使用using,则会直接调用std::swap,即使自定义了更高效的swap

```cpp
template<typename T>
void someAlgorithm(T& a, T& b) {
    using std::swap; // 引入 std::swap 作为后备
    swap(a, b);      // 
}
```