Effective C++ 简要条款分析(一)
c++实在是一门深奥晦涩的语言,不同专业水准的程序员写出来的代码质量有着天壤之别,以至于必须出版一本提供一些“专家经验”来引导c++程序员写出更加高质量的代码。
为驳回编译器(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像uncopyable这样的base class也是一种做法。
一般来说,如果我们不希望class提供某些功能,只需要不去定义相应的函数即可。但是这个策略对copy构造函数和
copy assignment操作符不起作用,因为如果你不声明,编译器会为你声明它们。那么如果禁止拷贝对象呢?这个问题解决的关键在于所有编译器产出的函数都是
public,因此我们只需要把这些函数声明为
private 便可解决禁止
copying 的问题。
这里有两个细节需要注意:为了防止member 函数和
friend 函数调用
private 函数,我们只声明不定义;为了将连接期错误转移至编译期,只需要定义如下的
base class ,然后继承即可。
class uncopyable{ protected: uncopyable(){} //允许derived 对象构造和析构 ~uncopyable(){} private: uncopyable(const uncopyable&);//但阻止copying uncopyable& operator=(const uncopyable&); }; class homeforsale: private uncopyable{ ... //不再声明copy构造函数或者copy assign.操作符 }; class homeforsale: public boost::noncopyable{ ... //使用boost库的noncopyable也可 }
polymorphic(带多态性质的)
base classes 应该声明一个
virtual 析构函数。如果
class 带有任何
virtual 函数,他就应该拥有一个
virtual 析构函数。但是如果类的设计并不是作为
base classes 使用,或不是为了具备多态性,那就不该声明
virtual 函数。
c++明确指出,当
derived class 对象经由一个
base class 指针被删除,而该
base class 带着一个
non-virtual 析构函数,其结果未有定义-实际执行时通常发生的是对象的
derived 部分没被销毁。
消除这个问题的方法很简单,就是给base class 一个
virtual析构函数。此后删除
derived class 对象就会如你所想的那样,正常销毁。而
virtual 函数的实现机制是通过虚函数表,会导致对象的体积增大,所以在非
base class 中使用
virtual 是一个馊主意。
普遍而常见的 raii class copying行为是:抑制copying、施行引用计数法。另外shared_ptr可以定制删除器。
在底层资源管理中,我们祭出“引用计数法”:保有资源,直到最后一个使用者(对象)被销毁。
shared_ptr可以轻松实现这种引用计数,但是它的缺省行为是“当引用计数为0时删除其所指物”,有时候这并不是我们想要的行为。幸运的是
shared_ptr允许指定所谓的删除器,在引用计数为0时调用,例子如下:
//定置删除器的仿函数 struct fclose { void operator()(void *ptr) { fclose((file *)ptr); cout << "fclose()" << endl; } }; void test() { //调用构造函数构造一个匿名对象传递过去,文件正常关闭 boost::shared_ptr sp(fopen("test.txt","w"),fclose()); }
以上就实现了通过定制的删除器对文件资源的管理,也正好说明了
shared_ptr并不仅仅局限于对内存这种资源的管理。另一方面,
shared_ptr通过定制删除器也可以防范
dll问题,可被用来自动解除互斥锁。
另备注一点:shared_ptr通过可以通过get方法获取到
raw pointer,这适用于某些对参数有限制的函数。
尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。但是以上规则并不适用于内置类型,以及stl的迭代器和对象,对它们而言,pass-by-value往往比较适当。
先说效率问题,
pass-by-refer-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(); }
想象当用一个windowwithscrollbars对象来调用这个函数时将发生什么:
windowwithscrollbars wwsb; printnameanddisplay(wwsb);
参数w将会作为一个windows对象而被创建(它是通过值来传递的,记得吗?),所有wwsb所具有的作为windowwithscrollbars对象的行为特性都被“切割”掉了。
printnameanddisplay内部,w的行为就象是一个类window的对象(因为它本身就是一个window的对象),而不管当初传到函数的对象类型是什么。尤其是
printnameanddisplay内部对display的调用总是
window::display,而不是
windowwithscrollbars::display。
解决的方法就是使用
pass-by-ref-to-const来传递w,因为pass-by-ref通常意味着传递的是指针。
// 一个不受“切割问题”困扰的函数 void printnameanddisplay(const window& w) { cout << w.name(); w.display(); }
至于内置类型和stl的迭代器和函数对象,一般他们都被设计为pass-by-value,效率往往更高一些,这只是一个建议。
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能需要多个这样的对象。
一旦你领悟了
pass-by-value在效率方面的牵连,往往一心一意根除pass-by-value带来的种种邪恶,在这个过程中,有可能会产生一些致命错误!就像上面条款提到的。
条款中对operator *()返回&导致错误的例子这里不再提及。对于返回local stack对象的引用,很明显,在函数退出前,对象被销毁,这会导致“未定义行为”。对于
heap-allocated对象,则因为需要额外的delete,很可能导致内存泄露。static则容易导致多线程安全性问题。
推荐阅读
-
Effective C++ 简要条款分析(一)
-
Effective C++ 笔记:条款 31 将编译关系降至最低
-
Effective Modern C++ 条款32 对于lambda,使用初始化捕获来把对象移动到闭包
-
Effective Modern C++ 条款23 理解std::move和std::forward
-
Effective Modern C++ 条款37 在所有路径上,让std::thread对象变得不可连接(unjoinable)
-
Effective Modern C++ 条款38 意识到线程句柄的析构函数的不同行为
-
条款01:视C++为一个语言联邦
-
Effective Modern C++ 条款22 当使用Pimpl Idiom时,在实现文件中定义特殊成员函数
-
Effective C++ 笔记:条款 33 避免继承导致的名称遮掩
-
Effective C++:条款26:尽可能延后变量定义式的出现时间