C++常见面试题之:构造函数和析构函数能否为虚函数?
简单回答是:构造函数不能为虚函数,而析构函数可以且常常是虚函数。
如果需要详细解释一下,那么我们首先就要了解一下构造函数、析构函数以及虚函数的概念?以及它们的特点或差别。
看文章之前,别忘了关注我们,在我们这里,有你所需要的干货哦!
那些被virtual关键字修饰的成员函数,就是虚函数。
虚函数的作用: 用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离; 用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
下面来看一段代码: (这段代码,是之前写在区别继承和多态文章中的一段代码,这里我们主要用来学习一下虚函数的相关概念。)
#include <iostream>
using namespace std;
// 基类
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
// 派生类
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
return 0;
}
运行结果:Rectangle class area
(Tips: shape中的area函数若没有用virtual定义,则无法实现调用派生类中area函数的目的)
①. 构造函数是干什么的
具有构造函数的类对象被创建时,编译系统为该对象分配内存空间,并自动调用该构造函:即由构造函数完成数据成员的初始化工作。
例如:
class A
{
public:
// 类A的构造函数
// 特点:以类名作为函数名,无返回类型
A()
{
m_value = 0;
}
private:
int m_value; // 私有数据成员
}
此时如果用A类创建一个对象a:
A a;
于是编译系统为对象a的每个数据成员(m_value)分配内存空间,并调用构造函数A()自动地初始化对象a的m_value值设置为0
②. 析构函数是干什么的
析构函数(destructor)是成员函数的一种,它的名字与类名相同,但前面要加
~
,没有参数和返回值。且一个类有且仅有一个析构函数。如果定义类时没写析构函数,则编译器生成默认析构函数。如果定义了析构函数,则编译器不生成默认析构函数。
析构函数在对象消亡时即自动被调用。可以定义析构函数在对象消亡前做善后工作。例如,对象如果在生存期间用 new 运算符动态分配了内存,则在各处写 delete 语句以确保程序的每条执行路径都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用 delete 语句,就能确保对象运行中用 new 运算符分配的空间在对象消亡时被释放。
class A
{
private:
char* p; // 私有数据成员
public:
// 类A的构造函数
// 特点:以类名作为函数名,无返回类型
A(int n);
// 类A的析构函数
// 特点:类名前加~作为函数名,无参数和返回类型
~A();
}
A::A(int n)
{
p = new char[n];
}
A::~A()
{
delete[] p;
}
比如:
A a;
A 类的成员变量 p 指向动态分配的一片存储空间,用于存放字符串。动态内存分配在构造函数中进行,而空间的释放在析构函数 ~A() 中进行。这样,在其他地方就不用考虑释放空间的事情了。
只要对象消亡,就会引发析构函数的调用。
- 从存储空间角度来看 虚函数的调用需要虚函数表(vptr)指针,而该指针存放在对象的内存空间中,在构造函数中进行初始化工作,即初始化vptr,让它指向正确的虚函数表。所以需要调用构造函数才可以创建或初始化它的值,否则即使开辟了空间,该指针也为随机值;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。
- 从多态角度来看 构造一个对象的时候,必须知道对象的实际类型;而虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数,例如通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。那使用虚函数也就没有了实际意义。 在调用构造函数时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。
=>简言之:构造函数不能是虚函数,因为虚函数是基于对象的,构造函数是用来产生对象的,若构造函数是虚函数,则需要对象来调用,但是此时构造函数没有执行,就没有对象存在,产生矛盾,所以构造函数不能是虚函数。
例如:
#include "stdafx.h"
#include "stdio.h"
class A
{
public:
A();
virtual~A();
};
A::A()
{
}
A::~A()
{
printf("Delete class APn");
}
class B : public A
{
public:
B();
~B();
};
B::B()
{
}
B::~B()
{
printf("Delete class BPn");
}
int main(int argc, char* argv[])
{
A *b=new B;
delete b;
return 0;
}
输出结果为:
Delete class B
Delete class A
如果把A的virtual去掉:那就变成了Delete class A 也就是说不会删除派生类里的剩余部分内容,也即不调用派生类的虚函数因此在类的继承体系中,基类的析构函数不声明为虚函数容易造成内存泄漏。所以如果你设计一定类可能是基类的话,必须要声明其为虚函数。
创建一个对象时,我们需要明白指定对象的类型;或写通用函数时,运行根据传入对象的类型确定函数的地址,然后调用该函数。虽然我们可能通过基类的指针或引用去访问它,但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
所以析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则会存在内存泄露的问题。【内存泄漏:析构函数是虚函数,因为若有父类指针指向子类对象存在,需要析构的是子类对象,但父类析构函数不是虚函数,则只析构了父类,造成子类对象没有及时释放,引起内存泄漏。】
例如:
在上述构造函数的例子中,子类B继承自基类A:
A *b = new B;
delete b;
1) 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。
2) 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。
【Tips: B *p = new B; delete p;时也是先调用B的析构函数,再调用A的析构函数。】
构造函数不能声明为虚函数,析构函数可以且常常声明为虚函数,有时甚至是必须声明为虚函数。