虚函数与虚析构函数原理
Posted siwuxie095
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚函数与虚析构函数原理相关的知识,希望对你有一定的参考价值。
----------------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: