C++虚函数表解析
C++虚函数及虚函数表解析
C++虚函数及虚函数表解析虚函数的定义: 虚函数必须是类的⾮静态成员函数(且⾮构造函数),其访问权限是public(可以定义为private or proteceted,但是对于多态来说,没有意义。
),在基类的类定义中定义虚函数的⼀般形式: virtual 函数返回值类型虚函数名(形参表) { 函数体 } 虚函数的作⽤是实现动态联编,也就是在程序的运⾏阶段动态地选择合适的成员函数,在定义了虚函数后, 可以在基类的派⽣类中对虚函数重新定义(形式也是:virtual 函数返回值类型虚函数名(形参表){ 函数体 }),在派⽣类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。
以实现统⼀的接⼝,不同定义过程。
如果在派⽣类中没有对虚函数重新定义,则它继承其基类的虚函数。
当程序发现虚函数名前的关键字virtual后,会⾃动将其作为动态联编处理,即在程序运⾏时动态地选择合适的成员函数。
实现动态联编需要三个条件: 1、必须把需要动态联编的⾏为定义为类的公共属性的虚函数。
2、类之间存在⼦类型关系,⼀般表现为⼀个类从另⼀个类公有派⽣⽽来。
3、必须先使⽤基类指针指向⼦类型的对象,然后直接或者间接使⽤基类指针调⽤虚函数。
定义虚函数的限制: (1)⾮类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。
实际上,优秀的程序员常常把基类的析构函数定义为虚函数。
因为,将基类的析构函数定义为虚函数后,当利⽤delete删除⼀个指向派⽣类定义的对象指针时,系统会调⽤相应的类的析构函数。
⽽不将析构函数定义为虚函数时,只调⽤基类的析构函数。
(2)只需要在声明函数的类体中使⽤关键字“virtual”将函数声明为虚函数,⽽定义函数时不需要使⽤关键字“virtual”。
(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的⾮虚函数。
C++中虚函数工作原理和(虚)继承类的内存占用大小计算
C++中虚函数工作原理和(虚)继承类的内存占用大小计算一、虚函数的工作原理虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。
典型情况下,这一信息具有一种被称为vptr(virtual table pointer,虚函数表指针)的指针的形式。
vptr 指向一个被称为vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到vtbl。
当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的vptr 指向的vtbl,然后在vtbl 中寻找合适的函数指针。
虚拟函数的地址翻译取决于对象的内存地址,而不取决于数据类型(编译器对函数调用的合法性检查取决于数据类型)。
如果类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。
而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。
所以,由于对象的内存空间包含了虚表入口,编译器能够由这个入口找到恰当的虚函数,这个函数的地址不再由数据类型决定了。
故对于一个父类的对象指针,调用虚拟函数,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。
虚函数需要注意的大概就是这些个地方了,之前在More effective C++上好像也有见过,不过这次在Visual C++权威剖析这本书中有了更直白的认识,这本书名字很牛逼,看看内容也就那么回事,感觉名不副实,不过说起来也是有其独到之处的,否则也没必要出这种书了。
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。
深入浅出C++虚函数表
深入浅出C++虚函数表为什么要了解虚函数表了解虚函数表对理解C++实现多态的机制会有更深的了解,对对象的内存布局会有更好的认识。
验证虚函数表的存在(C++中的struct和class实际上是一样的)C++中当一个类中存在virtual函数(虚函数)或者它的父类中存在虚函数,那么编译器就会为这个类生成虚函数表(virtual table),下面我用代码来验证这个事情。
首先写一个classclass A{};cout<<sizeof(A)<<endl;输出结果1,就是说这个类占1个字节(具体为什么要占1个字节我也不太清楚,应该就这么设计的吧,有高手请告诉我为什么)。
下面改写这个类,为其加上虚函数class A{public: virtual int add(int i);};cout<<sizeof(A)<<endl;输出4。
我们知道函数不会影响类的大小,函数的地址是另外被存在一片内存区域。
struct Super{int data;virtualint add(int i){return i;};virtual string toString()=0;};class Sub: public Super{public:string toString(){return string("Sub class");}};Super* s = new Sub();cout<<s->toString()<<endl;delete s;输出”Sub class”。
Super vtableSuper:: add地址Super::toString地址Sub vtableSuper::add地址Sub::toString地址int data;编译器会把虚表编译成上面的样式,注意两件事情:第一,虚函数表在类所有成员的最前面。
虚函数表的工作原理
虚函数表的工作原理
虚函数表(virtual function table),也称为虚表(vtable),是
C++语言中实现动态多态性的机制之一。
虚函数表是每个带有
虚函数的类的特殊数据结构,它存储了该类的虚函数的地址。
当一个类定义了虚函数时,编译器会为该类生成一个虚函数表。
虚函数表是一个静态数组,其中的每个元素是一个函数指针,指向对应的虚函数实现代码的地址。
每个对象实例都包含一个指向该类对应的虚函数表的指针。
这个指针通常被称为虚函数指针或虚表指针。
通过这个指针,程序可以访问到正确的虚函数表,并调用相应的虚函数。
当调用一个虚函数时,实际上是通过对象的虚函数指针找到对象的虚函数表,然后再根据函数在虚函数表中的索引找到对应的函数指针,最后通过函数指针调用虚函数的实现代码。
这个过程被称为虚函数的动态绑定或运行时多态性。
使用虚函数表的好处是,它使得在继承关系中,子类可以重写父类的虚函数,并且通过父类指针或引用调用虚函数时,能够正确地调用到子类中的虚函数实现。
这样可以实现多态性,使得程序在运行时能够根据实际对象类型动态地调用适当的函数。
c语言虚函数
C语言虚函数中的特定函数简介C语言是一种面向过程的编程语言,并不直接支持面向对象的概念,其中包括了“类”、“对象”、“继承”等概念。
然而,通过使用一些技巧和设计模式,我们可以在C语言中实现类似于面向对象的功能,其中一个重要的概念就是虚函数。
虚函数是一种特殊的函数,它可以在派生类中被重写,从而实现多态。
虚函数的定义、用途和工作方式是C语言中面向对象编程的重要部分,本文将详细介绍这些内容。
虚函数的定义在C语言中,虚函数的定义需要使用函数指针和结构体实现。
我们可以使用函数指针将一个函数地址赋值给一个结构体中的成员变量,从而形成一个具有特定功能的“方法”。
这样,我们就可以通过这个函数指针来调用结构体中的函数,实现类似于面向对象中对象的方法调用的功能。
下面是一个虚函数的定义示例:typedef struct {void (*function_ptr)(void);} VTable;void function1(void) {printf("This is function1\n");}void function2(void) {printf("This is function2\n");}VTable vtable = {.function_ptr = function1};在上述示例中,我们使用typedef定义了一个VTable结构体,其中有一个function_ptr成员变量,它是一个指向函数的指针。
我们定义了两个函数function1和function2,并分别赋值给了vtable中的function_ptr成员变量。
虚函数的用途虚函数的主要用途是实现多态,使不同类型的对象可以调用相同的接口名称,但执行不同的操作。
通过使用虚函数,我们可以在C语言中实现类似于面向对象的继承和多态的功能。
在面向对象的编程中,我们可以定义一个基类(或接口),然后派生出不同的子类,每个子类都可以重写基类的虚函数,以实现它们自己的特定行为。
C++ 虚函数[详讲]
什么是虚函数?简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。
为什么要引入虚函数?虚函数的作用是实现类的继承所体现的多态性,具体点是实现动态联编。
从程序的角度上来说,在定义了虚函数后,可以在派生类中对虚函数重新定义,以实现统一的接口,不同定义过程,在程序的运行阶段动态地选择合适的成员函数。
什么是多态性?简单点说,多态性是将接口与实现进行分离;C++实现运行时多态性的关键途径:在公有派生情况下,一个指向基类的指针可用来访问从基类继承的任何对象。
语法:普通函数的前面加上virtual[cpp]view plaincopyprint?1.virtual函数返回值类型虚函数名(形参表)2.{3.//函数体4.}虚函数的调用方式:只能通过指向基类的指针或基类对象的引用来调用虚函数调用语法:[cpp]view plaincopyprint?1.指向基类的指针变量名->虚函数名(实参表)2.基类对象的引用名. 虚函数名(实参表)注意:正常情况下,如果不把函数声明为虚函数,指向基类的指针的访问情况如下:1)基类指针指向基类对象:基类指针可以直接访问基类对象中的成员2)基类指针指向派生类对象:基类指针只能访问派生类中的从基类中继承的成员,派生类有同名的函数或成员,也只能调用基类的成员。
如果定义成虚函数时:定义一个基类指针,把不同的派生类对象付给它,会调用对应派生类的函数,而非基类函数。
举例:[cpp]view plaincopyprint?1.#include <iostream>ing namespace std;3.class A4.{5.public:6.virtual void show()7. {8. cout<<"A"<<endl;9. }10.};11.class B:public A12.{13.public:14.void show()15. {16. cout<<"B"<<endl;17. }18.};19.class C:public A20.{21.public:22.void show()23. {24. cout<<"C"<<endl;25. }26.};27.void main()28.{29. A*a;30. B b;31. C c;32. a=&b;33. a->show();34. a=&c;35. a->show();36. system("pause");37.}运行结果:B(换行)C(换行)--指向不同的派生类,调用不同的函数如果不加基类A中的Virtual,则输出结果:A(换行)A(换行)--基类指针,调用派生类中继承的基类成分定义虚函数,实现动态联编需要三个条件:1)必须把动态联编的行为定义为类的虚函数---定义虚函数2)类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来---类之间是公有继承3)基类指针指向派生类的对象,然后使用基类指针调用虚函数注意:1、使用时,虚函数可以在基类中声明,提供界面。
c++11 虚函数表原理
c++11 虚函数表原理C++的虚函数表(vtable)是C++实现多态性的一种重要机制。
虚函数表本质上是一个函数指针数组,每个类都会有一个与之关联的虚函数表。
以下是虚函数表的基本原理:虚函数:在C++中,如果一个成员函数在基类中被声明为虚函数,那么在派生类中,该函数会被重写。
这意味着当通过基类指针或引用调用该函数时,会调用派生类中的版本,而不是基类中的版本。
虚函数表:每个包含虚函数的类都会有一个与之关联的虚函数表。
这个表是一个函数指针数组,数组中的每个元素都是指向类中某个虚函数的指针。
虚指针:在每个包含虚函数的类的对象中,都会有一个虚指针(vptr),它指向类的虚函数表。
当通过基类指针或引用调用虚函数时,会先通过虚指针找到对应的虚函数表,然后根据函数的偏移量找到正确的函数进行调用。
动态绑定:由于虚函数表的存在,C++可以在运行时根据对象的实际类型确定要调用的函数,这就是所谓的动态绑定或晚期绑定。
以下是虚函数表的基本使用和创建过程:cppclass Base {public:virtual void foo() {}};class Derived : public Base {public:void foo() override {}};int main() {Base* base = new Derived;base->foo(); // 实际上会调用Derived类的foo()方法delete base;return 0;}foo函数在Base类中被声明为虚函数,然后在Derived 类中被重写。
当我们通过Base类的指针调用foo函数时,会根据对象的实际类型(在这里是Derived)来调用正确的版本。
这就是多态性的体现。
c语言实现虚函数表
c语言实现虚函数表在C语言中实现虚函数表通常涉及结构体和函数指针的使用。
虚函数表是面向对象编程中用于实现多态的一种技术,它包含了指向各个虚函数的指针,使得在运行时能够动态地调用相应的函数。
以下是一个简单的示例来说明如何在C语言中实现虚函数表:c.#include <stdio.h>。
// 定义一个结构体来表示类。
typedef struct {。
void (func1)();void (func2)();} VirtualTable;// 定义一个类。
typedef struct {。
VirtualTable vtable;int data;} MyClass;// 定义类的成员函数。
void func1_impl() {。
printf("Function 1\n"); }。
void func2_impl() {。
printf("Function 2\n");}。
// 初始化虚函数表。
VirtualTable myClassVTable = {func1_impl, func2_impl}; // 初始化类的实例。
MyClass myObject = {&myClassVTable, 123};int main() {。
// 调用虚函数。
myObject.vtable->func1();myObject.vtable->func2();return 0;}。
在上面的示例中,我们首先定义了一个虚函数表`VirtualTable`,它包含了指向两个成员函数的指针。
然后我们定义了一个类`MyClass`,它包含了一个指向虚函数表的指针`vtable`和一些数据成员。
接着我们定义了两个成员函数`func1_impl`和`func2_impl`,并初始化了虚函数表。
最后在`main`函数中,我们通过类的实例`myObject`的虚函数表指针来调用相应的函数。
C++虚函数表结构及内存布局
C++虚函数表结构及内存布局C++虚函数表结构及内存布局最近被问及C++的多态实现原理,老实说,除了知道个一个指针和虚表的概念,其他并不了解。
今天稍微闲点,回头来拿指针指來指去的喵喵虚表到底是个什么结构。
一切建立在虚表存在和虚表指针存在且存在在对象首部的理论基础上。
class Base{public:virtual void f() { cout << "Base::f" << endl; }void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }private:int n;};class Derive1 : public Base{ // 重写一个虚函数public:virtual void f() { cout << "Derive1::f" << endl; }// virtual void h() { cout << "Derive1::h" << endl; }};class Derive2 : public Base{ // 重写两个虚函数virtual void f() { cout << "Derive2::f" << endl; }virtual void h() { cout << "Derive2::h" << endl; }};class Derive3 : public Base{ // 重写两个函数,并增加一个虚函数virtual void f() { cout << "Derive3::f" << endl; }virtual void j() { cout << "Derive3::j" << endl; }virtual void h() { cout << "Derive3::h" << endl; }};typedef void (*Func)(void);先上代码,1个基类,3个类继承它,并以不同的特色重写基类的函数,然后定义了相应的函数指针。
虚函数表-C++多态的实现原理解析
虚函数表-C++多态的实现原理解析⽬录1、说明2、虚函数表3、代码⽰例1、说明我们都知道多态指的是⽗类的指针在运⾏中指向⼦类,那么它的实现原理是什么呢?答案是虚函数表在关于virtual⼀⽂中,我们详细了解了C++多态的使⽤⽅式,我们知道没有virtual关键⼦就没法使⽤多态2、虚函数表我们看⼀下下⾯的代码class A{public:int i;virtual void func() { cout << "A func" << endl; }virtual void func2() { cout << "A func2" << endl; }void func3() { cout << "A func3" << endl; }};class B : public A{int j;void func() { cout << "B func" << endl; }void func3() { cout << "B func3" << endl; }};int main(){cout << sizeof(A) << ", " << sizeof(B); //输出 8,12return 0;}在32位编译模式下,程序的运⾏结果是:8,12但是如果把代码中的virtual删掉,则程序的运⾏结果为:4,8可以发现,有了虚函数之后,类所占的存储空间⽐没有虚函数多了4个字节,这个4个字节就是实现多态的关键 -- 位于对象存储空间的最前端的指针,存放的是虚函数表的地址,这个是由编译器实现的每个带有虚函数的类(包括其⼦类)都有虚函数表虚函数表中存放着虚函数的地址,注意是虚函数的地址,⾮虚函数不在此列虚函数表是编译器实现的,程序运⾏时被载⼊内存,⼀个类的虚函数表中列出了该类的全部虚函数地址。
c++内存分布之虚函数(多继承)
c++内存分布之虚函数(多继承)系列【本⽂】结论1.虚函数表指针和虚函数表1.1 影响虚函数表指针个数的因素只和派⽣类的⽗类个数有关。
多⼀个⽗类,派⽣类就多⼀个虚函数表指针,同时,派⽣类的虚函数表就额外增加⼀个1.2 派⽣类和⽗类同时含有虚函数,派⽣类的虚函数按照⽗类声明的顺序(从左往右),存放在继承的第⼀个⽗类中虚函数表后⾯,⽽不是单独再额外建⽴⼀张虚函数表1.3 按照先声明、先存储、先⽗类、再⼦类的顺序存放类的成员变量1.4 ⽆论是派⽣类还是⽗类,当出现了虚函数(普通虚函数、虚析构函数、纯虚函数),排在内存布局最前⾯的⼀定是虚函数表指针2.覆盖继承其实,覆盖继承不够准确。
2.1 成员变量覆盖派⽣类和⽗类出现了同名的成员变量时,派⽣类仅仅将⽗类的同名成员隐藏了,⽽⾮覆盖替换派⽣类调⽤成员变量时,按照就近原则,调⽤⾃⾝的同名变量,解决了当调⽤同名变量时出现的⼆义性的现象2.2 成员函数覆盖需要考虑是否有虚函数的情况存在虚函数的覆盖继承⽗类和派⽣类出现了同名虚函数函数((普通虚函数、纯虚函数),派⽣类的虚函数表中将⼦类的同名虚函数的地址替换为⾃⾝的同名虚函数的地址-------多态出现不存在虚函数的覆盖继承⽗类和派⽣类同时出现同名成员函数,这与成员变量覆盖继承的情况是⼀样的,派⽣类屏蔽⽗类的同名函数关于演⽰环境: VS2017 + 32位程序多继承(本⽂主要展开)代码写的不够规范:因为多态中,任何带虚函数的基类类的析构函数都应该是虚析构函数。
但是我这⾥没有写出来,⽬的是缩短⽂章篇幅。
序号情况(多继承,基类个数:2)1基类均⽆虚函数(A,派⽣类有虚函数,B,派⽣类不含有虚函数)2两个基类中只有⼀个类有虚函数(A,派⽣类有虚函数,B,派⽣类不含有虚函数)3两个基类中都含有虚函数(A,派⽣类有虚函数,B,派⽣类不含有虚函数)1.基类均⽆虚函数1.1 基类均⽆虚函数,派⽣类有虚函数1.1.1 代码// 基类class baseA{public:int _ma = 1;int _mb = 2;};1.1.2 内存分布因为派⽣类存在虚函数,故排在最前的是虚函数表指针(此时,虚函数表指针属于派⽣类,⽽⾮基类),接着在世基类成员变量,这⾥先是基类baseA,然后才是基类baseB,最后才是派⽣类成员变量。
C++虚函数表内存布局
C++虚函数表内存布局虚表指针虚函数有个特点。
存在虚函数的类会在类的数据成员中⽣成⼀个虚函数指针 vfptr,⽽vfptr 指向了⼀张表(简称,虚表)。
正是由于虚函数的这个特性,C++的多态才有了发⽣的可能。
其中虚函数表由三部分组成,分别是 RTTI(运⾏时类型信息)、偏移及虚函数的⼊⼝地址。
⽽虚表与类及类⽣成的对象有存在着以下两种关系:类与虚表的关系:⼀个类只有⼀个虚表对象与类的关系:所有对象共享⼀个虚表如下图所⽰:对象通过⼀个 vfptr (虚表指针)共享虚表.虚表指针在类中的布局1、虚表指针 vfptr 在上,类成员变量 ma 在下(图左)2、类成员变量 ma 在上,虚表指针 vfptr 在下(图右)在类中,vfptr 的优先级最⾼,所以虚函数在类中的布局应该是上图左边的结构,其中vftpr指针指向虚表,在虚表的起始位置存放这虚表所属类的类型信息RTTI(运⾏时类型信息Run-Time Type Identification)。
可以通过typeid(pb).name()查看。
虚函数表在类中的布局现有基类 Base、派⽣类 Deriver 为测试代码:#include<iostream>class Base //定义基类{public:Base(int a) :ma(a) {}virtual void Show() // 声明为虚函数{std::cout << "Base: ma = " << ma << std::endl;}protected:int ma;};class Deriver : public Base //派⽣类{public:Deriver(int b) :mb(b), Base(b) {}void Show() // 没有声明为虚函数{std::cout << "Deriver: mb = " << mb << std::endl;}protected:int mb;};1. 查看Base类的内存布局在VS 2019开发者命令提⽰中输⼊:cl 虚函数.cpp /d1reportSingleClassLayoutBase其中,虚函数.cpp 为源⽂件的⽂件名,最后的Base为要查看的类/* Base类内存布局 */class Base size(8):+---0 | {vfptr}4 | ma+---Base::$vftable@:| &Base_meta //运⾏时类型信息 Run-Time Type Identification| 0 //虚函数指针相对于整体作⽤域的偏移0 | &Base::Show //虚函数⼊⼝地址,虚函数⼊⼝地址有⼀个或多个2. 查看Deriver内存布局输⼊:cl 虚函数.cpp /d1reportSingleClassLayoutDeriver我们查寻到 Deriver的内存布局中类对象占据12个字节的空间。
C++:虚函数的详解
C++:虚函数的详解5.4.2 虚函数详解1.虚函数的定义虚函数就是在基类中被关键字virtual说明,并在派⽣类重新定义的函数。
虚函数的作⽤是允许在派⽣类中重新定义与基类同名的函数,并且可以通过基类指针或引⽤来访问基类和派⽣类中的同名函数。
虚函数的定义是在基类中进⾏的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual。
定义虚函数的格式如下:virtual 函数类型函数名(形参表){函数体}在基类中的某个成员函数声明为虚函数后,此虚函数就可以在⼀个或多个派⽣类中被重新定义。
在派⽣类中重新定义时,其函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
//例 5.21 虚函数的使⽤#include<iostream>using namespace std;class B0{public:virtual void print(char *p) //定义基类B0中的虚函数{cout<<p<<"print()"<<endl;}};class B1:public B0{public:virtual void print(char *p) //定义基类B0的公有派⽣类B1中的虚函数{cout<<p<<"print()"<<endl;}};class B2:public B1{public:virtual void print(char *p) //定义基类B1的公有派⽣类B2中的虚函数{cout<<p<<"print()"<<endl;}};int main(){B0 ob0,*op; //定义基类对象ob0和对象指针opop=&ob0;op->print("B0::"); //调⽤基类的B0的print()B1 ob1; //定义派⽣类B1的对象ob1op=&ob1;op->print("B1::"); //调⽤派⽣类B1的print()B2 ob2; //定义派⽣类B2的对象ob2op=&ob2;op->print("B2::"); //调⽤派⽣类B2的print()return0;}/*在程序中,语句op->print();出现了3次,由于op指向的对象不同,每次出现都执⾏了相应对象的虚函数print程序运⾏结果:B0::print()B1::print()B2::print()说明:(1)若在基类中,只是声明虚函数原型(需要加上virtual),⽽在类外定义虚函数时,则不必再加上virtual。
使用gdb探索C++虚函数表
使用gdb探索C++虚函数表学习和使用C++,继承和多态是避免不了的话题,其中核心就是虚函数表。
接下来几篇文章,分几种继承情况,利用gdb解析虚函数表的结构。
C++标准并没有规定编译器如何实现C++的各种特性,所以不同编译器实现方法一般不同。
在上世纪90年代,GCC使用“setjmp”和“longjmp”实现异常处理。
当Intel制造Itanium处理器时,Intel想让Intel Compiler 和GCC可以一定程度互操作,当然包括互抛异常。
于是Intel和GCC 组织了一个工作组来设计新的ABI,即Itanium ABI。
随后GCC开发者将该ABI扩展到了其他的处理器和操作系统。
由于该ABI设计良好,LLVM也选择使用它。
微软的MSVC编译器使用自己的ABI实现。
以下内容均是在下列环境讨论的•Ubuntu 19.04 64bit系统•gcc 8.3.0,即Itanium ABI•gdb 8.2.91•Python 3.7.3•编译时使用-g选项过程中使用了上篇文章实现的gdb扩展“checksymbol”。
该扩展主要是结合了gdb的“x”和“info symbol”命令。
“checksymbol”接受两个参数,地址a和长度l,它会打印从地址a开始l字节的地址及其内容,每8字节一行地址a <地址a的符号名(如果有)>: 地址a中的内容(8字节)地址a中内容(8字节)的符号名(如果有)单继承没有覆盖1 2 3 class A {public:A() : ma{-1} {}4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 virtual ~A() = default; virtual void va1() {} virtual void va2() {} void fa1() {} long ma; }; class B : public A { public: B() : mb{-2} {} virtual ~B() = default; virtual void vb1() {} virtual void vb2() {} void fb1() {} long mb; }; class C : public B { public: C() : mc{-3} {} virtual ~C() = default; virtual void vc1() {} virtual void vc2() {} void fc1() {} long mc; }; int main() { A a; B b; C c; return 0; }在gdb 调试上述代码1 2 3 4 5 6 7 8 9 // 检查a 的内存布局(gdb) checksymbol &a sizeof(a)debug info, addr is: 0x7fffffffde20, len is: 16 0x7fffffffde20: 0x0000555555557d50 vtable for A + 16 0x7fffffffde28: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.// 检查a 的虚函数表(gdb) checksymbol *(long*)&a-16 4810 11 12 13 14 15 debug info, addr is: 93824992247104, len is: 48 0x555555557d40 <vtable for A>: 0x0000000000000000 No symbol matches 0x0000000000000000. 0x555555557d48 <vtable for A+8>: 0x0000555555557da0 typeinfo for A 0x555555557d50 <vtable for A+16>: 0x0000555555555206 A::~A()0x555555557d58 <vtable for A+24>: 0x0000555555555220 A::~A()0x555555557d60 <vtable for A+32>: 0x00005555555551ee A::va1()0x555555557d68 <vtable for A+40>: 0x00005555555551faA::va2()只有虚函数才会出现在虚函数表中。
C虚函数表解析
C++ 虚函数表解析陈皓前言C++中的虚函数的作用主要是实现了多态的机制。
关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。
大家可以看看相关的C++的书籍。
在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。
不利于学习和阅读,所以这是我想写下这篇文章的原因。
也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
虚函数表对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Vi rtual Table)来实现的。
简称为V-Table。
在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。
这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。
没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:class Base {public:virtual void f() { cout << "Base::f"<< endl; }virtual void g() { cout << "Base::g"<< endl; }virtual void h() { cout << "Base::h"<< endl; }};按照上面的说法,我们可以通过Base的实例来得到虚函数表。
c++虚函数底层原理
c++虚函数底层原理C++ 的虚函数实现依赖于虚函数表(vtable)和虚函数指针(vpointer)。
这是一种在 C++ 中实现运行时多态性的机制。
下面是关于 C++ 虚函数底层原理的简要解释:1. 虚函数表(vtable):•对于包含虚函数的每个类,编译器会在编译时创建一个虚函数表。
这个表是一个指向虚函数的指针数组,其中存储了类的虚函数的地址。
每个类的对象都包含一个指向相应虚函数表的指针,这个指针称为虚函数指针。
2. 虚函数指针(vpointer):•对于包含虚函数的每个类的对象,编译器会在对象的内存布局中添加一个指向虚函数表的指针,这个指针称为虚函数指针。
这个指针通常位于对象的内存布局的最前面。
3. 虚函数的调用:•当调用一个虚函数时,实际上是通过对象的虚函数指针来查找虚函数表,然后找到对应的虚函数的地址进行调用。
这使得在运行时能够动态地决定使用哪个类的虚函数,实现了多态性。
4. 派生类的虚函数表:•派生类继承了基类的虚函数表,并在其虚函数表中添加或覆盖相应的虚函数。
派生类的虚函数表的地址通常与基类的虚函数表的地址相同,但派生类的虚函数表包含更多的条目。
5. 纯虚函数和抽象类:•如果基类中的虚函数被声明为纯虚函数,那么这个基类就是抽象类。
抽象类不能实例化,但它可以有派生类。
纯虚函数的虚函数表中存储的是虚函数的地址为 0,表示没有实际的实现。
这种虚函数机制使得 C++ 中的继承和多态性变得灵活且强大。
在运行时,程序能够动态地选择正确的虚函数实现,而不需要在编译时确定。
这对于实现多态性和面向对象编程非常有用。
c++深入理解虚函数
c++深入理解虚函数为什么使用虚函数?什么是虚函数?虚函数是为了解决什么问题?面向对象的三大特征:•封装•多态•继承1.普通虚函数2.虚析构函数3.纯虚函数4.抽象类5.接口类6.隐藏 vs 覆盖7.隐藏与覆盖之间的关系8.早绑定和晚绑定9.虚函数表什么是多态?相同对象收到不同消息或不同对象收到相同消息时产生的不同的动作。
静态多态 vs 动态多态[-:>静态多态也叫做早绑定class Rect //矩形类{public:int calcArea(int width);int calcArea(int width,int height);};如上面的代码,他们函数名相同,参数个数不同,一看就是互为重载的两个函数1int main()2 {3 Rect.rect;4 rect.calcArea(10);5 rect.calcArea(10,20);6return0;7 }程序在编译阶段根据参数个数确定调用哪个函数。
这种情况叫做静态多态(早绑定)[-:>动态多态也叫做晚绑定比如计算面积当给圆形计算面积时使用圆形面积的计算公式,给矩形计算面积时使用矩形面积的计算公式。
也就是说有一个计算面积的形状基类,圆形和矩形类派生自形状类,圆形与矩形的类各有自己的计算面积的方法。
可见动态多态是以封装和继承为基础的。
1class Shape//形状类2 {3public:4double calcArea()5 {6 cout<<"calcArea"<<endl;7return0;8 }9 };10class Circle:public Shape //公有继承自形状类的圆形类11 {12public:13 Circle(double r);14double calcArea();15private:16double m_dR;17 };18double Circle::calcArea()19 {20return3.14*m_dR*m_dR;21 }22class Rect:public Shape //公有继承自形状类的矩形类23 {24public:25 Rect(double width,double height);26double calArea();27private:28double m_dWidth;29double m_dHeight;30 };31double Rect::calcArea()32 {33return m_dWidth*m_dHeight;34 }35int main()36 {37 Shape *shape1=new Circle(4.0);38 Shape *shape2=new Rect(3.0,5.0);39 shape1->calcArea();40 shape2->calcArea();41 .......42return0;43 }如果打印结果的话,以上程序结果会打印两行"calcArea",因为调用到的都是父类的calcArea函数,并不是我们想要的那样去分别调用各自的计算面积的函数。
c++的虚函数
c++的虚函数C++的虚函数是面向对象编程中一项重要的特性,它为派生类提供了一种可以覆盖其基类成员函数的方法。
这种覆盖能够保证在运行时正确调用适当的成员函数,从而实现了多态性的特性。
虚函数是通过在函数声明的前面加上关键字virtual来定义的。
例如:virtual void foo();虚函数可以是类成员函数,也可以是类模板的成员函数。
虚函数的返回类型可以是任意类型,但是不允许使用引用类型或者是void类型。
虚函数的使用主要分为两个方面:1. 声明在类中申明一个虚函数,用关键字virtual来声明,在基类中虚函数只是申明,没有实现;子类必须在类内或类外实现这个函数。
2. 调用在调用虚函数时,编译器会根据对象的类型来选择调用哪个版本的虚函数,调用的版本取决于对象的实际类型,而不是指针或引用的类型。
在C++中,虚函数的实现是通过虚函数表来实现的。
虚函数表是一个数组,它存储了类的虚函数的地址。
在类中每个具有虚函数的对象都有一个指向虚函数表的指针。
在创建对象时,虚函数表会被创建并初始化,指向每个虚函数的地址。
当对象的虚函数被调用时,实际的函数调用是通过虚函数表来完成的,这样就能够正确调用派生类的虚函数。
虚函数的注意事项1. 不要将构造函数和析构函数声明为虚函数。
2. 如果一个函数被声明为虚函数,则其所有子类都必须重写这个函数。
3. 在虚函数中不要使用默认参数,会被忽略。
虚函数的优缺点虚函数的最大优点就是多态能够方便地实现。
通过虚函数,可以将一个基类指针指向一个派生类对象,调用相同的虚函数,就可以得到不同的实现结果。
这种多态的特性能够更方便地实现程序的抽象和封装。
虚函数的缺点是运行时的开销会比较大,因为需要在运行时进行动态绑定。
此外,在多继承的情况下,虚函数的调用可能会出现二义性,需要使用虚拟继承来解决。
总结C++的虚函数是面向对象编程中一项重要的特性,支持多态性,通过虚函数表来实现。
虚函数的优点在于方便实现程序的抽象和封装,缺点则是会出现运行时的开销和可能出现二义性的问题。
c++多态和虚函数表实现原理
c++多态和虚函数表实现原理⾃⼰开发了⼀个股票智能分析软件,功能很强⼤,需要的点击下⾯的链接获取:C++多态以及虚函数表实现原理⽬录1 定义2 虚函数表实现原理3 实例解析3.1 定义⽗类3.2 ⽗类对象地址空间剖析3.3 ⼦类继承⽗类的虚函数表3.3.1 单继承覆盖和不覆盖对⽐3.3.2 ⼦类继承多个⽗类3.3.3 多层继承的⼦类1 定义C++中的虚函数的作⽤主要是实现了多态的机制。
关于多态,⼦类对象赋值给⽗类指针,然后通过⽗类的指针调⽤实际⼦类覆盖⽗类的虚函数实现。
赋值不同的⼦类对象给⽗类指针,可以使⽤⽗类指针调⽤不同⼦类的虚函数(必须是覆盖⽗类的函数),这种技术可以让⽗类的指针有“多种形态”。
2 虚函数表实现原理每个⼦类对象创建时,会有⼀个虚函数表,⽽且虚函数表在对象⾸地址开始,以达到⾼效访问的⽬的。
⼀个⼦类继承多个⽗类时,⼦类对象的头部有多个虚函数表,把⽗类的虚函数表复制过来,按照继承顺序排列。
如果⼦类覆盖了⽗类的虚函数,则替换掉复制过来的⽗类虚函数表中对应位置的虚函数。
⼦类中的⾃定义虚函数,在第⼀个虚函数表中,排在⽗类虚函数后⾯(主流说法是这样,但是实际测试,并没有)。
当⽤⼦类的对象赋值给⽗类指针时,⽗类指针指向了⼦类虚函数表,当调⽤函数时,就会从⼦类虚函数表中查找函数,去调⽤⼦类覆盖⽗类的虚函数,这样就实现了多态。
3 实例解析3.1 定义⽗类class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }int a=0;};3.2 ⽗类对象地址空间剖析创建⼀个对象Base b;⽗类的虚函数表⽰意图为虚函数表在对象地址⾸部,函数指针按照定义的顺序在虚函数表中,可以通过地址访问的⽅式去访问这些函数。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
C++虚函数表解析(转) ——写的真不错,忍不住转了收藏C++中的虚函数的作用主要是实现了多态的机制。
关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。
大家可以看看相关的C++的书籍。
在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。
不利于学习和阅读,所以这是我想写下这篇文章的原因。
也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
虚函数表对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。
简称为V-Table。
在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。
在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。
这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。
没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }};按照上面的说法,我们可以通过Base的实例来得到虚函数表。
下面是实际例程:typedef void(*Fun)(void);Base b;Fun pFun = NULL;cout << "虚函数表地址:" << (int*)(&b) << endl;cout << "虚函数表—第一个函数地址:" << (int*)*(int*)(&b) << endl;/*这里的一点争议的个人看法*/原文认为(int*)(&b)是虚表的地址,而很多网友都说,(包括我也认为):(int *)*(int*)(&b)才是虚表地址而(int*)*((int*)*(int*)(&b)); 才是虚表第一个虚函数的地址。
其实看后面的调用pFun = (Fun)*((int*)*(int*)(&b)); 就可以看出,*((int*)*(int*)(&b));转成函数指针给pFun,然后正确的调用到了虚函数virtual void f()。
// Invoke the first virtual functionpFun = (Fun)*((int*)*(int*)(&b));pFun();实际运行经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)虚函数表地址:0012FED4虚函数表—第一个函数地址:0044F148Base::f通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。
通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:(Fun)*((int*)*(int*)(&b)+0); // Base::f()(Fun)*((int*)*(int*)(&b)+1); // Base::g()(Fun)*((int*)*(int*)(&b)+2); // Base::h()这个时候你应该懂了吧。
什么?还是有点晕。
也是,这样的代码看着太乱了。
没问题,让我画个图解释一下。
如下所示:注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束。
这个结束标志的值在不同的编译器下是不同的。
在WinXP+VS2003下,这个值是NULL。
而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。
没有覆盖父类的虚函数是毫无意义的。
我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。
在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)下面,再让我们来看看继承时的虚函数表是什么样的。
假设有如下所示的一个继承关系:请注意,在这个继承关系中,子类没有重载任何父类的函数。
那么,在派生类的实例中,其虚函数表如下所示:对于实例:Derive d; 的虚函数表如下:我们可以看到下面几点:1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。
下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。
那么,对于派生类的实例,其虚函数表会是下面的一个样子:我们从表中可以看到下面几点,1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,Base *b = new Derive();b->f();由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。
这就实现了多态。
多重继承(无虚函数覆盖)下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。
注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:我们可以看到:1)每个父类都有自己的虚表。
2)子类的成员函数被放到了第一个父类的表中。
(所谓的第一个父类是按照声明顺序来判断的)这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。
如:Derive d;Base1 *b1 = &d;Base2 *b2 = &d;Base3 *b3 = &d;b1->f(); //Derive::f()b2->f(); //Derive::f()b3->f(); //Derive::f()b1->g(); //Base1::g()b2->g(); //Base2::g()b3->g(); //Base3::g()安全性每次写C++的文章,总免不了要批判一下C++。
这篇文章也不例外。
通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。
水可载舟,亦可覆舟。
下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。
因为多态也是要基于函数重载的。
虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:Base1 *b1 = new Derive();b1->f1(); //编译出错任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。
但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)二、访问non-public的虚函数另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如:class Base {private:virtual void f() { cout << "Base::f" << endl; }};class Derive : public Base{};typedef void(*Fun)(void);void main() {Derive d;Fun pFun = (Fun)*((int*)*(int*)(&d)+0);pFun();}结束语C++这门语言是一门Magic的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。
需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要去了解C++中那些危险的东西。
不然,这是一种搬起石头砸自己脚的编程语言。
本文来自CSDN博客,转载请标明出处:/hairetz/archive/2009/04/29/4137000.aspx。