----------------siwuxie095
关于虚函数和虚析构函数的实现原理,因为涉及到 函数指针,
所以先介绍什么是函数指针
函数指针
如果通过一个指针指向对象,就称其为 对象指针,指针除了可以
指向对象之外,也可以指向函数,就称其为 函数指针
函数的本质,其实就是一段二进制的代码,它写在内存中,
可以通过指针来指向这段代码的开头,计算机就会从开头
一直往下执行,直到函数的结尾,并通过指令返回回来
如果有这么 5 个函数指针,它们所存储的就是 5 个函数的函数地址,当
使用时,如:使用 Fun3_Ptr,就可以通过 Fun3_Ptr 拿到 Fun3() 的函数
入口,当用指针指向函数入口,并命令计算机开始执行时,计算机就会使
得 Fun3() 中的二进制代码不断的得到执行,直到执行完毕为止,其它的
函数也是如此
可能会有人说 函数指针 很神奇,其实,函数的指针与普通的指针,本质上
是一样的,它也是由 4 个基本的内存单元组成,存储着一个内存地址,也
就是 函数的首地址
虚函数的实现原理
看如下实例:
定义一个形状类:Shape,其中有一个虚函数和一个数据成员
再定义一个圆类:Circle,它公有继承了 Shape 类,其中并没有给
Circle 定义一个计算面积的虚函数,即 Circle 所使用的是 Shape 的
虚函数 calcArea() 来计算面积
当实例化一个 Shape 对象时,这个对象中除了数据成员 m_iEdge 之外,
它还会有另一个数据成员:虚函数表指针
虚函数表指针,也是一个指针,占有 4 个基本的内存单元
虚函数表指针,顾名思义,它指向一个虚函数表,该虚函数表会与 Shape
类的定义同时出现
在计算机中,虚函数表也是占有一定空间的,假设虚函数表的起始位置是
0xCCFF,那么这个 虚函数表指针 的值就是 0xCCFF,父类的虚函数表只
有一个,通过父类实例化出来的所有对象,它们的 虚函数表指针 的值都是
0xCCFF,以确保每一个对象的 虚函数表指针 都指向自己的虚函数表
在父类 Shape 的虚函数表中,肯定定义了这样一个函数指针,该函数指针
就是计算面积 calcArea() 这个函数的入口地址,如果 calcArea() 的入口地
址是 0x3355,则虚函数表中 calcArea_ptr 的值就是 0x3355,调用时,就
可以先找到 虚函数表指针,再通过 虚函数表指针 找到虚函数表,再通过位
置的偏移找到相应的虚函数的入口地址(即 函数指针),从而最终找到当前
定义的虚函数 calcArea()
当实例化一个 Circle 对象时,因为 Circle 中并没有定义虚函数,但却从父类
中继承了虚函数 calcArea(),所以,在实例化 Circle 对象时也会产生一个虚
函数表
注意:这个虚函数表是 Circle 的虚函数表,和 Shape 的虚函数表不同,
它的起始位置是 0x6688,但是在 Circle 的虚函数表中,计算面积的函
数指针却是一样的,都是 0x3355
这就能够保证:在 Circle 中去访问父类计算面积的函数 calcArea(),
也能通过 虚函数表指针 找到自己的虚函数表,在自己的虚函数表中
通过偏移找到的计算面积的函数指针 calcArea_ptr,也是指向父类的
计算面积的函数入口
如果在 Circle 中定义了计算面积的函数,又会是怎样的呢?
对于 Shape 类来说,它的情况不变:有自己的虚函数表,并且在
实例化一个 Shape 的对象之后,通过 虚函数表指针 指向自己的
虚函数表,然后虚函数表中有一个指向计算面积的函数指针
对于 Circle 类来说,则有些变化:它的虚函数表和之前的虚函数表
是一样的,但因为 Circle 此时已经定义了自己的计算面积的函数,
所以它的虚函数表中关于计算面积的函数指针,已经覆盖掉了父类
中原有的函数指针的值
即 Circle 类 0x6688 中计算面积的函数指针的值是 0x4B2C,
而 Shape 类 0xCCFF 中计算面积的函数指针的值是 0x3355
二者是不一样的,如果用 Shape 的指针去指向 Circle 的对象,就会
通过 Circle 对象中的 虚函数表指针 找到 Circle 的虚函数表,通过偏
移就能在 Circle 的虚函数表中找到 Circle 的虚函数的函数入口地址,
从而执行子类中的虚函数
函数的覆盖和隐藏
函数的覆盖和隐藏,在 C++ 中用的非常多,笔试和面试时遇到的机会
也非常大
在没有学习多态时,如果定义了父类和子类,父类和子类出现的同名
函数,这就称之为 函数的隐藏,即 父子关系-成员同名-隐藏
在学习多态之后,如果没有在子类中定义同名的虚函数,在子类的虚
函数表中就会写上父类的相应的虚函数的函数入口地址,如果在子类
中也定义了同名的虚函数,那么在子类的虚函数表中就会把原来父类
的虚函数的函数入口地址覆盖一下,覆盖成子类的虚函数的函数入口
地址,这就称之为 函数的覆盖,即 父子关系-虚函数同名-覆盖
虚析构函数的实现原理
虚析构函数的特点是:在父类中通过 virtual 修饰析构函数后,通过
父类指针再去指向子类对象,然后通过 delete 接父类指针,就可以
释放掉子类对象了
有了这个前提,如果使用父类的指针通过 delete 的方式去释放子类的
对象,那么只要能够实现通过父类的指针执行到子类的析构函数即可
看如下实例:给 Shape 和 Circle 都加上虚析构函数
如果 Circle 中不写虚析构函数,计算机会默认给你定义一个虚析构函数,
前提是你在父类中得有 virtual 来修饰父类的析构函数
在使用时:
如果在 main() 函数中通过父类指针指向子类对象,然后通过 delete
接父类指针释放子类对象
此时,虚函数表的工作:
如果在父类中定义了虚析构函数,那么在父类的虚函数表中就会
有一个父类析构函数的函数指针,指向父类的析构函数
而在子类的虚函数表中也会产生一个子类析构函数的函数指针,
指向子类的析构函数(注意:虚析构函数没有覆盖)
当 Shape 的指针指向 Circle 的对象,通过 delete 接 Shape 的
指针时,就可以通过 Circle 对象的 虚函数表指针 找到 Circle 的
虚函数表,通过 Circle 的虚函数表找到 Circle 的析构函数
从而使得子类的析构函数得以执行,子类的析构函数执行完毕后,
系统会自动执行父类的析构函数
程序 1:
Shape.h:
#ifndef SHAPE_H #define SHAPE_H #include <iostream> using namespace std; class Shape{ public: Shape(); ~Shape(); double calcArea(); }; #endif |
Shape.cpp:
#include "Shape.h" Shape::Shape() { //cout << "Shape()" << endl; } Shape::~Shape() { //cout << "~Shape()" << endl; } double Shape::calcArea() { //cout << "Shape::calcArea()" << endl; return 0; } |
Circle.h:
#ifndef CIRCLE_H #define CIRCLE_H #include "Shape.h" class Circle :public Shape { public: Circle(int r); ~Circle(); protected: int m_iR; }; #endif |
Circle.cpp:
#include "Circle.h" Circle::Circle(int r) { m_iR = r; //cout << "Circle()" << endl; } Circle::~Circle() { //cout << "~Circle()" << endl; } |
main.cpp:
#include <stdlib.h> #include "Circle.h" #include <iostream> using namespace std; int main(void) { Shape shape; //对象的大小是其数据成员大小的总和这里对象shape没有任何的数据成员 //理论上其所占的内存单元是0 // //但实际上却打印出了1 这是因为shape在实例化的时候要标明自己的存在 //而C++对一个数据成员都没有的对象用1个内存单元去标记它只是标记它的存在 cout << sizeof(shape) << endl; //指针p是int型的而shape是Shape类型的取地址时不能直接指 //必须使用强制类型转换将Shape类型的地址转换成int型的 int *p = (int *)&shape; //打印出对象shape的地址 cout << p << endl; //cout << (unsigned int)(*p) << endl; Circle circle(100); //这里的circle有一个int型的数据成员m_iR 理论上应该打印出4 //实际上也是4 而不是5 没有加1 //因为它已经有了数据成员能够标记出自己的存在 //不需要额外的内存单元来标记自己的存在 cout << sizeof(circle) << endl; int *q = (int *)&circle; //打印出对象circle的地址 cout << q << endl; //circle的地址第一个位置应该放的就是其数据成员m_iR //即m_iR处在对象地址的第一个位置指针q就是指向了m_iR //这里的 unsigned int 要不要均可 //会打印出实例化circle时传入的100 cout << (unsigned int)(*q) << endl; /*q++; cout << (unsigned int)(*q) << endl;*/ system("pause"); return 0; } //概念: //(1)对象的大小:在类实例化出的对象中,它的数据成员所占据的内存大小 //(注意:是数据成员,而不包括成员函数) // //(2)对象的地址:通过一个类实例化了一个对象,该对象在内存中会占有 //一定的内存单元,该内存单元的第一个内存单元的地址,即对象的地址 // //(3)数据成员的地址:当用一个类去实例化一个对象之后,这个对象当中可能 //有一个或多个数据成员,每一个数据成员所占据的地址,就是这个对象的数据成 //员地址。对象的每一个数据成员,因为数据类型可能不同,所以占据的内存大小 //也有不同,地址也是不同的 // //(4)虚函数表指针:在具有虚函数的情况下,实例化一个对象时,该对象的 //第一块内存中所存储的是一个指针,即虚函数表指针,因为它也是一个指针, //所以占据的内存大小也应该是 4 |
运行一览:
程序 2:
Shape.h:
#ifndef SHAPE_H #define SHAPE_H #include <iostream> using namespace std; class Shape { public: Shape(); ~Shape(); //double calcArea(); //virtual ~Shape(); virtual double calcArea(); }; #endif |
Shape.cpp:
#include "Shape.h" Shape::Shape() { //cout << "Shape()" << endl; } Shape::~Shape() { //cout << "~Shape()" << endl; } double Shape::calcArea() { //cout << "Shape::calcArea()" << endl; return 0; } |
Circle.h:
#ifndef CIRCLE_H #define CIRCLE_H #include "Shape.h" class Circle :public Shape { public: Circle(int r); ~Circle(); protected: int m_iR; }; #endif |
Circle.cpp:
#include "Circle.h" Circle::Circle(int r) { m_iR = r; //cout << "Circle()" << endl; } Circle::~Circle() { //cout << "~Circle()" << endl; } |
main.cpp:
#include <stdlib.h> #include "Circle.h" //通过计算对象的大小来证明虚函数表指针的存在 int main(void) { Shape shape; //当类中出现虚函数或虚析构函数(任一或同时只要有"虚")时 //随着类的实例化,对象的数据成员中产生了虚函数表指针vftable_ptr //(和普通的指针一样占4个基本内存单元)所以应该打印出4 cout << sizeof(shape) << endl; int *p = (int *)&shape; cout << p << endl;//此时shape的前4个内存单元就是虚函数指针所在 cout << (unsigned int)(*p) << endl; //继承了父类的虚函数那么也会产生虚函数表(内含指向成员函数入口的指针) //同时在circle的数据成员中产生指向虚函数表首地址的虚函数表指针 Circle circle(100); cout << sizeof(circle) << endl; int *q = (int *)&circle; //虚函数表指针的地址 cout << q << endl; //虚函数表指针中存的指向虚函数表的地址 cout << (unsigned int)(*q) << endl; q++; //对象的前四个基本内存单元是虚函数表指针的所在 //后四个基本内存单元是m_iR的所在 cout << (unsigned int)(*q) << endl; system("pause"); return 0; } //两个类各自产生自己的虚函数表和虚函数表指针 //(1)当只有Shape类中有double calcArea(); 并带virtual时两个虚函数 //表里指向calcArea()的指针相同(此时Circle类中没有定义calcArea()) //(2)当Circle类中也有 double calcArea();父类中有virtual时两个虚函 //数表里指向calcArea()的指针就不同了 // // //子类中定义了计算面积的函数那么虚函数表中关于计算面积的函数指针就 //【覆盖】掉了父类中的原有指针的值 // //如果没有子类中没有定义同名的计算面积的虚函数那么子类的虚函数表里 //就会使用父类的虚函数表里的相应的函数指针(函数入口地址) // //如果子类也定义了同名的虚函数在子类的虚函数表中就会把原来的父类的 //虚函数地址给【覆盖】掉覆盖成子类的虚函数的函数地址 // // //虚析构函数在父类中通过virtual 修饰析构函数之后通过父类指针指向子类 //对象再通过delete 接父类指针就可以释放掉子类对象 // //理论前提:执行完子类的析构函数就会执行父类的析构函数 // //那么如果我们使用父类的指针通过delete的方式去释放子类的对象只要能做到 //通过父类的指针执行到子类的析构函数就可以实现了 // //虚析构函数没有【覆盖】现象 |
运行一览:
【made by siwuxie095】