前言
大家好,我是雨墨,我深知好记性不如烂笔头的道理,所以在阅读的时候都尽量写读书笔记,方便日后复习,当然笔记并不能代替书籍上的内容。希望我的笔记也能帮助到大家,如果我的笔记有什么问题,欢迎大家给小老弟纠错~
条款十八总结
设计者应该把用户当作白痴,这样才能将问题考虑周到。
-
设计良好的接口,不容客户使用错误,比如年月日那就专门定义类来实现年月日,同时每个月由专门设计的函数来实现。
-
让 types 容易被使用,尽量让你的 types 的行为与内置类型保持一致性。
-
当你写一个工厂函数的时候,一定要留意,你的客户是个傻子,他会忘记最后 delete 这个指针的,因此建议返回智能指针,例如:
Investment* createInvestment(); // 改为 shared_ptr<Investment*> createInvestment();
-
如果你想给指针传递一个函数,而不是对这个指针动用 delete 这把刀子,那也请你显式声明出来:
shared_ptr<Investment*> createInvestment() { shared_ptr<Investment*> retVal(static_cast<Investment*>(0), getRidOfInvestment); // note! retVal = ...; return retVal; }
条款十九总结
设计一个 class 就像设计一个 type 一样,需要考虑许多问题,详见书 p84-p86。
条款二十总结
宁以 pass-by-reference-to-const 替换 pass-by-value,这句话一定要强调一下,下面阐述为什么要这么做:
-
pass-by-value 往往带着昂贵的拷贝构造操作、析构操作,而 pass-by-reference-to-const 没有任何构造函数或析构函数调用,因此没有任何新对象被创建。
-
pass-by-value 会造成对象切割的问题,详见代码
class Window { public: ... string name() const; virtual void display() const; } class WindowWithScrollBars : public Window { public: ... virtual void display() const; } void printNameAndDisplay(Window w) { cout << w.name(); w.display(); // 由于是 pass-by-value ,所以调用的永远是 Window::display() }
上述传递参数是 pass-by-value ,base class 的 ctor 就会被调用,所以 derived class 的特化性质全部被切割,仅仅留下一个 base class 对象。
-
窥视 C++ 编译器的底层,reference 往往是以指针实现出来的,所以 pass by reference 通常意味着真正传递的是指针。
-
尽管当前的 class 可能含有的成员非常小,但是编译器依旧会拒绝将其放入 cache 中,谁知道它将来会有多大,但是如果是 by reference 传递,编译器很乐意将其放入 cache 中。
条款二十一总结
不要试图返回 pointer 或 reference 指向一个 local stack 对象,或是指向 heap-allocated 对象,亦或是返回 reference 或 pointer 指向 local static 对象,这样会给你带来各种各样的麻烦,想象一下,你正在使用 reference 指向一个对象的残骸,或是忘记 delete 一个 heap-allocated 对象从而造成内存泄漏,亦或是带来多个一样结果。这些都在警告你不要使用 pass-by-reference 返回一个原来根本不存在的对象,而应该使用 pass-by-value。
条款二十二总结
将你的成员变量声明为 private !protected 也不行!
原因:
- 一致性,假设 public 全是接口,这样客户就不用记访问 class 成员需不需要加小括号
- 使用函数可以对成员变量的处理有更精确的控制
- 封装!将成员变量隐藏在函数接口背后,为 “所有可能的实现” 提供弹性,封装性与当其内容改变时造成的代码破坏量成反比,例如:将成员变量声明为 private ,造成的代码破坏量是最小的,因此它的封装性更好,如果是声明为 public ,那删除该 public 成员变量,所有使用它的用户代码都会被破坏,封装性极差,而声明为 protected ,你以为就没事了吗?使用它的 derived class 都会被破坏。
条款二十三总结
面向对象的守则:要求对象尽可能被封装,而不是数据和操作数据的函数应该被捆绑在一起。
对于封装的讨论:越多的对象被封装,对用户就越不可见,就能越自由的修改代码而只对少部分用户产生影响,这样弹性也就更大。
那这样才能导致较大的封装性呢?可以以数据的函数被调用的次数来粗略的评估数据的封装性如何。因为 non-member non-friend func 并不能直接访问 private 成员,所以它并不增加能够访问 private 成员的函数的数量。所以使用 non-member non-friend func 可以导致良好的封装性。
在 C++ 中,将这些 non-member non-friend 函数(便利函数)声明与 class 放在同一个 namespace 中,然后再根据这些便利函数的功能划分,定义在不同的头文件中。如果向扩展一组遍历函数,那就在 namespace 中加入函数的声明,再在对应头文件中给出函数的定义即可。
条款二十四总结
当你希望为某个函数的所有参数都进行类型转换(包含 this 所指的隐式参数),你必须将该函数声明为 non-member 。举例说明:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
const Rational operator* (const Rational& rhs) const; // 如果是这样声明,那么后续不能使用 result = 2 * oneHalf
private:
...
};
// 但如果是这样,那么就可以使用 reslut = 2 * oneHalf
const Rational operator* (const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator()),
lhs.denominator() * rhs.denominator()); // 注意,这里的 numerator() 你不觉得设计的也很棒吗?
};
无论何时你可以避免 friend 函数就该避免!
条款二十五总结
-
如果缺省实现的 swap 能够为你的 class 或 class template 提供可以接受的效率,那就直接用吧!
-
如果你觉得缺省实现的 swap 效率不太行,一般是因为调用缺省实现的 swap 会调用很多次构造函数以及拷贝复制,即你的 class 中或者 template 中使用了某种 pimpl 手法(pointer to implementation),那你就要考虑自己动手实现一个 swap 函数了:
-
在 public 中提供一个 swap 成员,这个函数绝不能抛出异常,因为 swap 是异常安全性编程的脊柱!
-
在 class 或 template 所在的 namespace 中提供一个 non-member swap ,调用上述的成员 swap,例如:
namespace WidgetStuff { class WidgetStuff { public: ... private: int a, b, c; std::vector<double> v; ... }; ... class Widget { public: ... void swap(Widget& other) { // 这个函数一定不能抛出异常 using std::swap; // 这个一定要!见下方解释 swap(pImpl, other.pImpl); } ... private: WidgetImpl* pImpl; }; //void Swap(Widget& lhs, Widget& rhs) { // lhs.swap(rhs); // 调用 swap 成员函数 //} } namespace std { template<> void swap(Widget& lhs, Widget& rhs) { lhs.swap(rhs); } }
-
如果你正在编写一个 class (而非 class template),为你的 class 特化 std::swap(见上方代码),并令其调用你的 swap 成员函数。因为 std::swap 是一个 func template , template func 不能被偏特化,如果你实在想要偏特化一个 func template ,作者给出的做法是,提供一个它的重载版本,例如:
namespace std { template<typename T> void swap<Widget<T>> (Widget<T>& a, // error!!!不可以对函数模板进行偏特化 Widget<T>& b) { a.swap(b); } } // 做法:重载 namespace std { template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
解释为什么要多家一个 using std::swap ?
这样做并非多余,因为编译器看到对 swap 的调用,会优先选择调用专属版本的 swap 版本,但如果是没有 T 类型专属的 swap ,编译器就使用 std::swap 。
-
参考书籍:
《Effective C++》