C++默认调用的函数
当你写一个空类的时候,编译器会默认声明一个编译器版本的copy构造函数,copy assignment操作符、一个析构函数和默认构造函数(都是inline的与public)。
//当声明一个空类的时候编译器默认声明以下函数
//当函数调用的时候,编译器才会创建这些函数
class Empty(){
public:
Empty(){...}; //默认构造函数
Empty(const Empty& rhs){...}; //copy构造函数
~Empty(){...}; //析构函数,non-virtual
Empty& operator= (const Empty& rth) {...}; //copy assignment操作符
};
//如果类的成员是常量或是引用的话,默认的赋值操作是拒绝编译的
//因为C++不允许reference指向不同的对象,更改常量也是不合法的。
//还有一种情况是基类的将copy assignment操作符定义为private的话
//派生类是无法生成默认的copy assgnment操作符函数的(derived class没有权限处理base class的成分)
明确拒绝所不需要的编译器默认生成的函数
书上介绍了两个方法,以copy函数为例,一个是在主动声明private的copy成员,但是却不实现它。另一个就类似上一部分内容说的一个情况,构建一个专门阻止copy的类(用第一个方法),让其他需要阻止copy的类继承于该base class。
class MyClass{
protected:
MyClass(){...}; //允许默认构建构造函数
~MyClass(){...}; //允许默认构建析构函数
private:
MyClass(const MyClass); //只进行声明,但是不实现,此时MyClass也不允许copy操作
};
class A : private MyClass{
//A不允许构建copy函数
};
为多态基类声明virtual析构函数
主要讲如果基类里面有virtual(基本上该类就是多态性质了),那么其析构函数几乎是必须定义为virtual,如果不想让某个类成为base class(并具有多态的性质),那么定义其析构函数没什么意义(反倒导致对象体积增加,不再具有移植性)。当然不是所有的基类都要virtual 析构函数,只有多态基类才需要声明virtual析构函数。(如上面所述的阻止copy的base class就不是多态用途,只是继承,并不需要指派虚析构函数)
多态中子类指针可以赋值给父类指针,继承是指子类可以使用父类的功能。
一些标准容器是(如string)是没有virtual函数的,所以,要是继承的话,可能会导致不确定行为(将派生类指针赋予基类指针并释放)。
class TimerKeeper{
public:
TimerKeeper();
~TimerKeeper(); //non-virtual的析构函数
};
class ALittleTimer : TimerKeeper{
...
};
//设计工厂函数(工厂模式)用来返回实例。
TimerKeeper* getTimerKeeper(){...};
//在调用的时候,如果使用delete的时候(书上建议不要delete,用RAII机制)
//delete的时候可能base成分销毁了,但derived成分没有被销毁
TimerKeeper* ptk = getTimerKeeper();
//getTimerKeeper()可能是返回ALittleTimer对象
...
delete ptk; //此时会导致局部销毁
如果class中有纯虚函数的话(会导致抽象类),析构函数应该是声明为纯虚析构函数,但是由于析构函数的运作是从最derived class的析构开始调用的,然后逐渐调用到base class的析构函数,此时需要对base class析构函数进行定义(纯虚函数通常来说不被定义,在抽象类中声明,派生类中实现),连接器才不会报错。
class AWOV{
public:
virtual ~AWOV() = 0; //声明纯虚的析构函数
};
AWOV::~AWOV(){} //pure virtual析构函数的定义
禁止析构函数吐出异常
禁止析构函数吐出异常,利用try-catch来捕捉异常并吞下或是使用abort来结束程序。
异常的处理机制:在某个函数A中出现异常,并不进行处理,抛出一个异常给A的调用者,这样一层一层的抛出(如果每一层都不进行处理),直至main函数,如果也不进行处理的话,则程序中止。
析构函数如果抛出异常的话,在调用析构函数的时候,程序总会强制性停止,不会返回main函数中止程序,而是在析构函数中直接中止程序。(构造函数抛出异常时则会导致内存泄露)。
//class.cpp
#include "Class.h"
#include <iostream>
Class::~Class() {
std::cout << "i dead" << std::endl;
throw ("ss");
}
void Class::Error() {
throw("error");
}
//main.c
#include <iostream>
#include "Class.h"
using namespace std;
int main()
{
Class* s = new Class();
try {
delete s; //在此处直接中止程序
s->Error(); //如果把上一行注释掉,就不会中止程序,Error产生的异常跳到外层进行处理
}
catch(...){
cout << "you die" << endl;
}
}
在析构函数不得不产生异常的时候,必须将异常吞掉。
不产生异常就好了,但有时候程序员明白异常产生的原因(但不知道怎么处理还是不要catch,直接中止吧),可以在catch中恢复那些程序员可以弥补的数据,如果不能弥补,再进行数据保存。不过异常安全问题实在麻烦,使用异常机制也会有损性能,noexcept 可能是个好选择(拒绝抛出异常)。
例子是在RAII类中的析构函数里面处理异常,如果客户需要对某个操作函数运行期间抛出的异常进行反应,那么应该提供一个普通函数对其进行操作。
//DBConnection中有个函数close可能会出现异常
class DBConnection{
public:
...
void close();
};
class DBConn{
public:
void close() //给客户机会使用函数
{
db.close();
close = true;
}
~DBConn()
{
if(!closed){
try //如果发生异常时吞下异常
{
db.close();
}
catch(...)
{
//制作运转记录,记录下对close调用的失败
std::abort(); //结束程序
}
}
}
private:
DBConnection db;
bool closed;
};
不要在构造与析构过程中调用virtual函数
在创建derived class对象的时候,base class 构造函数的执行先于derived class,所以说,如果base class 的构造函数调用了virtual函数的话,会导致多态失效,该函数是base class的函数,而不是derived class的(此时virtual函数不是virtual函数)。
不单单是virtual函数,dynamic_cast和typeid也会有一样的效果。
同样在析构函数中使用这些函数时,他们也不会变成一个derived class对象(只会属于base class)。
为了避免重复代码,又不允许使用虚函数,书上提出一个解决方案,利用derived class将必要的构造信息向上传递至base class构造函数中(因为你无法使用virtual函数向下进行调用)。
//
class Transaction{
public:
explicit Transaction(const std::string& logInfo); //拒绝隐式转化
void logTransaction(const std::string& logInfo) const; //const成员函数,说明不会修改数据类型
...
};
Transaction::Transaction(const std::string& logInfo){
...
logTransaction(logInfo);
}
class BuyTranction : public Tranction{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters)) //将参数传给基类构造函数
{...}
private:
static std::string createLogString(parameters); //辅助函数通过参数parameters来返回不同的string值。
};
在构建派生类对象的时候,由于基类构造函数肯定是首先构造的,如果不指定调用构造函数,会进行隐式调用基类的构造函数,为了向上向基类传递信息,通过显示调用基类的构造函数并传递某些参数信息。
有个部分不太明白:书上说定义createLogString为static的话,也就不可能意外指向“初期未成熟的BuyTransaction对象内部尚未初始化的成员变量”,猜测的一个原因是static修饰的成员函数是无法访问非static的成员变量的。
令operator= 返回一个reference to *this
理由是随众。不return *this 编译也是能通过的。
处理operator = 中的自我赋值的问题
如果不进行自我赋值的处理的话,当两个指针指向同一个对象的时候,对其中的一个进行销毁,那么另外一个就会指向一个删除了的对象。
书上建议采用证同试验来进行检验是否自我赋值。
class Bitmap{...};
class Widget{
private:
Bitmap* pb;
};
Widget& Widget::operator= (const Widget& rhs){
if(this == &rhs) //证同试验,如果是自我赋值就啥都不做
return *this;
delete pb;
pb = new Bitmap(*rhs.pb); //如果此处产生异常,会导致返回一块被删除的Bitmap
return *this;
}
//改进之后,进行异常安全处理,也保证了自我赋值
Widget& Widget::operator= (const Widget& rhs){
Bitmap* pOrig = pb; //此时留有一个副本,后面发现异常时,可以进行弥补,返回副本
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
//使用copy and swap技术进行异常安全处理
//在待赋值的原对象复制一个副本,对副本进行修改,如果出现异常则返回原对象,没有异常就交换副本与原对象
class Widget{
...
void swap(Widget& rhs);
...
};
Widget& Widget::operator= (const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
复制对象时勿忘记其每一个成分
当你自己声明copying(copy函数和copy assignment操作符)函数的时候,编译器在你忘记复制某些变量的时候很有可能不报错。
在新增加一个成员变量的时候,就要修改copying函数。如果发生继承的话,就必须将连带复制基类的成员变量。
class A{
public:
A(const A& rhs);
A& operator= (const A& rhs);
private:
int a;
};
A::A(const A& rhs) : a(rhs.a)
{...}
A& A::operator= (const A& rhs)
{...}
class B : public A{
public:
B(const B& rhs);
B& operator= (const B& rhs);
private:
int b;
};
class B::B(const PriorityCustomer& rhs)
:b(rhs.b),A(rhs) //调用A的复制构造函数
{...}
B& B::operator= (const B& rhs){
A::operator= (rhs); //必须对A的成分进行赋值
...
}
不允许copy函数与copy assignment操作符之间相互调用,如果是为了消除重复代码,建议建立第三个函数来减少代码重复。