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

  • 函数接口传参,使用者可能理解错误或不小心传错
    • 将函数参数封装为新的类型,比如封装新的年月日类,而非直接传入数字
  • 限制类型内可以进行的操作
    • 比如添加const限制,比如item3
  • 尽量使自定义类型和内置类型的行为保持一致
  • 消除使用者的资源管理责任
    • 比如直接返回一个智能指针,而非返回一个raw指针

19:设计class犹如设计type

设计类时,考虑的问题:

  • 如何创建以销毁:如何实现构造函数和析构函数
  • 初始化与赋值的区别:如何实现构造函数和赋值操作符
  • 类对象传值:如何实现拷贝构造函数
  • 成员类型的合法值:在构造函数和赋值操作中进行检查
  • 继承关系的约束:基类的相关虚函数、成员函数是否需要被声明为virtual
  • 是否允许由别的类转换而来:如何写转换构造函数
  • 哪些操作符和函数是合理的
  • 哪些操作符和函数应该拒绝
  • 成员给哪些用户使用:成员的访问控制权限
  • 新类型的未声明接口是什么
  • 是否有必要将类一般化为类模板
  • 这个新类型是否真的需要

20:传参时,尽量传常量引用而非传值

  • 优点:
    • 减少一次对象的复制
    • 避免对象切割(比如形参是基类,实参是派生类),同时实现多态
  • 使用传值的情况:内置类型,STL迭代器,函数对象

21:函数返回值尽量不要为引用

  • 禁止在函数中返回一个指向局部变量的指针或引用
  • 不要在函数中返回一个动态分配的对象
  • 不要在可能多次调用的函数中返回一个局部静态变量
  • 错误返回引用的例子:
    • 返回栈空间中局部变量的引用:函数返回后,栈上相应对象被销毁,因此未定义
    • 返回堆空间中局部变量的引用:虽然函数返回后不会释放对象,但是函数返回赋值的变量占有了堆空间的资源,而且极易容易忘记释放(因为一般也基本不会考虑对返回值进行delete),造成内存泄露
    • 返回静态变量的引用:当多次调用该函数返回静态变量的引用时,静态变量只有一个,例子
  • C++11中可以使用移动语义,减少拷贝带来的消耗

22:将成员变量声明为private

  • 将成员变量声明为public的缺点:
    • 缺乏语法一致性:访问public成员变量,可以直接访问或者调用成员函数
    • 对成员变量处理缺少准确控制:将成员变量设置为private的,可以提供setter/getter函数来控制其读写权限
    • 不利于封装:在成员变量发生变化时,可以在相关函数中通知其他变量,从而进行相应修改

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

功能颗粒度较高的函数设置为类外的函数,而非封装为public成员函数

  • 背景:public成员函数可分为两类:
    • 功能颗粒度较低的函数:public/protected成员函数,内部直接访问private成员
    • 功能颗粒度较高的函数:public/protected成员函数,内部由若干个public成员函数集成而来
  • 尽量将功能颗粒度较高的函数封装为类外的函数:
    • 优化类的封装性:如果封装为public函数,本来希望该函数只是public函数的集成,但是这样没法在代码层面体现出来
    • 允许我们从更多维度组织代码结构,提供更大的包裹弹性:比如将不同public成员函数封装为不同功能的外部函数
    • 优化编译依赖关系:比如不同的public成员函数可以封装为不同功能的外部函数,这些外部函数分别放在不同文件中,但是属于同一个命名空间中;这样使用时,需要哪个功能,就只需要包含该文件即可

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

如果希望运算符的任意操作数可以发生隐式类型转换,则应该将运算符重载为非成员函数(比如友元)

  • 背景:运算符可以重载,重载为成员函数呢,还是重载为非成员函数呢?
    • 规定:如果运算符是成员函数,则它的第一个运算对象不会发生隐式类型转换
      • 因为编译器根据第一个运算对象的类型,确定调用的运算符是属于哪一类的
      • 比如:z = x * y等价于z = x.operator*(y),x不会发生隐式类型转换

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

  • 如果Widge是一个类,可以在std命名空间中实现std::swap<T>的Widge全特化版本,同时在Widge中实现类内的swap函数以修改private成员的值
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    class Widget{
    public:
        void swap(Widget& other){ // member swap
            using std::swap; // default swap
            // 调用std::swap进行private成员的处理
        }
    }
    namespace std{
        template<> // std::swap特例化版本
        void swap<Widge> (Widget& a, Widget& b) { a.swap(b);}
    }
    
  • 如果Widge是一个类模板
    • 不能偏特例化一个函数模板
      1
      2
      3
      4
      
      namespace std{
          template<typename T> // non-member swap
          void swap<Widget<T>> (Widget<T>& a, Widget<T>& b) {a.swap(b);} // 编译报错
      }
      
    • 但是可以偏特例化一个类模板,添加一个重载版本
      • 但是不要在std命名空间中添加新东西
      1
      2
      3
      4
      
      namespace std{
          template<typename T>
          void swap(Widget<T>& a, Widget<T>& b) {a.swap(b);}
      }
      
    • 解决方法:置于一个新的命名空间中
      1
      2
      3
      4
      5
      6
      7
      
      namespace WidgetStuff{
          template<typename T>
          class Widget{ ... };
      
          template<typename T>
          void swap(Widget<T>& a, Widget<T>& b) {a.swap(b);}
      }
      
  • 因此,如果想使得Widget专属版swap在尽可能多的语境下被调用,需要
    • 在Widget中提供一个public swap函数(不可抛出异常),内部调用std::swap
    • 同时可能需要同时实现两个版本:
      • Widget所在命名空间WidgetStuff中,实现一个nom-member swap,内部调用Widget::swap
      • 如果Widget是一个类而非类模板,在std中特化std::swap,内部调用Widget::swap
  • C++11之后,std::swap改用std::move实现,所以几乎不存在性能缺陷