面向对象的本质, 手撕面向对象, 面向对象的底层原理, 面向对象的汇编实现
警告: 本文章包括但不限于以下内容: 面向对象的本质, 手撕面向对象, 面向对象的底层原理, 面向对象的汇编实现
学习目标: 用纯C实现虚函数, 多态还有继承, 加深对面向对象的理解, 手刃面向对象
-2147483648 岁以下的程序员请在父母的陪同下观看 (不是qq号)
作者: 溯水流光
脚本猫同名, 52pojie 1024Jessical, csdn RedDragon, 精易 帝都骑士
上来我先发表一个究极暴论:
C++ 不过是 C 的简化和封装, C 不过是对 汇编 的简化和封装
C 也是面向对象的语言, 只不过其没有面向对象的语法糖罢了
没有所谓的面向过程, 面向过程只不过是学艺不精的乌合之众凭空捏出来的概念罢了
没有"面向过程"和"面向对象"的对立, 万物皆对象, cpu也是对象, 调用cpu中断也是广义上的面向对象, 如同调用cpu的成员方法
先不要着急把我斗倒斗臭, 且听我娓娓道来, 图文结合, 手撕C++的面向对象, 用C也实现继承与多态
0x00 一个广为流传的比喻
面向对象的三大特性: 封装, 继承, 多态
很多人对面向对象的理解是有失偏颇的,我最常听到的面向对象的理解是一个洗衣服的比喻,"面向过程,就是你要自己手洗衣服,步步都要亲力亲为。而面向对象,就是直接交给洗衣机这个对象去洗,方便快捷,事情都交给对象干",这段比喻有两个问题,
-
这个比喻只体现了面向对象的封装性
-
它没考虑到函数的封装性,函数也能封装底层细节,对上层提供易用的接口,让你不用"亲力亲为",调用api就好了, 如控制台输出一句话, 直接printf就好了, 从这个角度看, 面向对象并没有体现出比"面向过程"更加强大的优势
0x01 数据 + 算法 = 程序 ✅
其实面向对象的本质,简单来说就是属性加上方法,也就是一块内存区域(就是一个实例的所有成员变量),再加上以这块内存区域为指针参数的代码块(就是成员方法)
非虚函数方法的本质
// Demo01.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <string>
using namespace std;
class Dog {
public:
string name;
int age; // val: 1
int price; // val: 2
int weight; // val: 3
Dog(string _name, int _age, int _price, int _weight)
: name(_name), age(_age), price(_price), weight(_weight) {};
// 狗吃肉, 变大变高, 返回新的重量
int Eat(int meat) {
this->weight += meat;
return this->weight;
}
};
int main()
{
Dog dog("旺财", 1, 2, 3);
int newWeight = dog.Eat(3);
cout << "dog's new weight is " << newWeight;
}
开VS, 打断点, 开反汇编
这里我们可以看到成员方法的调用方式和全局函数别无二致, 无非是多了个C++源码中看不到的对象实例的this指针
方法的本质也就是一块填满了汇编代码的代码块, 该代码块其实也是内存块,在汇编中,不区分数据和指令,通通都是位于内存中的二进制数据,关键是你如何去解读这段数据,你用IP寄存器指向的就是指令,DS段寄存器指向的就是数据, 详细可以看王爽老师的<汇编语言>
继承的本质
继承其实就是结构体嵌套
注: JS和lua里的继承,是通过原型链来实现的
// Demo01.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
int foo1 = 1; // val: 1
int foo2 = 2; // val: 2
};
class Derived: public Base {
public:
int fooToken = 0x99; // val: 0x99
int foo3 = 3; // val: 3
int foo4 = 4; // val: 4
int fooEnd = 0x88; // val: 0x88
};
int main() {
Derived derived01;
derived01.foo1 = 1;
Derived derived02;
derived02.foo1 = 1;
}
讲继承的写法改为结构体嵌套:
// Demo01.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
int foo1 = 1; // val: 1
int foo2 = 2; // val: 2
};
class Derived {
public:
Base base;
int fooToken = 0x99; // val: 0x99
int foo3 = 3; // val: 3
int foo4 = 4; // val: 4
int fooEnd = 0x88; // val: 0x88
};
int main() {
Derived derived01;
derived01.base.foo1 = 1;
Derived derived02;
derived02.base.foo1 = 1;
}
derived01实例在内存中的布局和继承一模一样, 没有因为多了个base类型的成员变量而内存布局产生改变
C语言也能实现面向对象,也能实现继承。你去研究C++继承底层实现的汇编代码就会发现,继承其实就是结构体嵌套(像JS和lua里的继承,是通过原型链来实现的)。有些人把C语言实现的这种叫“基于对象”,觉得只有底层封装好的那些语法糖才叫“面向对象”,这其实是他们没弄懂面向对象的底层逻辑
虚函数的本质
面向对象的一个关键点就是多态,虚函数可以实现多态,但虚函数的本质和底层实现,经过调试汇编代码和观察内存结构可以知道,虚函数是通过对象实例前的一个函数指针数组(虚表vtable)的指针成员变量实现的
// Demo01.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <string>
using namespace std;
class Animal {
public:
int weight; // val: 3
Animal(int _weight)
:weight(_weight) {};
// 动物吃肉, 变大变高, 返回新的重量
virtual int Eat(int meat) {
this->weight += meat;
return this->weight;
}
virtual void Move() {
cout << "坐地日行八万里" << endl;
}
};
class Dog : public Animal {
public:
Dog(int _weight)
:Animal(_weight) {};
void Bark() {
cout << "汪汪" << endl;
}
};
int Feed(Animal* animal) {
return animal->Eat(3);
}
int main()
{
Dog dog(3);
auto EatFnPtr = &Animal::Eat;
auto MoveFnPtr = &Animal::Move;
int newWeight = Feed(&dog);
cout << "dog's new weight is " << newWeight;
}
虚函数表的存储
虚表的调用
接下来就是重头戏了, 看我用C实现C++的多态与继承, 为了防止我作弊, 我直接把后缀改为.c
童叟无欺
这里用纯C实现了多态, 继承, 完整项目代码(记得保存为.c)
// Demo01.c : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#define CALL_VT(pObject, index) ((Object*)(pObject))->pvt[(index)]((pObject))
#define SET_VT(pObject, _pvt) ((Object*)pObject)->pvt = (_pvt);
typedef int (*FnPtr)();
// 所有类的父类, 其有一个虚表指针
typedef struct tagObject {
FnPtr* pvt;
} Object;
// ==============
// 动物的实现
// ===============
typedef struct tagAnimal {
Object object;
int weight; // val: 3
} Animal;
extern FnPtr g_animalVirtualTable[];
// 构造函数
void InitAnimal(Animal* pAnimal, int weight) {
SET_VT(pAnimal, g_animalVirtualTable);
pAnimal->weight = weight;
}
#define ANIMAL_EAT_VT_INDEX 0
// 用带后缀的_Animal代表是虚函数, C语法不支持重载
// 动物吃肉, 变大变高, 返回新的重量
int Eat_Animal(Animal* pAnimal, int meat) {
printf("调用了Animal的Eat\n");
pAnimal->weight += meat;
return pAnimal->weight;
}
#define ANIMAL_MOVE_VT_INDEX 1
void Move_Animal(Animal* pAnimal) {
printf("坐地日行八万里\n");
}
FnPtr g_animalVirtualTable[] = {
Eat_Animal,
Move_Animal
};
// ==============
// 动物派生类, 狗的实现
// ===============
typedef struct tagDog{
Animal animal;
} Dog;
extern FnPtr g_dogVirtualTable[];
void InitDog(Dog* pDog, int weight) {
InitAnimal((Animal*)pDog, weight);
SET_VT(pDog, g_dogVirtualTable);
}
// 普通的成员变量
void Bark(Dog* pDog) {
printf("汪汪");
}
// 狗生病了, 吃了就吐
int Eat_dog(Dog* pDog, int meat) {
printf("调用了Dog的Eat 但Dog生病了, 吃了就吐\n");
Animal* pAnimal = (Animal*)pDog;
pAnimal->weight += 1;
return pAnimal->weight;
}
FnPtr g_dogVirtualTable[] = {
Eat_dog,
Move_Animal
};
int Feed(Animal* pAnimal) {
// CALL_VT(pAnimal, ANIMAL_MOVE_VT_INDEX);
return CALL_VT(pAnimal, ANIMAL_EAT_VT_INDEX);
}
int main()
{
Dog dog;
InitDog(&dog, 3);
Feed(&dog);
Animal animal;
InitAnimal(&animal, 6);
Feed(&animal);
return 0;
}
所以,我们得从汇编的角度去看这些问题,才能真正理解面向对象的本质
重构, 添加虚析构函数
上面的代码为了方便阐述问题, 省略了虚析构函数, 下面可以重构代码, 添加虚析构函数, 先析构子实例, 再析构父实例
// Demo01.c : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
typedef int (*FnPtr)();
// 所有类的父类, 其有一个虚表指针
typedef struct tagObject {
FnPtr* pvt;
} Object;
// 将宏重构为 inline 函数
inline int CallVt(Object* pObject, int index) {
return (pObject)->pvt[index](pObject);
}
inline void SetVt(Object* pObject, FnPtr* pvt) {
pObject->pvt = pvt;
}
// ==============
// 动物的实现
// ===============
typedef struct tagAnimal {
Object object;
int weight; // val: 3
} Animal;
extern FnPtr g_animalVirtualTable[];
// 构造函数
void InitAnimal(void* _pAnimal, int weight) {
Animal* pAnimal = (Animal*)_pAnimal;
SetVt(pAnimal, g_animalVirtualTable);
pAnimal->weight = weight;
}
// 使用const, 避免define的重复定义, 增加代码的健壮性
const int ANIMAL_EAT_VT_INDEX = 0;
// 用带后缀的_Animal代表是虚函数, C语法不支持重载
// 动物吃肉, 变大变高, 返回新的重量
int Eat_Animal(Animal* pAnimal, int meat) {
printf("调用了Animal的Eat\n");
pAnimal->weight += meat;
return pAnimal->weight;
}
const ANIMAL_MOVE_VT_INDEX = 1;
void Move_Animal(Animal* pAnimal) {
printf("坐地日行八万里\n");
}
// C++ 规范: 所有的基类都要把析构函数定义为析构函数, 方便扩展
// 并保证, 使用父类指针的容器, 能够正确回收资源
const int ANIMAL_DESTROY = 2;
void Destroy_Animal(Animal* pAnimal) {
// 虚析构函数
printf("Animal 实例析构了\n");
}
FnPtr g_animalVirtualTable[] = {
Eat_Animal,
Move_Animal,
Destroy_Animal
};
// ==============
// 动物派生类, 狗的实现
// ===============
typedef struct tagDog{
Animal animal;
} Dog;
extern FnPtr g_dogVirtualTable[];
void InitDog(Dog* pDog, int weight) {
InitAnimal((Animal*)pDog, weight);
SetVt(pDog, g_dogVirtualTable);
}
// 普通的成员变量
void Bark(Dog* pDog) {
printf("汪汪");
}
// 狗生病了, 吃了就吐
int Eat_dog(Dog* pDog, int meat) {
Animal* pAnimal = (Animal*)pDog;
printf("调用了Dog的Eat 但Dog生病了, 吃了就吐\n");
pAnimal->weight += 1;
return pAnimal->weight;
}
void Destroy_Dog(Dog* pDog) {
// 虚析构函数
printf("Dog 实例析构了\n");
// 先析构子实例, 再析构父实例
Destroy_Animal(pDog);
}
FnPtr g_dogVirtualTable[] = {
Eat_dog,
Move_Animal,
Destroy_Dog
};
int Feed(Animal* pAnimal) {
// CallVt(pAnimal, ANIMAL_MOVE_VT_INDEX);
return CallVt(pAnimal, ANIMAL_EAT_VT_INDEX);
}
int main()
{
Dog dog;
InitDog(&dog, 3);
Feed(&dog);
Animal animal;
InitAnimal(&animal, 6);
Feed(&animal);
CallVt(&dog, ANIMAL_DESTROY);
CallVt(&animal, ANIMAL_DESTROY);
return 0;
}
0x03 指针为什么不安全? 从面向对象角度理解的指针安全问题
访问修饰符public
private
都是编译器加的限制, 而不是汇编层面, 基于段页, 设置了只读无法修改. 类在栈或堆中实例化, 没有什么private
成员变量无法修改这一说
对指针的理解要出神入化
#include <iostream>
class Base {
private:
int age = 18;
};
int main() {
Base base;
int* pInt = (int*)&base;
printf("%d", pInt[0]); // 输出18
}
尾声
感谢你的阅读, 如有任何问题欢迎来讨论