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

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++当中的虚函数

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

 

posted @ 2024-04-16 22:34  俊king  阅读(94)  评论(0)    收藏  举报