C++虚函数理解
虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态“。
虚函数主要解决的是C++中的多态这一特性。多态分为两类
- 静态多态: 函数重载和运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
虚函数例子
//基类
class Base {
public:
virtual void f() {
cout << "Base::f()" << endl;
}
};
//子类继承基类
class Son :public Base {
public:
virtual void f() {
cout << "Son::f()" << endl;
}
};
void test() {
Base *b;
b = new Son;
b->f();
b->g();
}
int main() {
test();
system("pause");
return 0;
}
Base作为基类,在Base中只定义了虚函数f(),在子类Son中重定义了f()的实现,即使我们定义的都是父类Base,将Base初始化指向一个子类Son(注意这里必须是指针)Base *b = new Son
。这样做的好处是,我们还可以定义Son2、Son3都是继承自Base,他们有各自的f()的实现,我们可以统一都定义为Base *b,但是在开辟堆的时候,可以开辟不同的子类,例如:
Base *b1 = new Son1;
Base *b2 = new Son2;
这样就可以实现多态。
虚函数查看方法(VS2017)
在cmd中找到VS2017的开发人员命令提示符 -> 进入cpp所在文件路径 ->输入
cl /d1 reportSingleClassLayout类名 文件名.cpp
例如:
cl /d1 reportSingleClassLayoutBase 虚函数.cpp
虚函数的实现原理
我们都知道,如果一个类是空(没有成员也没有方法),类的sizeof是1。而如果一个类中只有一个虚函数,他的sizeof是4,正好是一个指针的大小,这个指针就是vfptr,虚函数指针,指向的是虚函数表中的虚函数。虚函数表为vftable。
可以看到Base类,他的vfptr实际上指向了vftable中的某一项,两个虚函数f、g,分别在vftable中占据一项,调用的时候,利用vfptr去vftable中找到对应的函数地址进行调用。
同样的,我们再来看看Son类
可以看到,在子类Son中重写的虚函数,其实就是更新了vftable表中的函数,直接替代掉了父类中的函数,所以我们用子类去初始化父类,在调用函数,调用到的就是子类的函数,而不再是父类的函数。
一般继承(无虚函数覆盖)
对于这种一般继承,子类没有重写父类的虚函数,虚函数表是这样的
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面
一般继承(有虚函数覆盖)
对于这种子类有重写父类的虚函数(我们上面的例子),虚函数表会发生变化:
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
多重继承下的虚函数
多重继承就很简单了,C++允许一个类继承多个类,因此可以让一个Son继承Base1、Base2,他们的虚函数关系就是简单的相加,如果有同名的,全部覆写。
例子:
//基类1
class Base1 {
public:
virtual void f() {
cout << "Base1::f()" << endl;
}
virtual void g() {
cout << "Base1::g()" << endl;
}
};
//基类2
class Base2 {
public:
virtual void f() {
cout << "Base2::f()" << endl;
}
virtual void g() {
cout << "Base2::g()" << endl;
}
};
//子类继承基类
class Son :public Base1, public Base2 {
public:
virtual void f() {
cout << "Son::f()" << endl;
}
virtual void h() {
cout << "Son::h()" << endl;
}
};
子类Son继承了父类Base1、Base2,但是只对父类的f()进行了重写,新引入了一个虚函数h(),我们可以看到此时的虚函数表:
他有两个父类,所以划分了两个虚函数表Base1和Base2,两个指针vfptr分别指向。Base1中的f()和Base2中的f()都被Son重写的f()覆盖了,但是g()保留了,而且子类新引入的h()添加到了Base1表的最后。
用图例简单理解下:
多重继承(无虚函数覆盖)
对应的虚函数表:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
多重继承(有虚函数覆盖)
对应的虚函数表:
1)对于同名的(子类重写的虚函数),直接替换虚函数表中的父类
2)如果子类重写的虚函数只有部分父类有,则只替换那部分父类(按照虚函数名,只要子类重写了,就要替换父类中同名的,一切以子类重写的为准)
3)子类中新定义的虚函数直接放到了第一个父类的表中。(同样的,第一个父类是按照声明顺序来判断的)
纯虚函数
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
当类中有了纯虚函数,这个类也称为抽象类。抽象类的特点:
1.无法实例化对象
2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
示例:virtual void f() = 0;