2.构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后的代码”,。注意编译器产生的析构函数是个非虚的,除非这个类的基类声明有虚析构函数。
至于拷贝构造函数和拷贝赋值操作符,编译器创建版本知识单纯地将来源对象的每一个非静态成员变量拷贝给目标对象。考虑一个NamedObject template:
template
class NamedObject
{
public:
NamedObject(const char* name, const T&value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};
由于声明了一个构造函数,编译器不再为它创建一个默认构造函数。
看看拷贝构造函数的用法:
NamedObject no1("Smallest Prime Number" ,2);
NamedObject no2(no1); //调用拷贝构造函数
- 编译器可以暗自为class创建默认构造函数、拷贝构造函数、拷贝运算符和析构函数。
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
如果想声明一个类是独一无二的,即每一个人都是独一无二的。
class CPerson {... };
既然每个人都是独一无二的,因此人是不能复制的,即CPerson的对象拷贝动作以失败收场:
CPerson Person1;
CPerson Person2;
CPerson Person3(Person1); //企图拷贝h1 -不能通过编译
Person1 = Person2; //企图拷贝h2--不能通过编译
如何阻止这种情况发生:
手动声明拷贝构造函数和拷贝赋值操作符。即将其声明为private。
class CPerson
{
public:
...
private:
...
CPerson(const CPerson&); //只有声明
CPerson& operator=(const CPerson&);
};
为什么不写参数名称:
由于参数不会被使用,写出来没有必要。
有了上述的class定义,当客户企图拷贝CPerson对象,编译器会阻挠他,但是你不慎在成员函数和友元函数中调用了,则会出现问题。
- 解决方案,专门为阻止拷贝声明一个基类
class Uncopyable
{
protected:
Uncopyable() {} //允许子类对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); //阻止拷贝
Uncopyable& operator=(const Uncopyable&);
};
为了阻止CPerson对象被拷贝,我们唯一做的就是继承Uncopyable
class CPerson: private Uncopyable
{
...
};
Uncopyable class的实现和运用比较微妙,包括不一定得以public继承它,以及它的析构函数不一定是virtual等等。
其也符合条款39所描述的empty base class optimization资格,但是多重继承有时会阻止empty base class optimization。
请记住
为了驳回编译器提供的功能,可将相应的成员函数声明为private并且不予以实现。使用像Uncopyable这样的基类也是一种做法。
条款07:为多态基类声明virtual析构函数
有多种做法来记录时间,因此设计一个TimeKeeper基类和一些子类作为不同的计时方法:
class TimeKeeper
{
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper {...}; //原子钟
class WaterClock: public TimeKeeper {...}; //水钟
class WeistWatch: public TimeKeeper {...}; //腕表
TimeKeeper* getTimeKeeper(); //返回一个指针,指向一个TIMEKeeper派生类的动态分配对象
为了遵循factory函数的规矩,被getTimerKeeper()返回的对象必须位于堆之中,而将factory函数返回的每一个对象适当地delete掉非常重要:
TimeKeeper* ptk = getTimeKeeper(); //从TimeKeeper继承体系
//获得一个动态分配对象
... //运用它
delete ptk; //释放他,避免资源泄露。
条款13中说明,依赖用户delete动作,基本带有某种错误倾向。条款18则谈到factory函数接口该如何修改以防止可以预见的客户动作错误。
上述代码问题出在getTimeKeeper返回的指针指向一个子类对象(例如AtomicClock),而哪个对象却经由一个base class指针被删除,而目前base class有个非虚析构函数。
当子类对象经过一个父类指针被删除,而该父类带着一个非虚析构函数,结果就是子类中除了父类中的成分没被删除,会造成一种局部销毁的情况。
解决办法:给父类声明一个虚析构函数。此后就能够完全删除子类对象。
class TimeKeeper {
public:
TimeKeeper( );
virtual ~TimeKeeper();
...
};
TimeKeeper* ptk = getTimerKeeper();
...
delete ptk;
像TimeKeeper这样的基类除了析构函数之外,通常还有其他虚函数,因为虚函数的目的就是允许子类的实现得以客制化,只要任何一个class带有virtual函数,几乎可以确定应该有一个虚析构函数。
如果class不含虚函数,通常表示它并不是作为一个基类。当class不企图被当做基类,令其析构函数为虚往往不是一个好主意。考虑一个用来表示二维空间点坐标的class:
class Point
{
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果int占用4个字节,那么Point对象可以塞入一个8字节的缓存器中。这样的一个Point对象可以被当做一个“64-bit量”,传给其他语言撰写的函数。然而Point的析构函数是虚,形势就起了变化,使得程序不具有移植性。
因此,无端地将所有类的析构函数声明为虚,就像从未声明它们为虚一样,都是错误的。
许多人的心得是:只有当class内含有至少一个虚函数,才为他声明虚析构函数
即使类完全不带虚函数,还是有可能被困扰。标准string不含任何虚函数,但有时候程序员会错误地把它当做基类:
class SpecialString:public std::string //std::string有个非虚析构函数
{
...
};
乍看似无害,但如果你在程序任意某处无意间将一个指针指向SpecialString转换为一个指向string的指针,然后将转换所得的那个string指针delete掉,那可能会发生错误:
SpecialString* pss = new SpecialString("doom");
std::string* ps;
...
ps = pss;
...
delete ps; //发生资源泄露的情况,因为SpecialString的析构函数没被调用。
相同的分析适用于任何不带虚析构函数的类,包括STL容器如vector,list,set,tr1::unordered_map等等。
有时候令class带一个纯虚析构函数,可能更为便利。纯虚函数导致抽象类——也就是不能被实体化的类。也就是说,你不能为那种类型创建对象。然而有时候你希望抽象class,但手上没有任何纯虚函数,怎么办?
为你希望它成为抽象的那个类声明一个纯虚构函数。下面是个例子:
class AWOV {
public:
virtual ~AWOV( ) = 0; //声明纯虚析构函数
};
这个class有一个纯虚析构函数,所以它是一个抽象class,又由于它有个虚析构函数,所以你不需要担心析构函数的问题。
然而这里有个窍门:你必须为纯虚析构函数提供一份定义:
AWOV::~AWOV() { } //纯虚析构函数的定义
析构函数的运作方式是,最深层派生的那个类被调用,然后在逐渐向上调用析构函数,当调用到基类中的析构函数的时候,需要给此虚构函数提供一份定义,如果不这样做,连接器会发出抱怨。
给基类一个虚析构函数,这个规则只适用于带多态性质的基类身上。这种基类的设计目的是为了通过基类接口处理继承类对象。
某些类设计的目的是作为基类用途,但不是为了多态用途(如STL容器和string)
请记住
- 带多态性质的基类应该声明一个虚析构函数。如果类带有任何虚函数,它就应该拥有一个虚析构函数。
- 类设计目的如果不是作为基类使用,或不是为了具备多态性,就不应该声明虚析构函数。
条款08: 别让异常逃离析构函数
C++并不禁止析构函数发出异常,但它不鼓励你这样做。考虑如下代码:
class Widget
{
public:
...
~Wifget( ) {...} //假设这个可能吐出一个异常
};
void doSomething()
{
std::vector v;
...
} //v在这里被自动销毁