虚函数
被virtual关键字修饰的函数,称为虚函数, 它的应用有以下限制:
类对象有虚函数表,存放所有的虚函数地址
静态函数不能是虚函数,因为静态函数不属于任何一个class object
内联函数不能是虚函数
构造函数不能是虚函数
纯虚函数
纯虚函数是虚函数的一种特例,其格式是:
virtual func() = 0;
纯虚函数和一般虚函数的区别和联系:
一般虚函数必须实现(函数体中允许不做任何事,但是必须实现)
因为一般虚函数做了实现,所以可以被直接调用,但纯虚函数必须在子类中被重载且做出实现后才可以调用
两者都可以在子类中被重载,以实现多态功能
抽象类
至少包括一个纯虚函数的类称为抽象类。
抽象类的构造函数
抽象类不能被实例化为类对象,但是却允许有自己的构造函数,为什么不能实例化?为什么却可以有构造函数?为什么可以声明抽象类类型的指针?
为什么抽象类不能实例化,原因很明显,因为抽象类中至少有一个纯虚函数,纯虚函数没有函数体,抽象类的class object调用一个没有实现的函数没有意义。而且子类必须重新实现所有的抽象父类的纯虚函数以后,才可以实例化对象,否则依然是一个抽象类!
抽象类的构造函数作用是什么?除了在非默认构造函数中做一些初始化的动作,最大的用处是,抽象类需要在构造函数中初始化虚表!
class Abstruct {
public:
Abstruct() {
m_value = 10;
}
~Abstruct() {}
void value() {
cout << m_value << endl; //检测抽象类的构造函数是否被调用
}
virtual void fun() = 0;
private:
int m_value;
};
class Derived : public Abstruct {
public:
Derived() {
value();
}
void fun() {
//必须重写抽象类的虚函数,否则该派生类依然无法实例化
}
};
int main(int argc, const char *argv[])
{
//PureVirtual p; //错误,抽象类不能被实例化
Abstruct *ptr = new Derived(); //正确,可以声明抽象类指针
return 0;
}
C++的对象模型
简单的C++类并没有带来空间上的额外负担(假设这个简单的C++类只有有一个类方法和一个数据成员),每个class object中都有一份属于自己的类数据成员,但是类方法却始终不会出现在class object中,也就是说类对象不会生成一份自己的类方法拷贝!内联的类方法就在调用的地方直接展开,非内联的类方法则只有一份,这和C中的结构体机制是一样的。
C++类的数据成员有两种,静态和非静态的,类方法则分成三种,静态的、非静态的和虚的。虚函数给C++带来了空间上的负担!C++的每一个class object中包含所有的非静态数据成员,而静态数据成员和类方法(不管是静态还是非静态的)都在class object之外,不属于任何一个class object。对于虚成员函数,类为每一个虚函数创造一个指针,并将所有的这种指针放在虚表当中,每个class object中添加一个虚表指针,指向类的虚表的首地址,虚表指针的初始化和销毁由类的构造函数和析构函数完成。
C++对象模型表明,C++的空间负担主要来自抽象类的虚机制。
多态原理
class object中除了非静态数据成员,还有一个虚表指针指向类的虚表,虚表会与类的定义同时出现,这个表存放着该类所有的虚函数地址,找到所有的虚函数。
在子类中重写父类的虚函数(重写也称为覆盖),则子类就有了自己的虚表,子类class object通过函数名调用到的,就是子类自己虚表指向的虚函数,这就是多态的原理。
但如果子类没有重写父类的虚函数,则子类没有自己的虚表,子类class object的虚表指针指向的,依然是父类虚表的指针,最终调用到的就是父类中的虚函数。
实例:虚析构函数
父类之所以需要虚析构函数,是防止在delete指向子类的父类指针的时候,只回收了父类对象的资源而没有正确的回收子类对象资源,从而产生内存泄漏,这种释放局部对象资源的怪异现象,只需要父类定义虚析构函数即可避免。
那么C++是在这一过程中到底做了什么呢?如果父类当中定义了虚析构函数,那么父类的虚函数表中就会有一个父类的虚析构函数指针,指向父类的虚析构函数,子类虚函数表当中也会产生一个子类的虚析构函数指针,指向子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete父类指针,就会通过子类对象找到子类的虚函数表指针找到虚函数表,在虚函数表中找到子类的虚析构函数,从而使得子类的析构函数得以执行,子类的析构函数执行之后系统会自动执行父类的虚析构函数。
1.非虚析构函数
class Base {
public:
Base() {
cout << "构造父类" << endl;
}
~Base() {
cout << "析构父类" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "构造子类" << endl;
}
~Derived() {
cout << "析构子类" << endl;
}
};
int main(int argc, const char *argv[])
{
Base *ptr = new Derived();
delete ptr; //回收资源
return 0;
}
运行结果
2.虚析构函数
一个聪明的做法是:只要父类带有virtual函数(用于继承),那么你就准备以后将它作为父类,它的析构函数就被定义为virtual的。
class Base {
public:
Base() {
cout << "构造父类" << endl;
}
virtual ~Base() {
cout << "析构父类" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "构造子类" << endl;
}
~Derived() {
cout << "析构子类" << endl;
}
};
int main(int argc, const char *argv[])
{
Base *ptr = new Derived();
delete ptr; //回收资源
return 0;
}
运行结果
虚函数的代价
虚函数的代价是虚表导致的体积增大,所以当你不准备将一个类作为父类被其他类继承的时候(也就是类中没有任何一个virtual函数的情况下),就不应该将析构函数定义成virtual的,因为那样做除了增大类对象的体积(内存增大50%-100%),没有任何其他的用处!
总之:如果类中包含其他的virtual函数,最好把析构函数也声明为virtual的;否则,就没必要声明virtual析构函数来增加虚机制,进而增大类对象的体积。
如果不得已要将析构函数声明为纯虚函数(想要一个抽象类但是没有其他的函数可以被声明为纯虚函数),也必须给虚函数一个实现,虽然这样做很奇怪,但是如果析构函数没有函数体,那么在析构子类对象的时候调用到父类的析构函数就会出现链接错误!
参考:
《深度探索C++对象模型》
《Effective C++》