条款 18:让接口容易正确使用,不易被误用
- 好的接口应该容易使用,不易被勿用,在设计接口时应该努力达到这点。
- 要保持接口的一致性,以及与内置类型的行为相同。
- 阻止误用的措施有:
- 建立新类型(例如给同为 int 的多个参数分别外覆一个类型,加以区分);
- 限制类型上的操作(用
get
函数调用取代直接取参数); - 束缚对象值;
- 消除客户的资源管理责任(存入智能指针中)。
trl::shared_ptr
支持定制性删除器,这能解决 cross-DLL 问题,即对象在一个动态链接库(DLL)中被 new 创建,又在另一个 DLL 中被 delete。【多使用shared_ptr
】
条款 19:设计 class 犹如设计 type
- 设计一个 class 应该考虑以下问题:
- class 如何被创建和销毁:构造函数和析构函数以及重载 new、delete 的设计。
- 对象的初始化和对象的赋值之间的差别:构造函数和赋值操作符的行为以及差异,重点是区分初始化和赋值行为。
- 对象以 pass-by-value 意味着什么:意味着拷贝构造函数是传值。
- 什么是新 type 的合法值:即用特定的标志来维护对象的正确性(约束),这决定了错误检查机制,影响函数抛出的异常。
- 新 type 需要配合某个继承图系吗:是否为基类或子类,这会影响虚函数机制。
- 新 type 需要什么样的转换:考虑添加类型转换函数。
- 什么样的操作符和函数对该类是合理的:考虑添加多少成员函数。
- 什么样的标准函数应该驳回:即哪些部分必须设置为 private。
- 谁该取用新 type 的成员:即如何区分 public 和 protected 以及 private 权限。
- 新 type 的未声明接口:对效率、异常安全性以及资源运用的保证。
- 新 type 的一般性:考虑是否写成模板类。
- 是否真的需要一个新 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 变量,意思就是无论
a
、b
的乘积,还是c
、d
的乘积,最后都是返回函数内同一个 static 变量的结果,所以判断只能为true
。 - 其实对于多线程也会产生不符合预期的结果。
- 比如说一个重载乘法的操作符返回静态对象的引用,当执行下面语句时
- 采用返回对象的方式会有对象构造的开销,但相对于上述的不安全行为,这样一种开销是值得的。
条款 22:将成员变量声明为 private
- 把成员变量声明为 private,再设置
get
、set
函数来获取、设置该成员变量。- 好处就是加强封装,只使用接口,不直接改变变量。
- 比如程序员对一个 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
、memberswap
、non-memberswap
、std::swap
特化版本以及对swap
的调用进行了讨论。- 如果
swap
的缺省实现码对你的 class 或 class template 提供可接受的效率,你不需要额外做任何事。【任何尝试置换那种对象的人都会取得缺省版本,而那将有良好的运作】 - 如果
swap
缺省实现版的效率不足(那几乎总是意味你的 class 或 template 使用了某种 pimpl 手法),试着做下面这些事:- 提供一个 public
swap
成员函数,让它高效地置换你的类型的两个对象值。 - 在你的 class 或 template 所在的命名空间内提供一个 non-member swap,并令它调用上述
swap
成员函数。 - 如果你正编写一个 class(而非 class template),为你的 class 特化
std::swap
,并令它调用你的swap
成员函数。
- 提供一个 public
- 如果你调用
swap
,请确定包含一个 using 声明式,以便让std::swap
在你的函数内曝光可见,然后不加任何 namespace 修饰符,赤裸裸的调用swap
。 - 还有一点需要强调:成员版
swap
决不可抛出异常。
- 如果
-
总结:
- 当
std::swap
对你的类型效率不高时,提供一个swap
成员函数,并确定这个函数不抛出异常。 - 如果你提供一个 member
swap
,也该提供一个 non-memberswap
用来调用前者;对于 classes(而非 templates),也请特化std::swap
。 - 调用
swap
时应针对std::swap
使用 using 声明式,然后调用swap
并且不带任何命名空间资格修饰。 - 为用户定义类型进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
- 当