12.4 析构函数
1. 在类的定义中,使用一种特殊的声明语法:函数说明符(可选) + ~ + 析构函数的类名 + 空括号 来声明析构函数。在这个声明中, ~ 类名 可以被可选的圆括号括起来,圆括号被忽略。命名一个类的 typedef-name 也是个类名,但是这个 typedef-name 却不能用作析构函数的标识符。
2. 析构函数用来销毁该类的对象。它没有参数,不能指定返回类型(甚至 void 也不允许)。不要试图获取析构函数的地址。析构函数不能是静态的。它可以被 const 、 volatile 或 const volatile 的对象调用。析构函数不能声明为 const 、 volatile 或 const volatile 类型,因为 const 和 volatile 的语义对于一个正在销毁的对象来说是不适用的,这些语义在对象一旦开始销毁时就失效了。
3. 如果用户没有定义析构函数,编译器会隐式声明一个。隐式声明的析构函数是 inline public 属性的成员。满足以下条件,析构函数就是 trivial 的:
a) 所有直接基类具有 trivial 的析构函数
b) 对于所有非静态数据成员来说,如果它是类(或对象数组),那每个这样的数据成员都得有 trivial 的析构函数
4. 否则,析构函数就是非 trivial 的。
5. 当销毁对象时,隐式声明的析构函数会被隐式地定义。如果隐式定义的析构函数有如下情况之一,它就是非法的:
a) 静态数据成员是类(或对象数组),但它们的析构函数不可访问
b) 基类的析构函数不可访问
在隐式定义的析构函数被隐式定义之前,该类的所有基类的析构函数、该类的所有非静态数据成员的析构函数 都必须已经定义好。【注:隐式声明的析构函数还会有一个异常说明。】
6. 类 X 在执行了析构函数体并销毁了析构函数体内定义的自动对象之后,它调用直接成员、直接基类、的析构函数;如果 X 是最继承类,它还会调用虚基类的析构函数。所有析构函数都被当做限定了的名字来调用 ,也就是说,忽略任何外层派生类可能的虚析构函数覆盖。基类与数据成员以与其构造完成时相反的顺序被销毁。析构函数中的 return 语句不会直接返回到调用者;在将控制权交给调用者之前,成员和基类的析构会被调用。数值元素的析构顺序与它们的构造完成顺序相反。
7. 析构函数可以声明为虚函数或纯虚函数,如果在程序中该类被实例化,或该类的继承类被实例化,该析构函数应该被定义。如果基类的析构是虚函数,那该类的析构函数也必然为虚函数(无论是否用户自己定义)。
8. 【在析构过程中使用析构函数,在一些语法结构中有特殊的语义。】 【按:失去虚函数的语义】
9. union 的成员如果是个类(或对象数组),那么这个类的析构函数必须是 trivial 的
10. 已创建的静态存储类型的对象,其析构函数在程序结束的时候调用;已创建的自动存储类型的对象,其析构函数在定义该对象的块结束的时候调用;已创建的临时对象,其析构函数在该临时对象生命周期结束的时候调用;用 new 表达式创建的对象,其析构函数在 delete 表达式中调用,某些情况下也可能在异常处理中调用。如果类中的对象成员或对象数组成员的析构函数在该类的声明点处不可访问,程序就是非法的。析构函数还可以被显式地调用。
11. 在定义虚析构函数时(包括隐式定义),会在类中查找非定点的操作符 delete ,如果找到了,该操作符 delete 应该是可访问、且无二义性的。【注:这条规则确保了对应对象动态类型的操作符 delete 是可以执行 delete 表达式的。】
12. 在显式调用析构函数时,析构函数可以是 ~ + typedef-name 的形式。 析构函数的调用遵循普通成员函数的调用规则,也就是说,如果对象的类型不是析构函数所在的类、也不是该类的派生类,程序的行为是未定义的(除非对 NULL 进行 delete 操作,都是无效果)。【例:
struct B {
virtual ~B() { }
};
struct D : B {
~D() { }
};
D D_object;
typedef B B_alias;
B* B_ptr = &D_object;
void f() {
D_object.B::~B(); // calls B ’s destructor
B_ptr->~B(); // calls D ’s destructor
B_ptr->~B_alias(); // calls D ’s destructor
B_ptr->B_alias::~B(); // calls B ’s destructor
B_ptr->B_alias::~B_alias(); // error, no B_alias in class B
}
】
【注:显式析构函数的调用必须使用类域解析符,特殊的地方是,成员函数中的单目表达式 ~X() 不是显式析构调用。】【 按:加类域解析符就是为了避免与这种单目表达式(创建一个X 的临时对象,将其转换为bool 类型,然后取逻辑非)的二义性。】
13. 【注:显式调用析构函数的情况比较罕见。一个用途是对特定地址的对象(用定点 new 表达式创建的)进行析构。这种用法在处理特定的硬件资源和编写内存管理程序的时候有用。例如:
void* operator new(size_t, void* p) { return p; }
struct X {
// ...
X(int);
~X();
};
void f(X* p);
void g() // 比较罕见的用法
{
char* buf = new char[sizeof(X)];
X* p = new(buf) X(222); // 使用 buf[] 并初始化
f(p);
p->X::~X(); // 清除
}
】
14. 一旦对一个对象调用了析构函数,该对象就不存在了 ;对生命周期已经结束的对象调用析构函数,该行为是未定义的。【例:如果一个自动对象的析构函数被显式地调用了,在块结束的时候会隐式地再调用一次析构函数,该行为是未定义的。】
15. 【注:显式析构函数的调用语法可以用在标量类型名字上,这么做可以让写代码更方便:不必知道一个类型是否有析构函数。例如:
typedef int I;
I* p;
// ...
p->I::~I();
】