构造和析构函数浅析

一题简单的构造、析构函数题

#include <iostream>
using namespace std;

struct BaseClass
{
	char ch;
	BaseClass():ch('a') {cout<<ch<<"1";}
	~BaseClass()		{cout<<ch<<"2";}
};

struct MyClass:public BaseClass
{
	MyClass()			{cout<<ch<<"3";}
	~MyClass()			{cout<<ch<<"4";}
};

int main(void)
{
	MyClass x1;
	x1.ch='b';
	MyClass x2;
	x2.ch='c';
	return 0;
}

——出自人人网2011实习生招聘试题

输出结果:a1a3a1a3c4c2b4b2

构造函数(Constructor)由编译器在定义对象时调用,传递到构造函数的第一个参数是this指针,也就是调用这一函数的对象的地址,但是该this指针指向一个没有被初始化的内存块,构造函数的作用就是正确的初始化该内存块。
析构函数(Destructor )在当对象超出它的作用域时由编译器自动调用。析构函数不需要任何参数。

分析上面的代码可以知道,main函数中首先构造和析构的过程是:

  1. 构造x1
  2. 构造x2
  3. 析构x2
  4. 析构x1

又由于继承关系,构造总是从类层次的最根处开始,所以构造MyClass的对象时,首先会调用基类的构造函数,然后再调用成员对象构造函数。调用析构函数则与构造函数次序相反。所以我们细化步骤可得:

  1. 构造x1:BaseClass():ch(‘a’)–>MyClass()   output:a1a3
  2. 构造x2:BaseClass():ch(‘a’)–>MyClass()   output:a1a3
  3. 析构x2:~MyClass()–>~BaseClass()   output:c4c2  (p.s.析构发生在x2.ch=’c’赋值之后)
  4. 析构x1:~MyClass()–>~BaseClass()   output:b4b2
int main(void)
{
	MyClass x1;
        // MyClass::MyClass() 一般被安插在这儿
	x1.ch='b';
	MyClass x2;
        // MyClass::MyClass() 一般被安插在这儿
	x2.ch='c';
	return 0;
        // MyClass::~MyClass() 一般被安插在这儿
}

如果上述类定义中不含任何构造函数,那么会有一个默认构造函数被暗中生成,而这个暗中生成的默认构造函数通常是不做什么事的(无用的),下面四种情况除外。

  • 包含有带默认构造函数的对象成员的类
  • 继承自带有默认构造函数的基类的类
  • 带有虚函数的类
  • 带有一个虚基类的类

第一种情况,例如类A包含两个数据成员对象,分别为:string str和char*  Cstr,那么编译器生成的默认构造函数将只提供对string类型成员的初始化,而不会提供对char*类型的初始化。
第三种情况,带有虚函数的类,多了一个vptr,而vptr的设置是由编译器完成的,因此编译器会为类的每个构造函数添加代码来完成对vptr的初始化。
第三种情况,编译器需要初始化虚基类在类中的位置。

关于构造函数、析构函数和(纯)虚函数之间的关系注意点:

  • 构造函数不能是虚函数
    假设构造函数是虚函数,函数调用时就需要通过vptr来查找 vtable确定调用哪一个函数,但是对象此时还没有实例化,也就是说内存空间还没有初始化(包括vptr),也就无法查找vtable,无法实现动态绑定。
    (虚函数必须通过查虚函数表来获得函数地址,而调用构造函数的时候连对象都还没有,哪来的虚表?)
  • 析构函数可以是虚函数,也可以是纯虚函数,但必须有定义体(析构函数定义为纯虚不是个好设计)
    析构函数的任务是释放内存,它必须确切知道被释放的对象的类型,否则可能破坏有用的数据,产生不可预知的后果。例如,我们用基类指针指向了派生类对象,那么释放内存时,必须是释放派生类对象的存储空间。所以,析构函数经常被声明为虚函数。
    纯虚函数允许只声明而不定义,若将析构函数设计为纯虚函数且只声明不定义,则继承类会遇到链接错误。我们完全可以为纯虚函数指定函数体,但是通常的纯虚函数不需要函数体,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。
    (析构函数很多时候必须是虚函数,因为如果通过一个基类的引用来析构一个派生类,那么如果析构函数不为虚函数,那么只能析构派生类的基类部分。class B {};class D : public B {};B &b = new D();这就叫用基类的引用指向一个派生类,当b的生命周期到了时,需要析构。C++规定一个类必须有析构函数,所以析构函数不能为纯虚,但有时候你希望定义一个抽象类却没有纯虚函数,这时就把析构函数声明为纯虚,但编译器又规定析构函数必须有函数体,所以必须加上函数体。)
  • 不要在构造/析构函数中调用虚函数
    在构造函数中调用虚函数,实际执行的是父类的对应函数,也就是说,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。当一个base类的构造函数中含有对虚函数vf()的调用,当其派生类的构造函数调用基类构造函数的时候,其中调用的虚函数vf()是base类中的实体,而不是derived中的实体。因为vptr初始化的位置是在所有基类构造函数调用之后,在程序员供应的代码或是成员初始化队列之前。
    析构函数则不是因为调用时还未产生派生类的版本信息,而是因为派生类的版本信息已经不可靠了。由于析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。所以当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,若再调用虚函数,就相当于 对一些不可靠的数据进行操作,这是非常危险的。
    若要在构造函数或析构函数中调用虚函数,应当直接以静态方式调用,而不要通过虚函数机制。

One thought on “构造和析构函数浅析”

  1. 这个例子好,其实以前比较怕这种子类直接用父类值甚至改变父类值这种,存在的价值就是考

Leave a Reply

Your email address will not be published. Required fields are marked *