几乎你写的每一个class 都会有一个或者多个构造函数,一个析构函数,一个copy assignment操作符。这是类的一些基本操作。
条款5:了解C++默默并调用那些函数。
请记住:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
条款6:若不想使用编译器自动生成的函数,就该明确拒绝。
由于C++的类是默认生成复制构造函数和=操作函数,有时候会导致不必要的函数复制行为,我们不希望出现下面这种情况:
1
2 3 4 |
HomeForSale h1;
HomeForSale h2; HomeForSale h3(h1); //不希望调用复制 h1 = h2; //不希望复制 |
1
2 3 4 5 6 7 8 9 |
class HomeForSale
{ public: ... private: ... HomeForSale( const HomeForSale &); //只有声明。 HomeForSale & operator( const HomeForSale &); }; |
方法二:为了有效解决上面的问题,我们可以定义一个base class来解决这个问题。而集成这个base class不再声明copy构造函数或者copy assign操作符。
具体代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
class UnCopyable
{ protected: Uncopyable() {} ~Uncopyable() {} private: //私有的copy 构造函数和私有的copy assignment操作符函数 Uncopyable( const UnCopyable &); Uncopyable & operator=( const UnCopyable &); }; class HomeForSale: private UnCopyable //私有继承 { }; |
UnCopyable class 的实现和运用颇为微妙,包括不一定得public继承它,以及U年copyable 的析构函数一定不能是Virtual。
请记住:
为了驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像UnCopyable这样的base class 也是一种做法。
条款7:为多态基类声明virtual析构函数。
1
2 3 4 5 6 7 8 9 10 11 12 |
class TimeKeeper
//base class { public: TimeKeeper(); ~TimeKeeper(); }; class AtomicClock: public Timekeeper{...}; //原子钟 class WaterClock: public TimeKeeper{...}; //水表 TimeKeeper *ptk=getTimeKeeper(); //从TimeKeeper继承体系获得一个动态分配对象 ... //运用之 delete ptk; //释放它,避免资源泄漏 |
在上面的工厂模式中,我们设计工厂函数式,返回指针指向一个计时对象,工厂函数会返回一个基类指针,指向新生成的derived clas对象。为了遵守工厂函数的规矩,被getTimekeeper()返回的对象必须位于heap。因此为了避免泄漏内存和其他资源,将工厂函数返回的每一个对象适当的delete 很重要。
这样问题就会出现来了。问题出在getTimeKeeper返回的指针指向一个derived class对象,而那个对象却经由一个base class指针被删除,而目前的base class 有个non-Virtual 析构函数。这就带来了灾难,因为C++明白指出,当derived class 对象经由一个base class 指针被删除,而该base class 带着一个non-Virtual 析构函数,其结果是未定义的--实际导致derived class 没有被销毁,而导致内存泄漏。
针对上诉问题,提出解法一:
1
2 3 4 5 6 7 8 9 10 11 12 |
class TimeKeeper
//base class { public: TimeKeeper(); virtual ~TimeKeeper(); }; class AtomicClock: public Timekeeper{...}; //原子钟 class WaterClock: public TimeKeeper{...}; //水表 TimeKeeper *ptk=getTimeKeeper(); //从TimeKeeper继承体系获得一个动态分配对象 ... //运用之 delete ptk; //释放它,避免资源泄漏 |
像TimeKeeper这样的base classes除了析构函数之外通常还有其他Virtual 函数,因为Virtual 函数的目的是允许derived class 的实现得以客制化。任何class 只要带有Virtual 函数都几乎确定应该有一个Virtual 析构函数。
///这条建议并不适用于所有的类。。。
我们来看看下面两个例子。
1
2 3 4 5 6 7 8 |
class Point
//一个二维空间点 { public: Point( int xCoord, int yCoord); ~Point(); private: int x,y; } |
欲实现出Virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个Virtual函数该被调用,这份信息通常是由一个所谓的vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table),每个带有Virtual函数的class 都有一个相应的vtbl。
因此很多人的使用心得是:只有当class 内含至少一个Virtual函数,才为它声明Virtual析构函数。
还有另外一个例子;
1
2 3 4 5 6 7 8 9 10 11 |
class SpecialString:
public std::string
{ ... }; SpecialString *pss= new SpecialString( "nihao"); std::string *ps; ... ps=pss; ... delete ps; //未定义,现实中*ps的SpecialString资源会泄漏 //因为SpecialString析构函数没有被调用 |
最后我们来看看一个抽象类的例子。所谓的抽象类既是令class 带一个pure Virtual 析构函数,所谓的抽象类也就是不能被实体化的class。由于抽象类总是企图被当做一个base class来用,而有由于base class应该有个Virtual析构函数。为了你希望他称为抽象的那个class 声明一个pure Virtual 析构函数,下面是一个例子;
1
2 3 4 5 6 7 8 |
class AWOW
{ public: virtual ~AWOW()= 0; //声明pure Virtual析构函数。 } AWOW::~AWOW() {} //pure Virtual 析构函数的定义 |
你必须为这个pure Virtual 析构函数提供一份定义。
因为析构函数的运行方式是,最深层派生的那个class 其析构函数最先被调用,然后是其每一个base class 的析构函数被调用。
根据上面的例子我们可以做出总结了:
请记住:
polymorphic(带多态性质的) base classes 应该声明一个Virtual析构函数,如果class带有任何Virtual函数,它就应该拥有一个Virtual析构函数。
classes的设计目入股不是作为base classes 使用,或不是为了具备多态性质的,就不该声明Virtual析构函数。
条款8:别让异常逃离析构函数。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class DBConn
//这个class用来管理DBConnection 对象 { public: ... ~DBConn() //确保数据库连接总是被关闭。 { db.close(); } private: DBConnection db; }; //因此用户可以写出如下的代码: DBConn dbc(DBConnection::create()); //开启一个区块block 建立DBConnection对象并交给DBConn对象管理 //通过DBConn的接口,使用DBConnection对象在区块结束点,DBConn 对象被销毁,因为自动调用close() |
1
2 3 4 5 6 7 8 9 10 11 12 |
DBConn::~DBConn()
{ try { db.close(); } catch(...) { //制作运转记录,记下对close的调用失败 std::abort(); } } |
1
2 3 4 5 6 7 8 9 10 11 |
DBConn::~DBConn()
{ try { db.close(); } catch(...) { //制作运转记录,记下对close的调用失败 } } |
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class DBConn
{ public: ... void close() //提供给客户使用的新函数 { db.close(); closed = true; } ~DBConn() { if(!closed) { try { db.close(); //关闭连接 } catch(..) { //制作运转记录,记下对close的调用失败 //关闭失败,结束程序或者吞下异常。 } } } private: DBConnection db; bool closed; } |
条款9:绝不在构造和析构过程中调用Virtual函数。
在构造函数下调用虚函数如下所示,会发生意想不到的事情。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// never_call_vir.cpp : #include "stdafx.h" #include <iostream> using namespace std; class Base //基类 { public: Base() { print(); //构造函数调用虚函数 } virtual void print() //虚函数 { cout << "Base()~print()" << endl; } }; class Derived: public Base //派生类 { public: Derived() { cout << "Derived()" << endl; //构造函数 } virtual void print() { cout << "Derived()~print()" << endl; //重构虚函数 } }; int _tmain( int argc, _TCHAR *argv[]) { Derived der; //调用派生类函数 return 0; } |
从生成的结果可以看到,基类Base的print。基类型构造期间virtual函数绝对不会下降到派生类型阶层,也就是说virtual函数并不是virtual的。由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived class阶层,要知道derived class的函数几乎必然取用local成员变量,而那些成员变量尚未初始化。这将是一张通往不明确行为和彻夜调试大会串的直达车票。“要求使用对象内部尚未初始化的成分”是危险的代名词,所以C++不让你走这条路。
其实还有比上述理由根本的原因:在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至(resolve to)base class,若使用运行期类型信息(runtime type information,例如dynamic_cast(见条款27)和typeid),也会把对象视为base class类型。本例之中,当derived 构造函数正执行起来打算初始化“derived 对象内的base class成分”时,该对象的类型是base 。那是每一个C++成分(见条款1)的态度,而这样的对待是合理的:这个对象呢你的“derived 专属成分”尚未初始化,所以面对它们,最安全的做法就是视它们不存在。对象在derived class构造函数开始执行前不会成为一个derived class对象。
相同道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++的任何部分包括virtual函数,dynamic_casts等等也就那么看待它。
但是假设这样的调用virtual函数放在non-virtual函数里,而non-virtual函数则在构造函数和析构函数里调用,那么编译器很难检测到这样的情况。假设X定义的是pure virtual,,但运行时则会crash(如果你很愿意看的话)。
如下代码,就会出现问题
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// never_call_vir.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> using namespace std; class Base { public: Base() { Init(); } void Init( void) { Print(); } virtual void Print( void) const = 0; //纯虚函数 }; class Derived : public Base { public: virtual void Print( void) const { cout << "Derived Print" << endl; } }; int _tmain( int argc, _TCHAR *argv[]) { Derived der; return 0; } |
从图中可以看到,在pure virtual 函数被调用的时候,程序是会终止的。
所以确定你的构造函数和析构函数都没有(在对象被创建和销毁期间)调用virtual函数,而它们调用的所有函数也都服从同一约束
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Transatcion
{ public: explicit Transaction(cosnt std::string &logInfo); void logTransaction( const std::string &logInfo) const; //non-virtual }; Transaction::Transaction( const std::string &logInfo) { logTransaction(logInfo); //non-virtual调用 } class BuyTransaction: public Transaction { public: BuyTransaction(parameters) : Transaction(createLogString(parameters)) {} //将log信息传递给基类构造函数 private: static std::string createLogString(parameters); }; |
条款10 令operator=返回一个reference to *this
为了实现“连锁赋值”,赋值操作符必须返回一个reference to *this指向操作符的左侧实参。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Widget
{ public: ... Widget & operator=( const Widget &rhs) { ... return * this; //返回左侧对象 } Widget & operator+=( const Widget &rhs) //这个协议适用于+=、-=,*=等等。 { ... return * this; } Widget & operator=( int rhs) { ... return * this } } |
条款11 在operator=中处理‘自我赋值’
1
2 3 4 5 6 7 |
Widget& Widget::
operator=(
const Widget & rhs)
{ if( this==&rhs) return * this; //证同测试 如果是自我赋值,就不做任何事 delete pb; pb= new Bitmap(*rhs.pb); return * this; } |
这个版本的赋值函数基本上是可以接受的,但不见的是安全的,因为当new产生异常时pb依然是个不确定的指针。
第三个版本:
1
2 3 4 5 6 7 |
Widget &Widget::
operator=(
const Widget &rhn)
{ bitmap *pOrig = pb; pb = new bitmap(*rhn.pb); delete pOrig; return this; } |
这个函数在开始时用pOrig记录了pb,当new没有异常时我们在把Pb原来的指向空间释放掉,从而提高了安全性。
实现赋值函数还有另外一个思想,即copy and swap技术
1
2 3 4 5 6 7 8 9 10 11 |
class Widget
{ void swap( const Widget &rhs); } Widget &Widget:: operator=( const Widget &rhs) { Widget temp(rhs); //防止改变rhs swap(temp); return * this; } |
1
2 3 4 5 6 |
Widget& Widget::
operator=(
const Widget rhs)
//按值传递是实参的一个copy { swap(temp); return * this; } |
条款12 复制对象时勿忘其每一个成分
如
1
2 3 4 5 6 7 8 9 10 11 12 13 |
class Test
{ private: string name; public: Test( const Test &rhs): name(rhs.name) {} Test & operator = ( const Test &rhs) { name = rhs.name; return this; } }; |
编译器一般不会报错
2.在继承中,注意base类的拷贝
1
2 3 4 5 6 7 8 9 10 11 12 13 |
class derve:
public Test
{ private: string derveStr; public: derve( const derve &rhs): derveStr(rhs.derveStr) {} derve & operator = ( const derve &rhs) { derveStr = rhs.derveStr; return this; } }; |
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class derve:
public Test
{ private: string derveStr; public: derve( const derve &rhs): derveStr(rhs.derveStr), Test(rhs) {} derve & operator = ( const derve &rhs) { Test:: operator = (rhs); derveStr = rhs.derveStr; return this; } }; |
3.如果两个拷贝函数有重复的代码,可以定义一个成员函数(如init()),将重复的代码放到这个成员函数中,然后拷贝函数再调用这个init()函数,这两个拷贝函数不要互相调用,这样不太好,应该把相同代码放到第三方函数中
请记住:
Copying 函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
不要尝试以某个copying函数实现另一个copying函数。应该讲共同机能放进第三个函数中,并由两个copying函数共同调用。