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
- Widget所在命名空间WidgetStuff中,实现一个nom-member swap,内部调用
- 在Widget中提供一个public swap函数(不可抛出异常),内部调用
- C++11之后,
std::swap
改用std::move
实现,所以几乎不存在性能缺陷