改善程序与设计的55个具体做法——2、构造/析构/赋值运算
前言
几乎我们写的每一个class都会有一或多个构造函数、一个析构函数、一个copy assignment操作符。这些很难让你特别兴奋,毕竟它们是你的基本谋生工具,控制着基本操作,像是产出新对象并确保它被初始化、摆脱旧对象并确保它被适当清理、以及赋予对象新值。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及我们的整个类classes。所以确保它们行为正确是生死攸关的大事。本章提供的引导可让我们把这些函数良好地集结在一起,形成classes的脊柱。
条款05:了解C++默默编写并调用哪些函数
class Empty {
};
相当于以下类:
class Empty {
public:
Empty(){} //default 构造函数
Empty(const Empty&rhs){} //默认拷贝构造函数
~Empty(){} //默认析构函数
//virtual 见后
Empty& operator=(const Empty&rhs){}//copy assignment操作符
};
class Empty {
public:
Empty(){...} //default 构造函数
Empty(const Empty&rhs){...} //默认拷贝构造函数
~Empty(){...} //默认析构函数
//virtual 见后
Empty& operator=(const Empty&rhs){...}//copy assignment操作符
};
由于声明了一个构造函数,编译器于是不再为它创建默认的构造函数。这很重要
,意味着我们用心设计一个类class,其构造函数要求实参,那我们就无须担心编译
器会毫无挂虑地为你添加一个无实参构造函数而遮盖掉你的版本。
template<typename T>
class NameObject {
public:
NamedObject(const char*name, const T&value);
NamedObject(const std::string&name, const T&value);
private:
std::string nameValue;
T objectValue;
};
template<typename T>
class NamedObject {
public:
//以下构造函数如今不再接受一个const名称,因为nameValue
//如今是个reference-to-non-const string。先前那个char*构造函数
//已经过去了,因为必须有个string可供指涉。
NamedObject(std::string& name, const T&value);
//... 假设未申明operator=
private:
std::string& nameValue; //这如今是个reference
const T objectValue; //这如今是个const
};
现在考虑下面会发生什么事:
template<typename T>
class NamedObject {
public:
//以下构造函数如今不再接受一个const名称,因为nameValue
//如今是个reference-to-non-const string。先前那个char*构造函数
//已经过去了,因为必须有个string可供指涉。
NamedObject(std::string& name, const T&value);
//... 假设未申明operator=
private:
std::string& nameValue; //这如今是个reference
const T objectValue; //这如今是个const
};
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s;
请记住
- 编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
所有编译器产出的函数都是public。为阻止这些函数被创建出来,我们自行声明
它们,但这里并没有什么需求使你们必须将它们声明为public。因此你可以将
copy构造函数或copy assignment操作符声明为priate。
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
如果不慎在member函数或friend函数之内那么做,轮到连接器发出抱怨。
class Uncopyable {
protected:
Uncopyable(){} //允许derived对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&); //但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
为求阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
class HomeForSale :private Uncopyable {
...
};
请记住
- 为驳回编译器自动提供的机能,可以将响应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class 也是一种做法。
条款07:为多态基类声明virtual析构函数
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
//...
};
class AtomicClock:public TimeKeeper{}; //原子钟
class WaterClock :public TimeKeeper{}; //水钟
class WristWatch :public TimeKeeper{}; //腕表
//TimeKeeper派生类的动态分配对象
TimeKeeper* getTimeKeeper(); //返回一个指针,指向一个
为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。
因此为了避免泄漏内存和其他资源,将factory函数返回的每一个对象适当
地delete掉很重要。
TimeKeeper* ptk = getTimeKeeper(); //从TimeKeeper继承体系
//获得一个动态分配对象运用它...
delete ptk; //释放它,避免资源泄漏
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到
子类的析构代码。
问题出于getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),而那个对象却经由一个base class指针(例如一个TimeKeeper*指针)被删除,而目前的base class(TimeKeeper)有个non-virtual析构函数。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
class TimeKeeper {
public:
TimeKeeper();
virtual~TimeKeeper();
//...
};
TimeKeeper* ptk = getTimeKeeper();
delete ptk; //现在,行为正确
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数
如果class不含virtual函数,通常表示它并不意图被用到一个base class。
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。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl----编译器在其中寻找适当的函数指针。
虚析构和纯虚析构共性:
可以解决父类指针释放子类对象
都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
class AWOV {
public:
virtual ~AWOV() = 0;//声明纯虚析构函数
};
//必须为纯虚析构函数提供一份定义:
AWOV::~AWOV(){} //纯虚析构函数的定义
请记住
- polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励这样做。
class Widget {
public:
~Widget(){} //假设这个可能吐出一个异常
};
void doSomething()
{
std::vector<Widget> v;
} //v在这里被自动销毁
当vector v被销毁,它有责任销毁其内含的所有widgets。假设v内含十个Widgets,
而在析构第一个元素期间,有个异常被抛出。其他九个Widgets还是应该被销毁,
因此v应该调用它们各个析构函数。但假设在哪些调用期间,第二个Widget析构又
抛出异常。现在有两个同时作用的异常,这对C++而言太多了。
class DBConnection {
public:
static DBConnection create(); //这个函数返回DBConnection对象
//为求简化暂略参数,关闭联机失败则抛出异常
void close();
};
class DBConn {
public:
~DBConn() {
db.close();
}
private:
DBConnection db;
};
这便允许客户写出这样的代码:
{
...
DBConn dbc(DBConnection::create());
}
只要调用close成功,一切都美好,但如果该调用导致异常,DBConn析构函数会
传播该异常,也就是允许它离开这个析构函数。那会造成问题,因为那就是抛出
了难以驾驭的麻烦。
--------------------------------------------------------------------
两个解决办法可以避免这一问题:
- 如果close抛出异常就结束程序。通常通过调用abort完成:
DBConn::~DBConn() {
try { db.close(); }
catch (...) {
//制作运转记录,记下对close的调用失败
std::abort();
}
}
- 吞下因调用close而发生的异常:
DBConn::~DBConn() {
try { db.close(); }
catch () {
//制作运转记录,记下对close的调用失败
}
}
一般而言,将异常吞掉是个坏主意,因为它压制了"某些动作失败"的重要信息。
一个较佳的策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。
class DBConn {
public:
void close() { //提供用户使用的新函数
db.close();
closed = true;
}
~DBConn() {
if (!closed) {
try { //关闭连接
db.close();
}
catch (...) {
//制作运转记录,记下对close的调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
请记住
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通(而非在析构中)执行该操作。
条款09:绝不在构造和析构过程中调用virtual函数
不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来所预想的结果。
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction::Transaction() //基类构造函数之实现
{
logTransaction();
}
class BuyTransaction :public Transaction {
public:
virtual void logTransaction()const;
};
class SellTransaction :public Transaction {
public:
virtual void logTransaction() const;
};
请记住
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class.
条款10:令operator= 返回一个reference to *this
当形参和成员变量同名时,可用this指针区分
在类的非静态成员函数中返回对象本身,可是使用return *this
class Widget {
public:
Widget& operator=(const Widget&rhs) { //返回类型是个reference
//指向当前对象
return *this; //返回左侧对象
}
};
请记住
- 令赋值(assignment)操作符返回一个reference to *this。
条款11:在operator=中处理"自我赋值"
class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd);//rb和*pd有可能其实是同一个对象
请记住
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较"来源对象"和"目标对象"的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为任然正确。
条款12:复制对象时勿忘其每一个成分
class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd);//rb和*pd有可能其实是同一个对象
void logCall(const std::string& funcName);
class Customer {
public:
Customer(const Customer&rhs);
Customer& operator=(const Customer&rhs);
private:
std::string name;
};
如果你为class添加一个成员变量,你必须同时修改coying函数。如果忘记,编译器不太可能提醒。
一旦发生继承,可能回造成此一主题最暗中肆虐的一个潜藏危机:
class Date{};
class Customer {
public:
private:
std::string name;
Date lastTransaction;
};
class PriorityCustomer :public Customer {
public:
PriorityCustomer(const PriorityCustomer&rhs);
PriorityCustomer& operator=(const PriorityCustomer&rhs);
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
:priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer & PriorityCustomer::operator=(const PriorityCustomer & rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
base class name 和lastTransaction未被复制
任何时候只要我们承担其为"derived class撰写copying函数"的重责大任,
必须很小心地也复制其base class成分。哪些成分往往是private,所以
我们无法直接访问它们,应该让derived class的copying函数调用相应的
bass class函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
:Customer(rhs), //调用base class的copy构造函数
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer & PriorityCustomer::operator=(const PriorityCustomer & rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); //对base class 成分进行赋值动作
priority = rhs.priority;
return *this;
}
请记住
- Copying函数应该确保复制"对象内的所有成员变量"及"所有base class成分"。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用