~String()。
多个规则管理析构函数的声明。 析构函数:
const volatile static
。virtual
。
场景:什么时候析构函数应该声明为virtual
举个例子
由很多做法可以记录时间,因此,设计一个TimeKeeper基类和一个派生类作为不同的计时方法:
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper(); // 基类是非non-virtual析构函数
};
class AtomicClock : public TimeKeeper{}; // 原子钟
class WaterClock : public TimeKeeper{}; //水表
class WristWatch : public TimeKeeper{}; // 腕表
很多客户只想在程序中使用时间,不想操心时间怎么计算,这时候我们可以设计factory(工厂)函数,返回指针指向一个计时对象。
Factory函数会"返回一个基类指针,指向新生成的派生类对象",如下:
TimeKeeper* getTimeKeeper(); // 返回一个指针,指向一个TimeKeeper派生类的动态分配对象
其返回的TimeKeeper*对象一定是new出来的。因此为避免资源泄露,必须将每一个返回的对象delete掉:
TimeKeeper* ptk = getTimerKeeper();
delete ptk;
问题是getTimerKeeper返回的指针实际指向一个派生类对象,而其基类是non-virtual析构函数。这会导致重大错误:
怎么解决呢?很简单。给基类一个virtual析构函数。此后删除派生类对象就会如你想要的那样。是的,它会销毁所有的对象,包括所有的派生类成分。
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
};
原则
原因
举个例子
class Point{
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果int占用32bits,那么Point对象可塞入一个64bit缓存区中。甚至,这样一个Point对象可以被当作一个"64bit"量传给其他语言比如C写的函数。然后当Point的析构函数是vritual,形势起了变化。
当Point类内含虚函数,其对象的体积会增加:在32bit计算机系统中将占用64bits(为了存放两个ints)~96bits(两个ints+vptr);在64bit计算机中可能占用64-128bits。因此,为Point添加一个vptr会增加其对象大小的50%~
100%。Point对象不再能塞入一个64bit缓存区
C++的Point对象也不再和其他语言(比如C)内的相同声明有一样的结果,因此也就不再能把它传递到其他语言所写的函数,除非你明确补偿vptr。
什么是纯虚析构函数
class AWOV{
public:
virtual ~AWOV() = 0; // 声明纯虚析构函数
};
// 必须为纯虚析构函数提供一个定义:否则会报错
AWOV::~AWOV(){}; // 定义纯虚析构函数
什么时候要让类声明为纯虚析构函数
为什么必须为纯虚析构函数提供一个定义
析构函数的运作方式是:
也就是说:
虚析构函数 VS 纯虚析构函数
纯虚析构函数 VS 普通析构函数
区别:
当下列事件之一发生时,编译器将隐式调用类的析构函数:
new
使用进行了显式释放delete
。析构函数可以随意调用类成员函数和访问类数据
对析构函数的使用有两个限制:
什么时候需要显示调用析构函数
很少需要显式调用析构函数,可能会导致严重的错误
显式调用析构函数对置于绝对地址的对象进行清理会很有用。
布置new
运算符进行分配。delete
无法释放此内存,因为它不是从免费存储分配。但是,对析构函数的调用可以执行相应的清理。怎么写
s.String::~String(); // non-virtual call
ps->String::~String(); // non-virtual call
s.~String(); // Virtual call
ps->~String(); // Virtual call
C++并不禁止析构函数吐出异常,但是不鼓励这样做。
这是有理由的。
看个例子:
class Widget{
public:
//...
~Widget(){..}; //假设这里会抛出一个异常
};
void doSomething(){
std::vector<Widget> v;
} // v在这里被自动销毁
当v被销毁时,它有责任销毁其内含的所有异常。
本例中它会导致不明确行为。
另外:如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)。这可能造成资源泄漏
场景
假设你使用一个类负责数据库连接:
class DBConnection{
public:
static DBConnection create(); // 创建连接
void close(); // 关闭连接,失败时抛出异常
//...
};
为了确保客户不忘记DBConnection 对象身上调用close,一个常用的方法是用来管理DBConnection资源的类,并在其析构函数中调用close:
class DBConn{ // 这个类用来管理DBConnection 对象
public:
~DBConn{ // 确保数据库连接总是会关闭
db.close();
}
private:
DBConnection db;
}
从而,客户应该这样写:
{ // 开启一个区块
DBConn dbc(DBConnection::create()); // 建立DBConnection 对象并交给DBConn对象以便管理
//... 通过DBConn接口使用DBConnection 对象
} // 在区块结束点,DBConn对象被销毁,因而自动为DBConnection 对象调用close
只要close()成功,ok。但如果该调用异常,DBConn析构函数就会抛出异常。这会很麻烦
怎么处理
DBConn::~DBConn(){
try{db.close();}
catch(...){
// 制作运转记录,记下对close的调用失败
std::abort();
}
}
DBConn::~DBConn(){
try{db.close();}
catch(...){
// 制作运转记录,记下对close的调用失败
}
}
这两个方法都有缺点:它们都无法对"导致close抛出异常"的情况做出反应
一个较佳策略是重新设计DBConn接口, 比如DBConn自己可以提供一个close函数,使客户有机会对可能出现的问题做出反应。
class DBConn(){
public:
void close(){ // 供客户使用的新函数
db.close();
closed = true;
}
~DBConn(){
if(!closed){
try{
db.close();
}catch(...){
// 忽视或者结束程序
}
}
}
}
总之,如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数之外的某个函数。因为析构函数抛出异常是危险的,总会带来"过早结束程序"或者"发生不明确行为"的风险
构造函数与析构函数可以很好的与类层次配合。构造函数会“自上而下”的创建一个类对象
析构函数以相反顺序“拆除”一个对象:
特别的:
struct A1 { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };
struct B1 { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };
int main() {
// 析构函数以相反顺序“拆除”一个对象
A1 * a = new A3; // 1、2、3 ---> 3、2、1
delete a;
printf("\n");
// 非virtual将会发生资源泄露
B1 * b = new B3;
delete b;
printf("\n");
B3 * b3 = new B3;
delete b3;
}
非virtual基类的析构函数以声明基类名称的反向顺序调用。 考虑下列类声明:
class MultInherit : public Base1, public Base2
...
在前面的示例中,先于 Base2 , 再调用 Base1 的析构函数。
#include
#include
using namespace std;
class String {
public:
String( char *ch ); //声明构造函数
~String(); //声明析构函数
private:
char *_text;
size_t sizeOfText;
};
//定义构造函数。
String::String( char *ch ) {
sizeOfText = strlen( ch ) + 1;
//动态分配正确数量的内存。
_text = new char[ sizeOfText ];
//如果分配成功,复制初始化字符串。
if( _text )
strcpy_s( _text, sizeOfText, ch );
}
// 定义析构函数。
String::~String() {
// 释放以前为此字符串保留的内存。
delete[] _text;
}
int main() {
String str("The piper in the glen...");
String p= new String('a');
delete p;
}
#include
using namespace std;
class Base
{
public:
virtual void print() =0; // 普通纯虚函数可以有函数体(可以有但最好不要加)
virtual void print1() = 0 // 普通纯虚函数可以有函数体(可以有但最好不要加)
{
cout<< "Base print1" <<endl;
}
virtual ~Base() = 0 //基类析构函数为纯虚函数时,必须显示加上函数体。
{
cout<< "~Base" <<endl;
}
};
class Deriver:public Base
{
public:
void print()
{
cout<< "Deriver print" <<endl;
}
void print1()
{
cout<< "Deriver print1" <<endl;
}
~Deriver()
{
cout<< "~Deriver" <<endl;
}
};
void main()
{
Deriver d;
d.print();
//d.Base::print();//error
d.print1();
d.Base::print1();
}
场景
如果我们要求某个类的对象:
那应该怎么实现呢?
实现:
有两种方法:
在两种方法中,使用private更为灵活:
class NonLocal{
public:
void destroy() {
this->~NonLocal();
};
private:
~NonLocal() {}; // 析构函数就是无参的
};
void user(){
// NonLocal xl; // error, 对象不能在栈上生成,因为编译器没法(隐式)调用析构函数了
NonLocal *p = new NonLocal ; //ok
p->destroy(); //ok
// delete p; // error, 不能析构一个NonLocal
}
void user(){
//NonLocal xl;
X *p = new NonLocal ; //ok
//delete p; // error, 不能析构一个NonLocal
p.destory(); //ok
}
原因: