C++构造函数与析构函数调用顺序及析构函数虚拟的原因
C++ 构造函数与析构函数调用顺序
在 C++ 编程中,构造函数和析构函数扮演着至关重要的角色。构造函数用于在创建对象时初始化对象的成员变量,而析构函数则在对象销毁时执行清理工作,例如释放动态分配的内存。理解它们的调用顺序对于编写正确且高效的代码至关重要。
简单类的构造函数与析构函数调用顺序
考虑一个简单的类 SimpleClass
:
#include <iostream>
class SimpleClass {
public:
SimpleClass() {
std::cout << "SimpleClass constructor called." << std::endl;
}
~SimpleClass() {
std::cout << "SimpleClass destructor called." << std::endl;
}
};
int main() {
SimpleClass obj;
return 0;
}
在上述代码中,当 main
函数执行到 SimpleClass obj;
时,SimpleClass
的构造函数被调用,输出 SimpleClass constructor called.
。当 main
函数结束,obj
的生命周期结束,析构函数被调用,输出 SimpleClass destructor called.
。
包含成员对象的类的调用顺序
当一个类包含其他类的对象作为成员时,构造函数和析构函数的调用顺序遵循特定规则。成员对象的构造函数会在包含类的构造函数体执行之前被调用,而析构函数的调用顺序则相反。
#include <iostream>
class MemberClass {
public:
MemberClass() {
std::cout << "MemberClass constructor called." << std::endl;
}
~MemberClass() {
std::cout << "MemberClass destructor called." << std::endl;
}
};
class ContainerClass {
public:
ContainerClass() {
std::cout << "ContainerClass constructor body starts." << std::endl;
}
~ContainerClass() {
std::cout << "ContainerClass destructor body starts." << std::endl;
}
private:
MemberClass member;
};
int main() {
ContainerClass container;
return 0;
}
在这个例子中,ContainerClass
包含一个 MemberClass
类型的成员 member
。当 ContainerClass
的对象 container
被创建时,首先调用 MemberClass
的构造函数,输出 MemberClass constructor called.
,然后执行 ContainerClass
的构造函数体,输出 ContainerClass constructor body starts.
。当 container
被销毁时,先执行 ContainerClass
的析构函数体,输出 ContainerClass destructor body starts.
,然后调用 MemberClass
的析构函数,输出 MemberClass destructor called.
。
继承体系中的调用顺序
在继承体系中,构造函数和析构函数的调用顺序更为复杂。基类的构造函数会在派生类的构造函数体执行之前被调用,而析构函数的调用顺序则相反,派生类的析构函数先执行,然后是基类的析构函数。
#include <iostream>
class BaseClass {
public:
BaseClass() {
std::cout << "BaseClass constructor called." << std::endl;
}
~BaseClass() {
std::cout << "BaseClass destructor called." << std::endl;
}
};
class DerivedClass : public BaseClass {
public:
DerivedClass() {
std::cout << "DerivedClass constructor body starts." << std::endl;
}
~DerivedClass() {
std::cout << "DerivedClass destructor body starts." << std::endl;
}
};
int main() {
DerivedClass derived;
return 0;
}
当 DerivedClass
的对象 derived
被创建时,首先调用 BaseClass
的构造函数,输出 BaseClass constructor called.
,然后执行 DerivedClass
的构造函数体,输出 DerivedClass constructor body starts.
。当 derived
被销毁时,先执行 DerivedClass
的析构函数体,输出 DerivedClass destructor body starts.
,然后调用 BaseClass
的析构函数,输出 BaseClass destructor called.
。
如果派生类包含成员对象,调用顺序如下:
- 基类的构造函数被调用。
- 派生类中成员对象的构造函数按照声明顺序被调用。
- 派生类的构造函数体被执行。
析构函数的调用顺序则相反:
- 派生类的析构函数体被执行。
- 派生类中成员对象的析构函数按照声明顺序的相反顺序被调用。
- 基类的析构函数被调用。
#include <iostream>
class MemberInDerived {
public:
MemberInDerived() {
std::cout << "MemberInDerived constructor called." << std::endl;
}
~MemberInDerived() {
std::cout << "MemberInDerived destructor called." << std::endl;
}
};
class BaseForComplexInheritance {
public:
BaseForComplexInheritance() {
std::cout << "BaseForComplexInheritance constructor called." << std::endl;
}
~BaseForComplexInheritance() {
std::cout << "BaseForComplexInheritance destructor called." << std::endl;
}
};
class DerivedWithMember : public BaseForComplexInheritance {
public:
DerivedWithMember() {
std::cout << "DerivedWithMember constructor body starts." << std::endl;
}
~DerivedWithMember() {
std::cout << "DerivedWithMember destructor body starts." << std::endl;
}
private:
MemberInDerived member;
};
int main() {
DerivedWithMember obj;
return 0;
}
在这个例子中,当 DerivedWithMember
的对象 obj
被创建时,首先调用 BaseForComplexInheritance
的构造函数,输出 BaseForComplexInheritance constructor called.
,接着调用 MemberInDerived
的构造函数,输出 MemberInDerived constructor called.
,最后执行 DerivedWithMember
的构造函数体,输出 DerivedWithMember constructor body starts.
。当 obj
被销毁时,先执行 DerivedWithMember
的析构函数体,输出 DerivedWithMember destructor body starts.
,然后调用 MemberInDerived
的析构函数,输出 MemberInDerived destructor called.
,最后调用 BaseForComplexInheritance
的析构函数,输出 BaseForComplexInheritance destructor called.
。
析构函数虚拟的原因
多态与对象销毁
在 C++ 中,多态是一个强大的特性,允许通过基类指针或引用操作派生类对象。然而,当涉及到对象销毁时,如果析构函数不是虚拟的,可能会导致未定义行为。
考虑以下代码:
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor called." << std::endl;
}
~Base() {
std::cout << "Base destructor called." << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor called." << std::endl;
data = new int(42);
}
~Derived() {
std::cout << "Derived destructor called." << std::endl;
delete data;
}
private:
int* data;
};
int main() {
Base* basePtr = new Derived();
delete basePtr;
return 0;
}
在上述代码中,basePtr
是一个指向 Derived
对象的 Base
指针。当执行 delete basePtr
时,由于 Base
的析构函数不是虚拟的,只会调用 Base
的析构函数,而 Derived
的析构函数不会被调用。这将导致内存泄漏,因为 Derived
类中动态分配的 data
没有被释放。
虚拟析构函数的作用
为了避免上述问题,需要将基类的析构函数声明为虚拟的。当基类的析构函数是虚拟的时,通过基类指针或引用删除对象时,会根据对象的实际类型调用适当的析构函数。
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor called." << std::endl;
}
virtual ~Base() {
std::cout << "Base destructor called." << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor called." << std::endl;
data = new int(42);
}
~Derived() {
std::cout << "Derived destructor called." << std::endl;
delete data;
}
private:
int* data;
};
int main() {
Base* basePtr = new Derived();
delete basePtr;
return 0;
}
在这个修正后的代码中,Base
的析构函数是虚拟的。当执行 delete basePtr
时,首先会调用 Derived
的析构函数,输出 Derived destructor called.
,释放 data
所指向的内存,然后调用 Base
的析构函数,输出 Base destructor called.
,从而避免了内存泄漏。
虚拟析构函数与继承体系
在复杂的继承体系中,虚拟析构函数的作用更加明显。考虑多层继承的情况:
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor called." << std::endl;
}
virtual ~Base() {
std::cout << "Base destructor called." << std::endl;
}
};
class Middle : public Base {
public:
Middle() {
std::cout << "Middle constructor called." << std::endl;
middleData = new char('A');
}
~Middle() {
std::cout << "Middle destructor called." << std::endl;
delete middleData;
}
private:
char* middleData;
};
class Derived : public Middle {
public:
Derived() {
std::cout << "Derived constructor called." << std::endl;
data = new int(42);
}
~Derived() {
std::cout << "Derived destructor called." << std::endl;
delete data;
}
private:
int* data;
};
int main() {
Base* basePtr = new Derived();
delete basePtr;
return 0;
}
在这个例子中,Base
类有一个虚拟析构函数。当通过 Base
指针 basePtr
删除 Derived
对象时,析构函数的调用顺序是:Derived
的析构函数、Middle
的析构函数、Base
的析构函数。这确保了在继承体系中所有动态分配的资源都能被正确释放。
虚拟析构函数的实现原理
从实现角度来看,虚拟析构函数利用了 C++ 的虚函数表(vtable)机制。当一个类包含虚拟函数(包括虚拟析构函数)时,编译器会为该类生成一个虚函数表。虚函数表是一个函数指针数组,其中每个元素指向一个虚拟函数的实现。
当创建一个对象时,对象的内存布局中会包含一个指向虚函数表的指针(vptr)。当通过基类指针或引用调用虚拟函数时,程序会根据对象的 vptr 找到对应的虚函数表,然后从虚函数表中找到实际要调用的函数。
对于虚拟析构函数,当通过基类指针删除对象时,程序会根据对象的实际类型(通过 vptr 找到虚函数表),调用正确的析构函数。这保证了在多态场景下对象的正确销毁。
注意事项
- 效率考虑:虽然虚拟析构函数对于正确销毁对象至关重要,但它也带来了一些额外的开销。每个包含虚拟函数的对象都需要额外的空间来存储 vptr,并且调用虚拟函数时需要通过虚函数表间接调用,这会增加一定的时间开销。在性能敏感的应用中,需要权衡这种开销。
- 纯虚拟析构函数:在抽象基类中,可以定义纯虚拟析构函数。纯虚拟析构函数必须在类外提供实现,因为派生类的析构函数在调用自身的析构函数体后,会自动调用基类的析构函数。
#include <iostream>
class AbstractBase {
public:
virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {
std::cout << "AbstractBase destructor called." << std::endl;
}
class DerivedFromAbstract : public AbstractBase {
public:
~DerivedFromAbstract() {
std::cout << "DerivedFromAbstract destructor called." << std::endl;
}
};
int main() {
AbstractBase* ptr = new DerivedFromAbstract();
delete ptr;
return 0;
}
在这个例子中,AbstractBase
有一个纯虚拟析构函数,并且在类外提供了实现。当通过 AbstractBase
指针删除 DerivedFromAbstract
对象时,先调用 DerivedFromAbstract
的析构函数,然后调用 AbstractBase
的析构函数。
综上所述,理解 C++ 构造函数与析构函数的调用顺序以及析构函数虚拟的原因对于编写健壮、高效且无内存泄漏的代码至关重要。在实际编程中,应根据具体需求正确设计和使用构造函数、析构函数以及虚拟析构函数。