构造函数与析构函数
  • 构造函数和析构函数不能被继承。正因为如此,派生类对象会先调用基类的构造函数,而且只能调用这一次,析构函数也是同理。

  • 如果基类定义了带参数的构造函数,派生类没有定义任何带参数的构造函数,则不能直接调用基类的带参构造函数,程序编译失败。

  • 类的对象声明会调用构造函数,但类的指针不会。例如MyClass p1, *p2;中p1调用构造函数,p2不会。

  • 如果类A中的成员变量有类B的对象,那么声明A对象时,先运行B的构造,再运行A的构造。不过一般用B的指针比较好

  • 构造函数中的成员列表初始化顺序取决于类中声明的顺序;析构函数销毁数据成员的顺序与声明顺序相反

  • 构造函数不能是虚函数。类有一个指向虚函数表的指针用于调用虚函数,这个指针是在构造函数里初始化的,如果构造函数是虚函数,怎么在没有初始化的情况下调用它?

  • 构造函数中最好不要调用虚函数,某些编译器会报错。因为先运行基类构造函数,再运行派生类的。基类的构造函数时已经出现了虚函数表指针,它指向基类的虚函数表,所以基类的构造函数中调用的虚函数是基类的。这样就无法实现多态了,所以没有意义。参考Effective C++条款 9



  • 如果需要手动实现一个析构函数,通常意味着类实例中有动态分配的内存空间,只有这种情况,需要程序员手动释放分配的内存空间。 同时也需要实现拷贝构造函数和重载赋值运算符。

  • 析构函数无返回类型,只能有一个,也就不能重载。

  • 继承体系中,析构函数执行的顺序: 派生类析构 —- 类成员的析构 —- 基类析构

函数的局部对象是逆序销毁的

1
2
3
4
5
6
int main()
{
Foo f;
Base b;
return 0;
}

运行结果为:

1
2
3
4
Foo construct 
base constructor
base destructor
Foo destruct

构造函数不难理解,当然按顺序,析构是逆序执行的。看完这个例子就会发现,派生类对象在内存中的生存销毁和它很相似:基类构造——派生类构造——派生类析构——基类析构,相当于先有个基类对象,再有派生类对象

更广泛的结论:

  • 全局对象或全局静态对象不管在什么位置定义,它的构造函数都在main()函数之前执行
  • 所有在stack上的对象都比在全局或静态对象早销毁。
  • 不管是在栈上的对象,还是全局或静态对象,都遵循这样的顺序: 越是先产生的对象越是后被销毁

虚析构函数

假如一个类没有派生类,那么析构函数可以不是虚函数。 如果有派生类,析构函数要声明为虚函数,这是为了防止新手犯错误。如果基类指针指向了派生类的对象,析构时,只会调用基类析构函数,没有派生类的析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
Base(){ }
~Base(){ // 应加上virtual声明
qDebug()<<"Base destructor";
}
};

class Derived:public Base
{
public: Derived(){ }
~Derived(){
qDebug()<<"Derived destructor";
}
};

Base* p = new Derived();
delete p;

运行结果是Base destructor。给Base类析构函数加上virtual声明后,运行结果:
1
2
Derived destructor
Base destructor

一开始我没加virtual,仍然调用了派生类的析构函数,后来发现是基类习惯性继承了QObject类,而后者的析构函数是virtual声明的。于是有结论: 对于继承树,只要在根基类上声明虚析构函数就可以,所有的子类的析构函数也就是虚的。

如果析构函数是虚函数,运行时会动态绑定到派生类的析构函数.如果不是虚函数,想安全删除pBase必须delete (Derived*)p. 

可能有人要问了,为什么C++不直接把虚析构函数作为默认值?原因是虚函数表的开销以及和C语言的类型的兼容性。有虚函数的对象增加一个指针

不能随意声明虚析构函数,注意以下原则:

  • 类至少包含一个虚函数的时候,需要虚的析构函数,因为它必然有派生类
  • 类有派生类时,需要虚的析构函数,否则不需要

补充 override关键字

C++11新特性中的关键字override,编译器会检查基类中的虚函数和派生类中带有override的虚函数有没有相同的函数签名,一旦不匹配便会报错。 因此在子类析构函数后增加关键字override,一旦父类缺少关键字virtual就会被编译器发现并报错。 override 要求必须是虚函数且父类的虚函数必须有virtual关键字,函数的参数列表和返回值也必须相同。

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
virtual ~Base() { ::printf("base\n"); }
};

class Derived
: public Base
{
public:
~Derived() override { ::printf("derived\n"); }
};