effective C++ 3rd 笔记(二)
条款13: 以对象管理资源
1.为防止资源泄漏,使用RAII对象,它们在构造函数中获得资源, 在析构函数中释放资源
2.两个常用的RAII classes: tr1::shared_ptr和auto_ptr,后者复制会使被复制物指向null
RAII (Resource Acquisition is Initialization): 资源取得时机便是初始化时机------以对象管理资源。
auto_ptr:不能让多个auto_ptr指向相同对象,否则对象会删除多次,未定义。 因此,若通过copy构造函数或copy assignment操作符复制它们,它们会变成Null, 而复制所得的指针将取得资源的唯一所有权。
Investment* createInvestment(); void f() { std::auto_ptr<Investment> pInv1(createInvestment()); std::auto_ptr<Investment> pInv2(pInv1);//pInv2指向对象,pInv1为null pInv1 = pInv2;//pInv1指向对象,pInv2为null }
因为STL容器要求元素有“正常的”的复制行为,所以这些容器容不得auto_ptr.
auto_ptr替代方案: RCSP (reference-counting smart pointer)引用计数型智慧指针。追踪共有多少对象指向某资源,无人指向时自动删除该资源。无法打破环状引用。
tr1::shared_ptr: 是个RCSP,复制行为正常,所以可用于STL容器
Investment* createInvestment(); void f() { std::tr1::shared_ptr<Investment> pInv1(createInvestment()); std::tr1::shared_ptr<Investment> pInv2(pInv1);//pInv1和pInv2都指向同一对象 pInv1 = pInv2;//pInv1和pInv2都指向同一对象 }
auto_ptr和tr1::shared_ptr在析构函数中做delete而不是delete[]动作。因此在动态分配数组上用auto_ptr和tr1_share_ptr是个错误,但编译仍能通过。
//都会用上错误的delete形式 atd::auto_ptr<std::string> aps(new std::string[10]); std::tr1::shared_ptr<int> sp1(new int[1024]);
vecotr和string几乎可以取代动态分配的数组,boost::scoped_array和boost::shared_array也提供需要的行为。
条款14: 在资源管理类中小学copying行为
1.禁制复制:条款06:copying操作声明为private,private继承Uncopyable类
2.引用计数: r1::shared_ptr允许指定删除器(函数或函数对象),当引用次数为0时调用,是可有可无的第二参数
class Lock { public: explicit Lock(Mutex* pm) :mutexPtr(pm, unlock) //以unlock为删除器 { lock(mutexPtr.get()); }//不要声明析构函数,默认析构函数自动调用其他non-static成员变量的析构函数 private: std::tr1::shared_ptr<Mutex> mutexPtr; };
条款15: 在资源管理类中提供对原始资源的访问
auto_ptr和tr1::shared_ptr都提供一个get成员函数,来执行显示转换,返回内部的原始指针。并且都重载了operator->和operator*操作符
也可以提供从智能带原始指针的隐式转换, operator orig_ptr_type() const { return orig_ptr; }
条款16: 成对使用new 和 delete时要采取相同形式
如果在new表达式中使用[],必须在相应的delete表达式中也使用[];如果在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
在class内有多个构造函数,并动态分配内存时,必须小心所有构造函数中使用相同形式的new将指针初始化。
尽量不要对数组做typedef操作。 使用vector,string等templates.
因为单一对象的内存布局一般而言不同于数组。多数编译器对数组的内存布局通常含有数组大小的记录,使用[]可让编译器知道是否有个数组大小的记录。
条款17: 以独立语句将newed对象置入智能指针
int priority(); void processWidget(std::tr1::shared_ptr<Widget> pw, int priority); processWidget(new Widget,priority());//不能通过编译 processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());//能通过编译,但异常不安全,若priority()在new和shared_ptr之间抛出异常 std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());// OK
条款18: 让接口容易被正确使用,不易被误用
1.保持接口与内置类型的行为一致性。
2.tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范cross-DLL problem,可被用来自动解锁互斥锁。
class Date { public: Date(int month, int day, int year); }; Date d(30,3,1995); // //导入外覆类型(wrapper types)来区别年月日 struct Day { explicit Day(int d):val(d) {} int val; }; struct Month { explicit Month(int m):val(m) {} int val; }; struct Year { explicit Year(int y):val(y) {} int val; }; class Date { public: Date(const Month& m, const Day& d, const Year& y); }; Date d(30,3,1995); //error Date d(Month(3), Day(30), Year(1995)); //OK,类型正确 class Month { public: static Month Jan() {return Month(1);} static Month Feb() {return Feb(2);} ... static Month Dec() {return Month(12);} private: explicit Month(int m); int month; }; Date d(Month::Mar(), Day(30), Year(1995));
令factory函数返回智能指针
std::tr1::shared_ptr<Investment> createInvestment(); std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvetment); //编译失败 std::tr1::shared_ptr<Investment> pInv(static_cats<Investment*>(0), getRidOfInvetment);//OK
cross-DLL problem: 对象在DLL中被new,在另一个DLL中被delete,导致运行期错误, tr1::shared_ptr没有这个问题
条款19: 设计class犹如设计type
1.新type对象如何被创建和销毁: 构造函数,析构函数,内存分配和释放operator new, operator delete,operator new[], operator delete[]
2.对象初始化和赋值的差别。构造函数(引用和const成员),赋值操作符
3.新type对象被passed by value意味什么,copy构造函数
4.什么是新type的合法值
5.新type需要配合某个继承图系吗?约束,virtual析构
6.新type需要什么转换。non-explicit构造函数, operator T2
7.什么样的操作符和函数对新type合理: member函数
8.什么样的标准函数应该驳回:需要声明为private
9.谁该取用新type成员:public protected? private? friend? 嵌套?
10.什么是新type的“未声明接口”?它对效率,异常安全,资源运用提供何种保证
11.新type有多一般化: 还是应该定义class template?
12.真的需要一个新type吗?如果只是定义新的derived class添加新功能,说不得定义一或多个non-menber函数或template更好? 条款23.
条款20: 宁以pass-by-reference-to-const替换pass-by-value
除了内置类型,STL迭代器和函数对象外,其他任何东西都尽量遵守本条款。
pass-by-value的代价: 形参的copy构造,形参基类(如果有)的copy构造函数,形参基类所有成员的copy构造函数,形参所有成员的copy构造函数, 以及所有构造函数对应的析构行为。
by-reference也可以避免对象切割(slicing)问题: 派生类对象by-value传给基类。
条款21: 必须返回对象时,别妄想返回其reference
不要返回指针或引用指向一个local stack对象,或返回引用指向一个heap-allocated对象,或返回指针或引用指向一个local stack的对象而有可能同时需要多个这样的对象。
class Rational { public: Rational(int numerator = 0; int denominator = 1); private: int n, d; friend const Rational operator* (const Rational& lhs, const Rational& rhs); }; const Rational& operator* (const Rational& lhs, const Rational& rhs) { Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); return *resual; } Rational w, x, y, z; w = x * y * z; //两次new,找不到指针delete,内存泄漏 const Rational& operator* (const Rational& lhs, const Rational& rhs) { static Rational result; result = ...; return resual; } //static造成多线程安全性问题 if ((w * x) == (y * z)){} //总是为真 //两次operator*的确各自改变了static Rational的值,但由于返回的都是引用,调用端看到的永远是static Rational的现值 inline const Rational& operator* (const Rational& lhs, const Rational& rhs) { return Rational(lhs.n * rhs.n, lhs.d * rhs.d); }//承受返回值的构造和析构成本,而且编译器可能实现优化
条款22: 将成员变量声明为private
1.声明为private,可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性。
2.protected不比public更具封装性
某些东西的封装性与当其内容改变时可能造成的代码的破坏量成反比。
public成员变量当我们移除时,所有使用它的客户码都被破坏,一个不可知的大量。
protected成员变量被移除时,所有使用它的derived class 都被破坏,也是一个不可知的大量。
条款23:宁以non-member、non-friend替换member函数
可增加封装性、包裹弹性、和机能扩充性。
class WebBrowser { public: void clearCache(); void clearHistory(); void removeCookie(); void clearEverything();//调用clearCache,clearHistory,removeCookie }; //或定义non-member non-friend函数哪个好? void clearEverything(WebBrower& wb) { wb.clearCache(); wb.clearHistory(); wb.removeCookie(); }
愈多东西被封装,愈少人可以看到它。愈少人看到它,我们就有愈大的弹性与改变它。这就是我们首先推崇封装的原因:使我们改变事物而只影响有限客户。
愈少代码可以看到数据,愈多的数据被封装,我们就愈能自由地改变对象数据。 因此,愈多函数访问数据,数据的封装性就愈低。
如果数据不是private,就毫无封装性。能够访问private的只有member函数或friend函数。
因此non-member non-friend比member函数更有封装性。
//WebBrowser如有大量便利函数,降低各函数的编译相依的依存性 //使用时只需包含需要的头文件 //WebBrowser.h namespace WebBrowserStuff { class WebBrowser {...}; ... //核心机能,例如所有客户都需要的non-member函数 } //WebBrowserBookmark.h namespace WebBrowserStuff { ... //与书签相关的便利函数 } //WebBrowserBookmark.h namespace WebBrowserStuff { ... //与cookie相关的便利函数 }
条款24: 若所有参数需类型转换, 请为此采用non-member函数
class Rational { public: Rational(int numerator = 0, int denominator = 1); int numerator() const; int denominator() const; private:... }; //operator*应该实现为member还是non-member?不考虑条款23 //先看看member实现 class Rational { public: const Rational operator*(const Rational& lhs, const Rational& rhs); }; Rational oneEight(1, 8); Rational oneHalf(1, 2); Rational result; result = oneHalf * 2; //ok, oneHalf.operator*(2); 隐式转换 result = 2 * oneHalf; //error, 2.operator*(oneHalf); class Rational { ... }; const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } //各种OK
条款25: 考虑写出一个不抛异常的swap函数
//pimpl (point to implementation)手法:以指针指向一个对象,内含真正数据 namespace std{ template<typename T> void swap(T& a, T& b) { T temp(a); a = b; b = temp; } } class WidgetImpl { public: ... private: int a, b, c; //可能有许多数据 std::vector<double> v; //意味复制时间很长 }; class Widget { public: Widget(const Widget&); Widget& operator=(const Widget& rhs) { *pImpl = *rhs.pImpl; } private: WidgetImpl* pImpl; }; //置换两个Widget对象,只需置换pImpl指针,缺省的swap复制3和Widget对象,3个WidgetImpl对象,缺乏效率 namespace std { template<> void swap<Widget>(Widget& a, Widget& b) { swap(a.pImpl, b.pImpl); //编译失败,访问private } }//声明friend???? //一种策略,与STL有一致性 class Widget { public: void swap(Widget& other) { using std::swap; //曝光 swap(pImpl, other.pImpl); //查找顺序:本命名空间->global作用域->std::swap特化版(如有)->std::swap普通版 } }; namespace std { template<> void swap<Widget>(Widget &a, Widget& b) { a.swap(b); } } //若两个class都是template template<typename T> class WidgetImpl{...}; template<typename T> class Widget{...}; //不能偏特化函数模板,C++只允许偏特化类模板 //不能再std命名空间中增加新的template,只能全特化 //一种方案 class WidgetStuff { ... //模板化的WidgetImpl等等 template<typename T> class Widget{...}; //同前,内含swap成员函数 template<typename T> void swap(Widget<T>& a,Widget<T>& b){ a.swap(b); } };
总结:
如果swap的缺省版本的效率不高(几乎总意味你的class货template 使用了pimpl手法):
1. 提供一个public swap成员函数,高效地置换你的类型的两个对象值,且不该抛出异常(异常安全,条款29)
2.在你的class货template所在命名空间提供一个non-member swap,并令他调用上述swap成员函数
3.如果你正在编写一个class(而非template) ,为你的class特化std::swap,并令他调用你的成员函数。
最后用using表达式让std::swap在你的函数内曝光,然后不加任何namespace 修饰符,赤裸裸调用swap