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