我们用 sizeof 来运算有有虚函数的类和没虚函数的类的大小,会是什么结果呢?
class A {public:int i;virtual void Print() { } // 虚函数};class B{public:int n;void Print() { } };int main() {cout << sizeof(A) << ","<< sizeof(B);return 0;}
在64位机子,执行的结果:
16,4
从上面的结果,可以发现有虚函数的类,多出了 8 个字节,在 64 位机子上指针类型大小正好是 8 个字节,这多出 8 个字节的指针有什么作用呢?
01 虚函数表每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着 虚函数表的指针 。「虚函数表」中列出了该类的「虚函数」地址 。
多出来的 8 个字节就是用来放「虚函数表」的地址 。// 基类class Base {public:int i;virtual void Print() { } // 虚函数};// 派生类class Derived : public Base{public:int n;virtual void Print() { } // 虚函数};
上面 Derived 类继承了 Base类,两个类都有「虚函数」,那么它「虚函数表」的形式可以理解成下图:
文章插图
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中 存放的虚函数表的地址 ,在虚函数表中查找虚函数地址,并调用虚函数的指令 。
02 证明虚函数表指针的作用在上面我们用 sizeof 运算符计算了有虚函数的类的大小,发现是多出了 8 字节大小(64位系统),这多出来的 8 个字节就是指向「虚函数表的指针」 。「虚函数表」中列出了该类的「虚函数」地址 。
下面用代码的例子,来证明「虚函数表指针」的作用:
// 基类class A {public:virtual void Func()// 虚函数{cout << "A::Func" << endl;}};// 派生类class B : public A {public:virtual void Func()// 虚函数{cout << "B::Func" << endl;}};int main() {A a;A * pa = new B();pa->Func(); // 多态// 64位程序指针为8字节int * p1 = (int *) & a;int * p2 = (int *) pa;* p2 = * p1;pa->Func();return 0;}
输出结果:B::FuncA::Func
- 第 25-26 行代码中的 pa 指针指向的是 B 类对象,所以 pa->Func() 调用的是 B 类对象的虚函数 Func() ,输出内容是 B::Func ;
- 第 29-30 行代码的目的是把 A 类的头 8 个字节的「虚函数表指针」存放到 p1 指针和把 B 类的头 8 个字节的「虚函数表指针」存放到 p2 指针;
- 第 32 行代码目的是把 A 类的「虚函数表指针」 赋值给 B 类的「虚函数表指针」,所以相当于把 B 类的「虚函数表指针」 替换 成了 A 类的「虚函数表指针」;
- 由于第 32 行的作用,把 B 类的「虚函数表指针」 替换 成了 A 类的「虚函数表指针」,所以第 33 行调用的是 A 类的虚函数 Func() ,输出内容是 A::Func
虚析构函数析构函数是在删除对象或退出程序的时候,自动调用的函数,其目的是做一些资源释放 。
那么在多态的情景下,通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,这就会存在派生类对象的析构函数没有调用到,存在资源泄露的情况 。
看如下的例子:
// 基类class A {public:A()// 构造函数{cout << "construct A" << endl;}~A() // 析构函数{cout << "Destructor A" << endl;}};// 派生类class B : public A {public:B()// 构造函数{cout << "construct B" << endl;}~B()// 析构函数{cout << "Destructor B" << endl;}};int main() {A *pa = new B();delete pa;return 0;}
输出结果:construct Aconstruct BDestructor A
从上面的输出结果可以看到,在删除 pa 指针对象时,B 类的析构函数没有被调用 。解决办法:把基类的析构函数声明为virtual
- 派生类的析构函数可以 virtual 不进行声明;
- 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数,还是遵循「先构造,后虚构」的规则 。
// 基类class A {public:A(){cout << "construct A" << endl;}virtual ~A() // 虚析构函数{cout << "Destructor A" << endl;}};
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 这可能是讲分布式系统最到位的一篇文章
- 从原理到实战,彻底搞懂Nginx
- 计算机网络基础知识,仅此一篇足矣 电脑网络知识
- 如何选轮胎?浅显易懂,一篇搞定
- 彻底搞懂String:字符串常量池
- 一篇文章,教你学会Git
- 一篇文章带你吃透,Java界最神秘技术ClassLoader
- 每月都在交社保,大多数人不会正确使用,一篇文章看懂!
- 彻底搞懂MySQL分区
- 一文搞懂SQL中的所有JOIN