虚函数
在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。
注:派生类中的成员函数 与 基类中虚函数同名且同参数的函数,不加virtual也自动成为虚函数。
纯虚函数与抽象类
没有函数体的虚函数称作纯虚函数,包含纯虚函数的类叫抽象类。
注:
- 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象。
- 抽象类的指针和引用可以指向由抽象类派生出来的类的对象。
- 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部 不能调用纯虚函数。
- 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函 数,它才能成为非抽象类。
class A {
private:
int a;
public:
virtual void Print( ) = 0 ; //纯虚函数
void fun() { cout << "fun"; }
};
int main()
{
A a ; // 错,A 是抽象类,不能创建对象
A * pa ; // 对,可以定义抽象类的指针和引用
pa = new A ; //错, A 是抽象类,不能创建对象
return 0;
}
多态
多态的实现是通过虚函数。
多态的作用是增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。
多态的关键在于通过基类指针或引用调用一个虚函数时,编译阶段不能确定到底调用的是基类还是派生类的函数,运行时才能够确定——这叫动态联编。虚函数因为用了虚函数表机制,调用的时候会增加内存开销,具体见下文。
注:在非构造函数,非析构函数的成员函数中调用虚函数,是多态;在构造函数和析构函数中调用虚函数,不是多态。
补:一般多态指的是上述的动态多态,还有一种静态多态是通过函数的重载实现的。
非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。
虚表机制
每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表(数组),该表列出了各个虚函数地址,该类的任何对象中都放着一个虚表指针(32位占4字节,64位占8字节),虚表指针会在运行时动态绑定决定该对象的虚函数地址。
那么虚函数表是在运行时创建的还是编译时创建的呢?虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段确定的。(类的函数的调用并不是在编译时就确定的,而是在运行时才确定的,由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以声明为虚函数。虚函数和虚函数表是两个不同的东西,虚函数的调用是在运行时才确定的,虚函数表是在编译时就已经确定的了 。)
/*——参考自C++ primer一书中的案例——*/
#include "iostream"
using namespace std;
class Base {
public:
int a;
virtual void print1() {};
virtual void print2() { cout << "Base" << endl; };
};
class Derived : public Base {
public:
int b;
void print2() { cout << "Derived" << endl; }; //redefined
virtual void print3() {}; //new
};
int main() {
Derived A;
Base* B = &A;//调用派生类Derived的成员函数 输出Derived
B->print2();
//验证虚表指针占空间32位时输出8,12
cout << sizeof(Base) << "," << sizeof(Derived);
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/fec0a9405ae6364fe3428d18ef6f1336.png)
1、可观察到每个类都创建一个虚函数表,其中:
- Derived类未对继承的Base类中的虚函数print1()进行重写,则地址不变。
- 重写了print2()虚函数,地址则改变。
- 新定义了一个虚函数,则虚函数表新增一个虚函数地址。
2、那Base类与Derived类的字节大小为什么是8,12呢?因为虚表指针也会占用一个存储地址空间。
与此同时,调用虚函数时也要通过虚表指针来寻址,因而需要消耗一定量的时间,但这相较于没有多态人工重写这些函数的时间量小得多。
3、为什么调用Base类的指针下的虚函数,执行的是Dervied中的虚函数呢?
若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
如果派生类没有重定义虚函数,则会使用基类中的虚函数。