多态性----vptr----vtable

本文深入探讨了C++中多态性的实现原理,重点讲解了虚函数、vptr及vtable的工作机制。通过示例代码展示了如何利用底层机制调用虚函数,并解释了vtable的创建和共享方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考 :多态性----vptr----vtable

多态性 (polymorphism) 是面向对象编程的基本特征之一。而在 C++ 中,多态性通过虚函数 (virtual function) 来实现。我们来看一段简单的代码:

#include <iostream>

using namespace std;

class Base

{

int a;

public:

virtual void fun1() {cout<<"Base::fun1()"<<endl;}

virtual void fun2() {cout<<"Base::fun2()"<<endl;}

virtual void fun3() {cout<<"Base::fun3()"<<endl;}

};

class A:public Base

{

int a;

public:

void fun1() {cout<<"A::fun1()"<<endl;}

void fun2() {cout<<"A::fun2()"<<endl;}//在A的对象中,函数存放的地址顺序:A::fun1() ,A::fun2(),A.Base::fun3()。

};

void foo (Base& obj)

{

obj.fun1();

obj.fun2();

obj.fun3();

}

int main()

{

Base b;

A a;

foo(b);

foo(a);

}

运行结果为:

Base::fun1()

Base::fun2()

Base::fun3()

A::fun1()

A::fun2()

Base::fun3() 

仅通过基类的接口,程序调用了正确的函数,它就好像知道我们输入的对象的类型一样!

那么,编译器是如何知道正确代码的位置的呢?其实,编译器在编译时并不知道要调用的函数体的正确位置,但它插入了一段能找到正确的函数体的代码。这称之为 晚捆绑 (late binding) 或 运行时捆绑 (runtime binding) 技术。

通过virtual 关键字创建虚函数能引发晚捆绑,编译器在幕后完成了实现晚捆绑的必要机制。它对每个包含虚函数的类创建一个表(称为VTABLE),用于放置虚函数的地址。在每个包含虚函数的类中,编译器秘密地放置了一个称之为vpointer(缩写为VPTR)的指针,指向这个对象的VTABLE。所以无论这个对象包含一个或是多少虚函数,编译器都只放置一个VPTR即可。VPTR由编译器在构造函数中秘密地插入的代码来完成初始化,指向相应的VTABLE,这样对象就"知道"自己是什么类型了。 VPTR都在对象的相同位置,常常是对象的开头。这样,编译器可以容易地找到对象的VTABLE并获取函数体的地址。

如果我们用sizeof查看前面Base类的长度,我们就会发现,它的长度不仅仅是一个int的长度,而是增加了刚好是一个void指针的长度(在我的机器里面,一个int占4个字节,一个void指针占4个字节,这样正好类Base的长度为8个字节)。

//这是由于virtual 关键字创建虚函数能引发晚捆绑;如果没有virtual函数,不会分配此指针的空间。

每当创建一个包含虚函数的类或从包含虚函数的类派生一个类时,编译器就为这个类创建一个唯一的VTABLE。在VTABLE中,放置了这个类中或是它的基类中所有虚函数的地址,这些虚函数的顺序都是一样的,所以通过偏移量可以容易地找到所需的函数体的地址。假如在派生类中没有对在基类中的某个虚函数进行重写(overriding),那末还使用基类的这个虚函数的地址(正如上面的程序结果所示)。

至今为止,一切顺利。下面,我们的试验开始了。

就目前得知的,我们可以试探着通过自己的代码来调用虚函数,也就是说我们要找寻一下编译器秘密地插入的那段能找到正确函数体的代码的足迹。

如果我们有一个Base指针作为接口,它一定指向一个Base或由Base派生的对象,或者是A,或者是其它什么。这无关紧要,因为VPTR的位置都一样,一般都在对象的开头。如果是这样的话,那么包含有虚函数的对象的指针,例如Base指针,指向的位置恰恰是另一个指针——VPTR。VPTR指向的 VTABLE其实就是一个函数指针的数组,现在,VPTR正指向它的第一个元素,那是一个函数指针。如果VPTR向后偏移一个Void指针长度的话,那么它应该指向了VTABLE中的第二个函数指针了。

这看来就像是一个指针连成的链,我们得从当前指针获取它指向的下一个指针,这样我们才能"顺藤摸瓜"。那么,我来介绍一个函数:

void *getp (void* p)

{

return (void*)*(unsigned long*)p;

}

我们不考虑它漂亮与否,我们只是试验。getp() 可以从当前指针获取它指向的下一个指针。如果我们能找到函数体的地址,用什么来存储它呢?我想应该用一个函数指针:

typedef void (*fun)();

它与Base中的三个虚函数相似,为了简单我们不要任何输入和返回,我们只要知道它实际上被执行了即可。

然后,我们负责"摸瓜"的函数登场了:

fun getfun (Base* obj, unsigned long off)

{

void *vptr = getp(obj);

unsigned char *p = (unsigned char *)vptr;

p += sizeof(void*) * off;

return (fun)getp(p);

}

第一个参数是Base指针,我们可以输入Base或是Base派生对象的指针。第二个参数是VTABLE偏移量,偏移量如果是0那么对应fun1(),如果是1对应fun2()。getfun() 返回的是fun类型函数指针,我们上面定义的那个。可以看到,函数首先就对Base指针调用了一次getp(),这样得到了vptr这个指针,然后用一个 unsigned char指针运算偏移量,得到的结果再次输入getp(),这次得到的就应该是正确的函数体的位置了。

那么它到底能不能正确工作呢?我们修改main() 来测试一下:

int main()

{

Base *p = new A;

fun f = getfun(p, 0);

//如果VPTR向后偏移一个Void指针长度的话,那么它应该指向了VTABLE中的第二个函数指针了。

(*f)();

f = getfun(p, 1);

(*f)();

f = getfun(p, 2);

(*f)();

 

//f = getfun(p,3);//没有结果

//(*f)();

//f = getfun(p, 4);// //没有结果

//(*f)();

 

A aa;

aa.Base::fun1();

aa.Base::fun2();

//在派生类中,依然有基类Virtual函数的存在。但是目前没找到方法用指针调用。

delete p;

}

激动人心的时刻到来了,让我们运行它!

运行结果为:

A::fun1()

A::fun2()

Base::fun3()

至此,我们真的成功了。通过我们的方法,我们获取了对象的VPTR,在它的体外执行了它的虚函数。

   

源文档 <http://www.cppblog.com/fwxjj/archive/2007/01/25/17996.html>

   

探寻vtable实例:

#include <iostream>

using namespace std;

typedef void (*fun)(void); 


#if 1
    #define VIRTUAL virtual
#else
    #define VIRTUAL 
#endif

class Base
{
public:    
    VIRTUAL void fun1(void) {cout<<"Base::fun1()"<<endl;}
    VIRTUAL void fun2(void) {cout<<"Base::fun2()"<<endl;}
    VIRTUAL void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:    
    int a;
};

class A:public Base
{
public:    
    int a;
    void fun1() {cout<<"A::fun1()"<<endl;}
    void fun2() {cout<<"A::fun2()"<<endl;}
    //在A的对象中,函数存放的地址顺序:A::fun1() ,A::fun2(),A.Base::fun3()。

};

void foo (Base& obj)
{
    obj.fun1();
    obj.fun2();
    obj.fun3();
}

void showsizes()
{
    Base b;
    A a;
    printf("sizeof(class Base) = %d\n", sizeof(class Base));
    printf("sizeof(class A) = %d\n", sizeof(class A));
    printf("sizeof(int) = %d\n", sizeof(int));
}

void *getp (void* p)
{
    return (void*)*(unsigned long*)p;
} 

fun getfun (Base* obj, unsigned long off)
{
    void *vptr = getp(obj);
    unsigned char *p = (unsigned char *)vptr;
    p += sizeof(void*) * off;
    return (fun)getp(p);
} 

//下面这个函数主要说明VTABLE的存在以及其中的内容
void show_a_VTABLE(void)
{
    printf("下面这段代码主要说明VTABLE的存在以及其中的函数排布\n");
    Base *p = new A;
    fun f = getfun(p, 0);
    (*f)();
    
    f = getfun(p, 1);
    (*f)();
    
    f = getfun(p, 2);
    (*f)();
    
    //f = getfun(p, 3); //VTABLE里面只有3个函数, 因此这里出现Segmentation fault
    //(*f)();
}

//下面这段代码主要说明VTABLE是一个Class所有的实例共享的,即所有的实例的VTABLE都是同一个。而每个Class各自维护一个VTABLE
void show_3_VTABLE_addr(void)
{

    Base *p = new A;
    void *ptr = NULL;
    printf("下面这段代码主要说明VTABLE是一个Class所有的实例共享的,即所有的实例的VTABLE都是同一个。而每个Class各自维护一个VTABLE\n");
    fun f = getfun(p, 0);
    ptr = (void*) f; printf("show a addr of p->fun0() is %x\n", ptr);

    A aa;
    f = getfun(&aa, 0);
    ptr = (void*) f; printf("show aa addr of p->fun0() is %x\n", ptr);

    Base b;
    f = getfun(&b, 0);
    ptr = (void*) f; printf("show b addr of p->fun0() is %x\n", ptr);
    printf("A class的不同实例的第一个成员函数地址相同,但不同于B Class的一个实例第一个成员函数地址\n");
    
    delete p;
}
//下面的代码反映了一个类里面的所有能调用的方法
void show_all_funcs(void)
{
    A aa;
    printf("下面的代码反映了一个类里面的所有能调用的方法\n");
    printf("使用指针引用class里面的函数的结果:\n");
    fun f = getfun(&aa, 0);
    (*f)(); 
    f = getfun(&aa, 1);
    (*f)(); 
    f = getfun(&aa, 2);
    (*f)(); 
    printf("直接使用class里面的函数的结果:\n");
    aa.fun1();
    aa.fun2();
    aa.fun3();
    printf("使用子类中来自于基类的函数:\n");
    aa.Base::fun1();
    aa.Base::fun2();//在派生类中,依然有基类Virtual函数的存在。但是目前没找到方法用指针调用。
} 

int main()
{
    printf("DATE:"__DATE__" TIME:" __TIME__ "\n");
    
    show_all_funcs();
    show_a_VTABLE();
    show_3_VTABLE_addr();
    return 0;

}

/*
[root@localhost test]# g++ test.cpp ;./a.out 
DATE:May 30 2016 TIME:02:29:14
下面的代码反映了一个类里面的所有能调用的方法
使用指针引用class里面的函数的结果:
A::fun1()
A::fun2()
Base::fun3()
直接使用class里面的函数的结果:
A::fun1()
A::fun2()
Base::fun3()
使用子类中来自于基类的函数:
Base::fun1()
Base::fun2()
下面这段代码主要说明VTABLE的存在以及其中的函数排布
A::fun1()
A::fun2()
Base::fun3()
下面这段代码主要说明VTABLE是一个Class所有的实例共享的,即所有的实例的VTABLE都是同一个。而每个Class各自维护一个VTABLE
show a addr of p->fun0() is 8048ba2
show aa addr of p->fun0() is 8048ba2
show b addr of p->fun0() is 8048c26
A class的不同实例的第一个成员函数地址相同,但不同于B Class的一个实例第一个成员函数地址
*/

一个类的某个方法如果不定义为vitual,它就不会存在于方法列表中。
/*
#if 0
    #define VIRTUAL virtual
#else
    #define VIRTUAL
#endif
[root@localhost test]# g++ test.cpp ;./a.out
DATE:May 30 2016 TIME:02:31:17
下面的代码反映了一个类里面的所有能调用的方法
使用指针引用class里面的函数的结果:
Segmentation fault
*/

一个类的方法只有定义为vitual,才会存在于方法列表中,并且第一个声明为vitual的函数在offset为0的位置。依次类推。
/*
class Base
{
public:    
    void fun1(void) {cout<<"Base::fun1()"<<endl;}
    virtual void fun2(void) {cout<<"Base::fun2()"<<endl;}
    void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:    
    int a;
};
[root@localhost test]# g++ test.cpp ;./a.out
DATE:May 30 2016 TIME:02:35:55
下面的代码反映了一个类里面的所有能调用的方法
使用指针引用class里面的函数的结果:
A::fun2()
Segmentation fault
*/

如果基类没有vitual函数,将不会创建虚函数表。
/*
class Base
{
public:    
    void fun1(void) {cout<<"Base::fun1()"<<endl;}
    void fun2(void) {cout<<"Base::fun2()"<<endl;}
    void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:    
    int a;
};

sizeof(class Base) = 4
*/

不管基类的方法是不是vitural虚函数,子类都回将基类的函数继承下来。子类的头四个字节将会存储基类的信息。
/*
class Base
{
public:    
    void fun1(void) {cout<<"Base::fun1()"<<endl;}
    void fun2(void) {cout<<"Base::fun2()"<<endl;}
    void fun3(void) {cout<<"Base::fun3()"<<endl;}
public:    
    int a;
};

void showaaa(void)
{
    A aaa;
    aaa.a = 101;
    unsigned int addr;
    int *paaa_a = NULL;
    addr = (unsigned int) &aaa;
    addr+=4;
    paaa_a = (int *)addr;
    printf("aaa.a = 101, *paaa_a = %d\n", *paaa_a);//aaa.a = 101, *paaa_a = 101
    
    aaa.fun1();
    aaa.fun2();
    aaa.fun3();
    
    aaa.Base::fun1();
    aaa.Base::fun2();
    aaa.Base::fun3();
}

sizeof(class Base) = 4
sizeof(class A) = 8
sizeof(int) = 4
aaa.a = 101, *paaa_a = 101

A::fun1()
A::fun2()
Base::fun3()

Base::fun1()
Base::fun2()
Base::fun3()
*/

 

转载于:https://www.cnblogs.com/mylinux/archive/2013/05/04/3059093.html

基于数据挖掘的音乐推荐系统设计与实现 需要一个代码说明,不需要论文 采用python语言,django框架,mysql数据库开发 编程环境:pycharm,mysql8.0 系统分为前台+后台模式开发 网站前台: 用户注册, 登录 搜索音乐,音乐欣赏(可以在线进行播放) 用户登陆时选择相关感兴趣的音乐风格 音乐收藏 音乐推荐算法:(重点) 本课题需要大量用户行为(如播放记录、收藏列表)、音乐特征(如音频特征、歌曲元数据)等数据 (1)根据用户之间相似性或关联性,给一个用户推荐与其相似或有关联的其他用户所感兴趣的音乐; (2)根据音乐之间的相似性或关联性,给一个用户推荐与其感兴趣的音乐相似或有关联的其他音乐。 基于用户的推荐和基于物品的推荐 其中基于用户的推荐是基于用户的相似度找出相似相似用户,然后向目标用户推荐其相似用户喜欢的东西(和你类似的人也喜欢**东西); 而基于物品的推荐是基于物品的相似度找出相似的物品做推荐(喜欢该音乐的人还喜欢了**音乐); 管理员 管理员信息管理 注册用户管理,审核 音乐爬虫(爬虫方式爬取网站音乐数据) 音乐信息管理(上传歌曲MP3,以便前台播放) 音乐收藏管理 用户 用户资料修改 我的音乐收藏 完整前后端源码,部署后可正常运行! 环境说明 开发语言:python后端 python版本:3.7 数据库:mysql 5.7+ 数据库工具:Navicat11+ 开发软件:pycharm
MPU6050是一款广泛应用在无人机、机器人和运动设备中的六轴姿态传感器,它集成了三轴陀螺仪和三轴加速度计。这款传感器能够实时监测并提供设备的角速度和线性加速度数据,对于理解物体的动态运动状态至关重要。在Arduino平台上,通过特定的库文件可以方便地与MPU6050进行通信,获取并解析传感器数据。 `MPU6050.cpp`和`MPU6050.h`是Arduino库的关键组成部分。`MPU6050.h`是头文件,包含了定义传感器接口和函数声明。它定义了类`MPU6050`,该类包含了初始化传感器、读取数据等方法。例如,`begin()`函数用于设置传感器的工作模式和I2C地址,`getAcceleration()`和`getGyroscope()`则分别用于获取加速度和角速度数据。 在Arduino项目中,首先需要包含`MPU6050.h`头文件,然后创建`MPU6050`对象,并调用`begin()`函数初始化传感器。之后,可以通过循环调用`getAcceleration()`和`getGyroscope()`来不断更新传感器读数。为了处理这些原始数据,通常还需要进行校准和滤波,以消除噪声和漂移。 I2C通信协议是MPU6050与Arduino交互的基础,它是一种低引脚数的串行通信协议,允许多个设备共享一对数据线。Arduino板上的Wire库提供了I2C通信的底层支持,使得用户无需深入了解通信细节,就能方便地与MPU6050交互。 MPU6050传感器的数据包括加速度(X、Y、Z轴)和角速度(同样为X、Y、Z轴)。加速度数据可以用来计算物体的静态位置和动态运动,而角速度数据则能反映物体转动的速度。结合这两个数据,可以进一步计算出物体的姿态(如角度和角速度变化)。 在嵌入式开发领域,特别是使用STM32微控制器时,也可以找到类似的库来驱动MPU6050。STM32通常具有更强大的处理能力和更多的GPIO口,可以实现更复杂的控制算法。然而,基本的传感器操作流程和数据处理原理与Arduino平台相似。 在实际应用中,除了基本的传感器读取,还可能涉及到温度补偿、低功耗模式设置、DMP(数字运动处理器)功能的利用等高级特性。DMP可以帮助处理传感器数据,实现更高级的运动估计,减轻主控制器的计算负担。 MPU6050是一个强大的六轴传感器,广泛应用于各种需要实时运动追踪的项目中。通过 Arduino 或 STM32 的库文件,开发者可以轻松地与传感器交互,获取并处理数据,实现各种创新应用。博客和其他开源资源是学习和解决问题的重要途径,通过这些资源,开发者可以获得关于MPU6050的详细信息和实践指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值