C++ 虚函数与动态绑定
1. 虚函数基本概念
基类(Base)我们记作 B ,派生类(Derive)我们记作 D 。有时候会出现这么一种状况:对于某些函数,基类 B 希望它的派生类 D 各自定义适合自身的版本,
此时基类 B 就将这些函数声明成虚函数(virtual function)
。
将一个成员函数声明成虚函数,只需要在函数前添加
virtual
关键字。
Tips:
派生类
必须在其内部对所有重新定义的虚函数
进行声明
。
2. 虚函数的几点说明
- 所有的虚函数都必须有定义。
- 派生类中对继承来的虚函数提供自己的新定义,我们称之为
覆盖
。 - 如果基类把一个函数声明成虚函数,则
该函数在派生类中隐式地也是虚函数
。
3. 动态绑定基本概念
面向对象程序设计基于三个基本概念:数据抽象
,继承
,动态绑定
。此处的动态绑定
是指:我们使用基类的引用或指针
去调用虚函数,将会发生动态绑定即如果基类指针或引用指向的对象是基类对象,则调用基类的该函数
,如果基类的指针或引用指向的是派生类对象则调用的是派生类中的该函数
。
4. 动态绑定的原理剖析
Why 基类对象的指针或引用可实现动态绑定? 这其中的原理是什么呢?
每一个具有虚函数的类都叫做多态类,这个虚函数或者是从基类继承来的,或者是自己增加的。
C++ 编译器必须为每一个多态类至少创建一个【虚函数表(vtable)】,其本质是一个【函数指针数组】,其中存放着这个类所有的【虚函数的地址】及该类的类型信息,其中也包括那些【继承但未改写(Override)的虚函数】
。
————摘自:林锐博士《高质量程序设计指南第三版》
4.1 C++ 空类大小
#include <iostream>
using namespace std;
class A{};
int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //1
return 0;
}
为什么 C++ 空类大小是 1 ?
C++ 不允许任何一个对象大小为 0 ,因为这样无法为该变量分配存储空间。
当类为空时,C++ 编译器会向其中插入一个字节的数据,因此空类类型大小为 1 字节。
4.2 C++ 非空类大小
#include <iostream>
using namespace std;
class A{
int m_a;
};
int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //4
return 0;
}
就本例而言,因为此时 A 含有一个 int 型成员变量,因此编译器不会再给 A 类增加 1 个的数据,所以本例 A 的大小为 4。
总体而言,一个不含虚函数的类,类的大小是【大于或等于】类内所有非静态成员变量的总和。因为存在内存对齐问题,因此可能会大于非静态成员总和。
4.3 成员函数是否占用类的大小?
#include <iostream>
using namespace std;
class A{
void fun(){}
int m_a;
};
int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //4
return 0;
}
由本例可知,非虚成员函数,不占用类的大小。
4.4 含虚函数的类的大小
#include <iostream>
using namespace std;
class A{
virtual void vfun(){}
};
int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //32 位机器 大小为 4
return 0;
}
本例中,类型 A 的大小为 4 。此时类为空类,按理讲应该是 大小为 1。而此时不为 1 ,说明类中含有别的成员==> 此处正是
指向【虚函数表】的指针 【* __vptr】
所占用的存储空间。
4.5 含多个虚函数的类大小
#include <iostream>
using namespace std;
class A{
public:
virtual void vfun1(){}
virtual void vfunc2(){}
virtual void vfunc3(){}
};
int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //32 位机器 大小为 4
return 0;
}
此处可以得知,虚函数不占用类对象的存储空间,所以含有一个以上的虚函数的类对象大小与仅含一个虚函数大小相同。因为:针对每个类,只维护一个【虚函数表(函数指针数组数组)】用于存放该类中虚函数的地址,
每个【含一个及以上虚函数的对象都会含有一个指向该类虚函数表的指针】
。
4.6 非虚函数例子
#include <iostream>
using namespace std;
class B{
public:
void fun(){
cout << "B func" << endl;
}
};
class D:public B{
public:
void func(){
cout << "D func" << endl;
}
};
int main(int argc, char const *argv[])
{
B* b = new B;
b->func(); // label1: B func
D* d = new D;
d->func(); // label2: D func
B* pb = new D;
pb->func();// label3: B func
return 0;
}
- label1 处:
基类指针指向基类对象,自然调用基类中的函数。 - label2 处:
派生类与基类的【函数同名时】,【子类会覆盖掉父类所有的同名函数】。 因此此处调用的是派生类中的同名函数。 - label3处:
此时调用基类中的同名成员,因为不存虚函数故而没有动态绑定,在父类作用域下,自然调用父类的同名函数。
4.7 虚函数例子
#include <iostream>
using namespace std;
class B{
public:
virtual void VFun(){
cout << "B vFunc" << endl;
}
};
class D:public B{
public:
void vFunc(){
cout << "D vFunc" << endl;
}
};
int main(int argc, char const *argv[])
{
B* b = new B;
b->vFunc(); // label1: B vFunc
D* d = new D;
d->vFunc(); // label2: D vFunc
B* pb = new D;
pb->vFunc();// label3: D vFunc
return 0;
}
- label1 处:基类指针指向基类对象,调用的是基类中的 vFunc
- label2 处:
当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数。 - label3 处:
因为 派生类【覆盖】(重写)了基类虚函数,给出了派生类的版本,此时 派生类中【虚函数表内 vFun 函数的指针替换为派生类 vFun 的函数指针】,故而由基类实现了【动态绑定】调用了子类同名函数。