首页 > 其他分享 >vptr和vtbl(虚指针和虚函数表)

vptr和vtbl(虚指针和虚函数表)

时间:2024-04-16 22:46:37浏览次数:23  
标签:函数 vtbl void struct vptr 父类 eat 指针

vptr和vtbl(虚指针和虚函数表)

c++代码的抽象类是 -> 类当中只包含纯虚函数

当一个类有虚函数,即便类当中没有成员变量.他的对象大小也会有一根指针大小 -> 由操作系统决定指针多大

虚函数

子类的对象里面有父类的成分

示例结构代码:

#pragma
#ifndef __VPTR_AND_VTBL__
#define __VPTR_AND_VTBL__

class A
{
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data_one, m_data_two;
};

class B : public A
{
public:
virtual void vfunc1();
void func2();
private:
int m_data_three;
};

class C : public B
{
public:
virtual void vfunc1();
void func2();
private:
int m_data_one, m_data_four;
};

#endif // !__VPTR_AND_VTBL__

分析:

  • 继承结构是C类继承B类.B类继承A类 -> 继承包括数据继承和函数继承 -> 继承的函数是继承函数的调用权.所以父类有虚函数子类一定有

  • 内存层面结构图:

    •  

    • 当一个类里面有虚函数的时候(无论多少个) -> 类当中就会携带一根指针的内存空间

    • 实框框起来的就是父类的一部分

    • 声明虚函数以后类的内存地址当中存在一根指针,该指针指向类对于的虚表,虚表里面存放的都是虚函数的指针

  • 如果在上诉条件下new c会得到一根指向c的指针,如果通过指针p调用vfunc1()

    • c的时候会call地址.跳到地址的位置然后在返回回来 -> 静态绑定

    • 通过指针调用虚函数就是动态绑定 -> 面向对象的关键点 -> 通过通过new的指针.找到vptr虚指针.在找到vtbl,从虚表当中查找指向的函数

    • 上诉逻辑翻译成c代码是

      • (* p->vptr[n]) (p); // 指针p找到虚指针 p->vptr,然后根据索引[n]找到指向函数的指针(p) n是索引值.由编译器决定
  • 为了让容器可以存放内存大小不同的元素,那么容器里面必须放置指向父类的指针 -> list<A*> myLst;

    • 为什么是指向父类? -> 因为在设计的时候可能设计一个抽象的父类.在子类实例化的时候才会去告诉父类自己是什么子类. -> 子类当中有一个drwa()函数

  • c++当中有虚函数,那么虚函数指向虚表当中的什么类型那么就会去调用什么类型的draw(draw写成virtual function这就是好的

  • 他的设计原因是因为c当中如果要实现这个逻辑那么就需要判断指针指向什么类型.如果将来新增新的子类那么就需要增加判断代码

总结:

  • c++编译器看到函数首先要考虑把函数静态绑定还是动态绑定

    • 静态绑定 -> call 地址 -> call是汇编语言的一个动作

    • 动态绑定条件:

      • 必须通过指针调用

      • 指针必须是向上转型(upcat) -> 什么意思?

        • 例如上述代码:new c得到的是一个c的对象.但是c继承于B,B继承于A那么在一开始声明的时候就是A类型. -> 又因为继承关系会有父类的一部分.所以可以实现向上转型

      • 调用的是虚函数

    • 满足这三个条件就会实现动态绑定 -> 虚机制

  • 上诉的用法就是多态

    • 一个声明

    • 实际指向不同的东西

    • 不过这些东西都必须是子类

上述就是多态的内存层面的实现

C当中的实现

继承在C当中的实现

c代码当中.由于并没有直接的继承关键字(编译器层面没做这个设计),所以在c当中的继承类似c++中的复合的概念

示例代码:

#include <stdio.h>
#include <stdlib.h>
void animal_eat();
void dog_eat();

struct Animal
{
   int age;
   void (*eat)(void); // void是一个函数.这是一个函数指针
};

struct Dog
{
   struct Animal base; // 包含父类的结构题作为成员 -> c++中的复合的概念
   char* bread;
};

void animal_eat()
{
   printf("Animal eat");
}

// 重写了父类的eat方法
void dog_eat()
{
   printf("Dog eat");
}

int main()
{
   struct Animal animal;
   animal.age = 5;
   animal.eat = animal_eat; // 因为eat是一个函数指针.所以赋值animal_eat的时候就不需要使用() -> 会被编译器认为是调用方法

   /* 注意Dog是一个指针 */
   struct Dog* dog = (struct Dog*)malloc(sizeof(struct Dog)); // 在c++当中子类dog是一个被new出来的对象.在c中就声明成结构体指针来表示 -> 那么就有了一个指针指向一个对象并且可以向上转型的概念
   dog->base.age = 3; // dog是一根指针.拥有父类的一部分 -> 父类的属性
   dog->base.eat = dog_eat; // 这也是父类的一部分
   dog->bread = "Practice";

   // 调用父类对象的方法
   animal.eat();

   printf("\n");

   // 调用子类对象的方法
   dog->base.eat();

   free(dog);

   getchar();
   return 0;
}

c++中继承、多态的特性在c中的实现 -> c并没有直接提供这些概念.所以在实现的时候内存层面的处理并没有c++做得那么的好,更多的是最简单的模型

示例代码:

#include <stdio.h>
#include <stdlib.h>

struct Animal
{
   char* name;
   void (*speak)(void);
};

struct Cat
{
   struct Animal base;
};

struct Tomcat
{
   struct Cat cat;
};

// 可以看到Tomcat继承了Cat,Cat继承了Animal

// 模拟虚函数 -> 重写父类的speak方法
void cat_speak(void)
{
   printf("Cat Speak\n");
}

void tomcat_speak(void)
{
   printf("Tomcat Speak\n");
}

int main()
{
   struct Cat cat;
   cat.base.name = "Cat"; // 父类的一部分
   cat.base.speak = cat_speak; // 也是父类定义的函数指针 -> 这个赋值相当于重写,并且单独有一块内存空间

   struct Tomcat tcat; // new出来的对象 -> 获得一根对象指针
   tcat.cat.base.name = "Tomcat";
   tcat.cat.base.speak = tomcat_speak; // 父类当中的函数指针得到的是一块指针 -> 就类似c++当中的虚函数指向虚表当中的某个函数
   /**
    * 只不过在这里的模型是
    * 对象指针 -> 模拟虚函数指针 -> 具体方法
   */

   printf("%s says: ", cat.base.name);
   cat.base.speak();

   printf("%s says: ", tcat.cat.base.name);
   tcat.cat.base.speak();


   getchar();
   return 0;
}

由于c当中无法在结构体内声明具体方法,所以在实现起来的时候实现的方式是使用指针指向实现的模拟c++当中的虚函数

可以看到正是处于内存层面的动态绑定的设计才会有了虚函数虚表的设计

 

标签:函数,vtbl,void,struct,vptr,父类,eat,指针
From: https://www.cnblogs.com/JunkingBoy/p/18139444

相关文章

  • rust引用计数智能指针Rc<T>
    大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。为了启用......
  • 翻译|指针很复杂,或者说:字节里究竟有什么?
    本文原作者:RalfJung,原文地址:https://www.ralfj.de/blog/2018/07/24/pointers-and-bytes.html今天夏天,我再次完全使用Rust开发,并致力于(除其他事项外)为Rust/MIR开发一个“内存模型”。不过,在谈论我今年的想法之前,我终于要花点时间打破“指针很简单:他们只是一些整数”的神话了。......
  • 函数及指针
    c语言递归函数就是一个函数调用了函数本身要有一个明显结束的条件要有一个结束条件的趋势常用系统函数字符串函数标准库头文件<string.h>strlen(str)返回一个数组的长度(有元素的长度字符数组中的结束标识符不算)接收类型是size_tstrcpy(str,str1)将str1的字符串......
  • go切片和指针切片
    转载请注明出处:在Go语言中,切片(Slice)和指针的切片(即切片中每个元素都是指向某种数据类型的指针)是两个不同的概念,它们各自具有特定的用途和优势。切片(Slice)切片是对数组的一个连续片段的引用,它提供了对数组元素集合的抽象表示。切片底层数据结构都是数组,它包含三个关键部......
  • C语言10-指针(多级指针、空指针、野指针),自定义数据类型(枚举enum、结构体struct)
    第12章指针pointer12.6多级指针指向指针的指针称为多级指针eg:int*ptr1=&num; int**ptr2=&ptr1; int***ptr3=&ptr2;12.7空指针应用场景:1.暂时不确定指向的指针,可以在定义的时候先赋值为NULL2.有些指针函数,如果内部出现异常无法成功实现功能,可以返回NUL......
  • 二维字符串数组的传参时与指针互转时的问题
    二维数组如何传参二维字符串数组,转char**会导致的问题,以及编译报错要想得到正确的结果,需要按如下方式去写传参:#include<stdio.h>#include<string.h>//intchar_arr_copy(char**dest)//这样定义传参类型将导致编译报错,在低版本的编译器下或者没有报错但是得不到正确......
  • C++ 引用和指针:内存地址、创建方法及应用解析
    C++引用和指针创建引用引用变量是对现有变量的“别名”,它是使用&运算符创建的:stringfood="Pizza";//食物变量string&meal=food;//对food的引用现在,我们可以使用变量名food或引用名meal来引用食物变量:cout<<food<<"\n";//输出Pizzacout<<mea......
  • C语言09-指针(指针数组、数组指针、字符指针),值传递和引用传递,指针和函数,注释写法
    第12章指针pointer12.3指针和数组①数组名可以把数组名当做是存储了首元素地址的常量。//arr的类型表示为int[5]intarr[5]={10,20,30,40,50};②指针数组指针数组(PointerArray)是一个数组,其中的每个元素都是指针。intnum1=10,num2=20,num3=30;//ptr_......
  • 嵌入式之指针、数组、函数篇(三)
    三、指针、数组、函数1.什么是指针?指针其实也是个变量,只不过这个变量里面存储的是内存地址。2.什么是指针的类型?举个例子:int*a;指针类型为int*char*c;指针类型为char*3.什么是指针所指向类型举个例子:int*a;指针指向类型为intchar*c;指针指向类型为char注......
  • C语言—指针变量作函参改错
    下面程序用于将6个数按输入时顺序的逆序进行排列。找出其中错误,并改正之。#include<stdio.h>voidSwap(int*x,int*y){    int*temp;    temp=x;    x=y;        y=temp;}voidSort(char*p,intm){  inti;  charchange,......