构造/析构/赋值运算
条款05: 了解C++默默编写并调用哪些函数
C++会为类生成默认的关键函数,但是这些函数只有在被需要调用的时候才会生成,因此这4种功能不是保证都存在的。
class Empty {};
//假定以下功能都会被调用,此定义等价于:
class Empty{
public:
Empty(){....} //构造函数
Empty(const Empty& empty){.....} //拷贝构造函数
~Empty(){....} //析构函数
Empty& operator=(const Empty& empty){....} //拷贝赋值运算符
};
对于赋值运算符,只有当代码合法而且有意义时,编译器才会自动生成,否则拒绝编译。解决方法是自己定义赋值运算符。
条款06: 明确告诉编译器你不需要的某些自动生成的函数
所有编译器生成的函数都是public。通过将不需要的函数声明为private可以阻止编译器自动生成。
class House{
public:
....
private:
House(const House&); //只有声明,不要定义
House& operator=(const House&); //因为不会被用到,就不必再给参数想一个名字了
};
更好的解决方案:当一个父类将拷贝函数声明为私有时,编译器会拒绝为它的子类生成拷贝函数。因此我们可以专门使用一个父类,在其中声明拷贝操作为私有,并让我们的类继承自它。
class Uncopyable{
protected: //允许derived对象构造和解析
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable&); //将拷贝函数转移至此
Uncopyable& operator=(const Uncopyable&);
};
class House : private Uncopyable{...}; //私有继承
条款07: 为多态基类声明virtual析构函数
析构函数(destructor)用来释放对象所占用的资源。当对象的使用周期结束后,例如当某对象的范围(scope)结束时,或者是动态分配的对象被delete关键字解除资源时,对象的析构函数会被自动调用,对象所占用的资源就会被释放。以及像第五章讲过的,假如在你的类中不声明析构函数,编译器也会为你自动生成一个。
多态(polymorphism)则是C++面向对象的基本思想之一,即抽象(abstraction),封装(encapsulation),继承(inheritance),多态(polymorphism)。如果我们希望仅仅通过基类指针就能操作它所有的子类对象,那这就是多态。
当你通过基类指针使用子类,使用完毕后却只从基类删除。同时这个基类的析构函数并不是虚函数(virtual),也就是不允许子类有自己版本的析构函数,这样就只能删除子类中基类的部分,而子类衍生出来的变量和函数所占用的资源并没有被释放,这就造成了这个对象只被释放了一部分资源的现象,依然会导致内存泄漏。
class TimeKeeper{ //计时器类,用来当做基类
public:
TimeKeeper(); //这是构造函数
~TimeKeeper(); //这是析构函数
......
};
class AtomicClock : public TimeKeeper{...}; //原子钟是一种计时器
class WaterClock : public TimeKeeper{...}; //水钟也是一种计时器
TimeKeeper* getTimeKeeper(){...} //用来返回一个动态分配的基类对象
TimeKeeper* ptk = getTimeKeeper();
..... //使用这个指针操作它的子类
delete ptk; //(不建议这么写)使用完毕,释放资源
解决方法:给基类一个虚的析构函数,这样子类就允许拥有自己的析构函数,就能保证被占用的所有资源都会被释放。
class TimeKeeper{
public:
virtual ~TimeKeeper();
// 此时删除derived class对象就会销毁整个对象
....
};
TimeKeeper* ptk = getTiemKeeper();
......
delete ptk; // 现在行为正确
虚函数是用来在运行时(runtime),自动把编译时未知的对象,比如用户输入的对象,和它所对应的函数绑定起来并调用。当一个类包含虚函数时,编译器会给这个类添加一个隐藏变量,即虚函数表指针(virtual table pointer),用来指向一个包含函数指针的数组,即虚函数表(virtual table)。当一个虚函数被调用时,具体调用哪个函数就可以从这个表里找了。
对于抽象类(abstract class),抽象类是包含至少一个纯虚函数的类(pure virtual function),而且它们不能被实例化,只能通过指针来操作,是纯粹被用来当做多态的基类的。
相比具体类(concrete class),虽然它们都可以通过父类指针来操作子类,但抽象类有更高一层的抽象,从设计的角度上能更好概括某些类的共同特性。
多态的基类需要有虚析构函数,抽象类又需要有纯虚函数,那么在抽象类中就要把析构函数声明为纯虚函数:
class A{
public:
virtual ~A() = 0; //声明纯虚函数
};
同时注意,当在继承层级中某一类的析构函数被调用时,它下一级类的析构函数会被随后调用,最后一直到基类的析构函数,因此作为析构函数调用的终点,要保证有一个定义,否则链接器会报错。
A::~A(){} //基类的析构函数要有一个空的定义
析构函数的运作方式是:最深层派生(most derived)的那个class起析构函数最先被调用,然后是其每一个base class的析构函数被调用。
*某些类不是被用来当做基类的,比如std::string和STL,或者某些不是用来实现多态的基类,比如上一章的Uncopyable,就不需要虚的析构函数。
条款08: 阻止异常离开析构函数
一个例子:
class DBConnection{ //某用来建立数据库连接的类
public:
...
static DBConnection create(); //建立一个连接
void close(); //关闭一个连接,假设可以抛出异常
};
为确保客户不忘在DBConnection
对象上调用close()
,可以创建一个用来管理DBConnection
资源的class,并在其析构函数中调用close
。
class DBConn{ //创建一个资源管理类来提供更好的用户接口
public:
....
~DBConn{ db.close(); ] //终止时自动调用关闭连接的方法
private:
DBConnection db;
};
// 用户可这样写
{
DBConn dbc(DBConnection::create());//创建一个DBConn类的对象
... //使用该对象
} //对象dbc被释放资源
但是上述场景有一个问题,若对象dbc
的析构函数调用close()
时抛出异常该怎么办?
- 主动关闭
使用std::abort()
主动关闭程序,而非任由程序崩溃。
DBConn::~DBConn(){
try{
db.close();
}catch(...){
//记录访问历史
std::abort();
}
}
- 重新设计借口
把调用close的责任从DBConn析构函数移到DBConn客户手上。但在DBConn中仍有“双保险”调用。
class DBConn{
public:
...
~DBConn();
void close(); //[1]当要关闭连接时,手动调用此函数
private:
DBConnection db;
bool closed = false; //显示连接是否被手动关闭
};
void DBConn::close(){ //[1]
db.close();
closed = true;
}
DBConn::~DBcon(){
if(!closed)
try{
db.close();
}catch(...){
//记录访问历史
//消化异常或者主动关闭
}
}
条款09: 绝不在构造和析构过程中调用virtual函数
假设有一个class继承体系,用来记录股票交易:
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;
...
};
此时定义一个对象:
BuyTransaction b;
此时bass class构造函数先被调用,但base class构造期间virtual函数不会下降到derived class阶层。derived class 对象的base class构造期间,对象的类型是base class而不是derived class(可以看作在base class构造期间,virtual函数不是virtual函数)
如果一定想要对象在初始化的时候完成某些任务。那就需要在父类Transaction
中,把虚函数logTransaction
去掉virtual
关键字,变成普通的函数,然后在子类构造过程中,把某些信息传递到父类的构造函数中。
class Transaction{
public:
explicit Transaction(const std::string& info);
//explicit关键字用来防止隐式转换
void logTransaction(const std::string& info) const;
//增加一个传递参数,就可以从子类获得信息了
...
};
Transaction::Transaction(const std::string& info){
...
logTransaction(info);
}
现在就可以在子类对象中如下定义构造函数了,这样就能把子类的信息传递到父类中,让父类构造函数去完成子类构造函数想做的事:
class BuyTransaction : public Transaction{
public:
BuyTransaction(...) : Transaction(createLog(...)) { ...}
...
private:
static std::string createLog(...);
};
这里
createLog()
就是一个辅助函数(helper function),用来将某函数的一部分功能封装成另一个小函数,减少代码的复杂性,使代码更加可读。此外,因为这是一个子类的私有成员,父类构造函数被调用时不能保证它被初始化,所以使用static
关键字可以避免意外使用了未初始化的成员数据。
条款10: 令operator=返回一个referenc to *this
C++的赋值符号=
(assignment operator)具有链式赋值(Chained assignment)特性:
int x, y, z;
x = y = z = 15; // x = (y = (z = 15)));
对于自定义类,应该遵守如下两个规则:
- 返回类型是reference;
- 返回
*this
给左变量。
class Widget{
public:
...
Widget& operator=(const Widget& rhs){ // 返回reference并指向当前对象
...
return *this; // 返回*this给左变量
}
...
}
以上适用于所有赋值相关运算,如:
class Widget{
public:
...
Widget& operator+=(const Widget& rhs){// -=, *=, etc.
...
return *this; // 返回*this给左变量
}
...
Widget* operator=(int rhs){ // 适用。即使参数类型不符合规定
...
return *this;
}
}
条款11: 在operator=
中处理「自我赋值」
自我赋值(self-assignment):
class Widget{};
Widget w;
w = w; // self-assignment
//other situations (aliasing)
a[i] = a[j]; // when i == j
*px = *py; // both are pointers to the same object
在继承体系中,即使两个对象被声明为不同类型,但仍可能发生该情况:
class Base{...};
class Derived : public Base{...}; //同一个继承层级中
void doSomething(const Base& rb, Derived* pd); //rb与pd就可能指向同一个对象
如果使用条款13和条款14运用对象来管理资源则或许是“自我赋值安全的”(self-assignment-safe)。
但如果手动管理资源则可能出现问题。假设用一个class保存一个指针指向一块动态分配的位图(bitmap):
class Bitmap{...}
class Widget{
...
private:
Bitmap *bp;
};
Widget& Widget::operator=(const Widget& rhs){ // unsafe
delete pb;
pb = new Bitmap(*rhs.pb)
return *this;
}
如若传入的参数rhs
为自身,会导致delete语句把*this
自己的资源释放掉,同时也释放掉了rhs
的资源,最后返回的*this
包含了一个损坏的数据,你不能访问不能修改,甚至不能通过delete来为其释放资源,等于说这段空间就凭空消失了,所以这段代码不是自赋值安全的。
方案1:检查传入的参数rhs
是否为*this
但仍不是异常安全的(exception-safe)
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs) //先做一个身份检测
return *this;
delete pb; //如果不是自己,再执行如下操作
pb = new Bitmap(*this.pb); //存在的问题:若new抛出异常(如内存不足)
return *this; //则返回的*this为损坏的数据
}
方案2:重新排列语句
既对自赋值安全,对异常也是安全的。如果现在new的这行抛出了异常,指针pb也不会被提前删除。
Widget& Widget::operator=(const Widget& rhs){
Bitmap *pOrigin = pb; //先保存一个原pb的备份
pb = new Bitmap(*this.pb); //拷贝过来rhs的pb
delete pOrigin; //只删除备份
return *this;
}
方案3:先拷贝再调换(copy and swap技术)
class Widget{
...
void swap(Widget& rhs); //把rhs和*this的数据成员互相调换
...
};
Widget& widget::operator=(const Widget& rhs){
Widget temp(rhs); //拷贝rhs
swap(temp); //将*this的数据与这个拷贝的数据调换
return *this;
}
另一种形式,巧妙利用了C++传值(pass-by-value)会自动生成一份本地拷贝的特性:
Widget& Widget::operator=(Widget rhs){ // 此时rhs是被传对象的副本
swap(rhs);
return *this;
}
条款12: 对复制对象的所有成员变量都拷贝
C++有两种拷贝函数(copying function):拷贝构造函数(copy constructor)和拷贝赋值操作符(copy assignment operator)。如果在自己定义的类中不声明这些拷贝函数,编译器会自动生成。如果我们声明了自己的拷贝函数,程序将会执行我们自己的拷贝函数。
void logCall(const std::string& funcName);
class Customer{
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
private:
std::string name;
};
Customer::Customer(const Customer& rhs):name(rhs.name){
//使用初始化列表
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs){
logCall("Customer copy assignment operator");
name = rhs.name; //拷贝数据
return *this; //返回*this,见条款10
}
当上述代码新增了一个数据成员时,如果没改变拷贝函数,则只能得到一个部分拷贝(partial copy)的对象。同样的情况也发生在继承体系中:
class PriorityCustomer : public Customer{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
private;
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
:priority(rhs.prority){ //使用初始化列表来构造该类的数据成员
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority; //拷贝该类的数据成员
return *this;
}
上面的代码并没有拷贝基类部分的数据成员。基类的数据成员被设定为默认值。
⚠️base class的数据复制过程必须小心(数据往往是private,无法直接访问)。
所以应该直接让derived class的拷贝函数调用相应的base class函数。
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.prority), Customer(rhs){ //要把基类部分也添加进初始化列表
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); //要使用基类的拷贝操作符
priority = rhs.priority;
return *this;
}
C++的这两种拷贝函数有相似的功能和代码,那么我们能不能避免代码重复,让其中一个拷贝函数调用另一个呢?答案是不能。
使用拷贝赋值操作符调用拷贝构造函数,或者使用拷贝构造函数调用拷贝赋值操作符,都是没有意义的。拷贝赋值操作符适用于已经构造好的对象,而拷贝构造函数适用于还没有构造好的对象,所以这种做法在语义上是错误的。
消除代码重复需要做的是:建立一个新的成员函数给两者调用。
这样的函数往往是private且常被命名为
init
。