首页
社区
课程
招聘
[原创] C语言实现面向对象, 纯C实现多态还有继承, 面向对象的本质, 手撕面向对象
发表于: 2025-2-10 18:18 2455

[原创] C语言实现面向对象, 纯C实现多态还有继承, 面向对象的本质, 手撕面向对象

2025-2-10 18:18
2455

警告: 本文章包括但不限于以下内容: 面向对象的本质, 手撕面向对象, 面向对象的底层原理, 面向对象的汇编实现

学习目标: 用纯C实现虚函数, 多态还有继承, 加深对面向对象的理解, 手刃面向对象

-2147483648 岁以下的程序员请在父母的陪同下观看 (不是qq号)

作者: 溯水流光

脚本猫同名, 52pojie 1024Jessical, csdn RedDragon, 精易 帝都骑士

上来我先发表一个究极暴论:

C++ 不过是 C 的简化和封装, C 不过是对 汇编 的简化和封装

C 也是面向对象的语言, 只不过其没有面向对象的语法糖罢了

没有所谓的面向过程, 面向过程只不过是学艺不精的乌合之众凭空捏出来的概念罢了

没有"面向过程"和"面向对象"的对立, 万物皆对象, cpu也是对象, 调用cpu中断也是广义上的面向对象, 如同调用cpu的成员方法

先不要着急把我斗倒斗臭, 且听我娓娓道来, 图文结合, 手撕C++的面向对象, 用C也实现继承与多态

0x00 一个广为流传的比喻

面向对象的三大特性: 封装, 继承, 多态

很多人对面向对象的理解是有失偏颇的,我最常听到的面向对象的理解是一个洗衣服的比喻,"面向过程,就是你要自己手洗衣服,步步都要亲力亲为。而面向对象,就是直接交给洗衣机这个对象去洗,方便快捷,事情都交给对象干",这段比喻有两个问题,

  • 这个比喻只体现了面向对象的封装性

  • 它没考虑到函数的封装性,函数也能封装底层细节,对上层提供易用的接口,让你不用"亲力亲为",调用api就好了, 如控制台输出一句话, 直接printf就好了, 从这个角度看, 面向对象并没有体现出比"面向过程"更加强大的优势

0x01 数据 + 算法 = 程序 ✅

其实面向对象的本质,简单来说就是属性加上方法,也就是一块内存区域(就是一个实例的所有成员变量),再加上以这块内存区域为指针参数的代码块(就是成员方法)

非虚函数方法的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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, 打断点, 开反汇编

1

2

这里我们可以看到成员方法的调用方式和全局函数别无二致, 无非是多了个C++源码中看不到的对象实例的this指针

2

方法的本质也就是一块填满了汇编代码的代码块, 该代码块其实也是内存块,在汇编中,不区分数据和指令,通通都是位于内存中的二进制数据,关键是你如何去解读这段数据,你用IP寄存器指向的就是指令,DS段寄存器指向的就是数据, 详细可以看王爽老师的<汇编语言>

继承的本质

继承其实就是结构体嵌套

注: JS和lua里的继承,是通过原型链来实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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;
}

4

讲继承的写法改为结构体嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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;
}

5

derived01实例在内存中的布局和继承一模一样, 没有因为多了个base类型的成员变量而内存布局产生改变

C语言也能实现面向对象,也能实现继承。你去研究C++继承底层实现的汇编代码就会发现,继承其实就是结构体嵌套(像JS和lua里的继承,是通过原型链来实现的)。有些人把C语言实现的这种叫“基于对象”,觉得只有底层封装好的那些语法糖才叫“面向对象”,这其实是他们没弄懂面向对象的底层逻辑

虚函数的本质

面向对象的一个关键点就是多态,虚函数可以实现多态,但虚函数的本质和底层实现,经过调试汇编代码和观察内存结构可以知道,虚函数是通过对象实例前的一个函数指针数组(虚表vtable)的指针成员变量实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 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;
}

虚函数表的存储

6

虚表的调用

7

接下来就是重头戏了, 看我用C实现C++的多态与继承, 为了防止我作弊, 我直接把后缀改为.c童叟无欺

8

这里用纯C实现了多态, 继承, 完整项目代码(记得保存为.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 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;
}

所以,我们得从汇编的角度去看这些问题,才能真正理解面向对象的本质

重构, 添加虚析构函数

上面的代码为了方便阐述问题, 省略了虚析构函数, 下面可以重构代码, 添加虚析构函数, 先析构子实例, 再析构父实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 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成员变量无法修改这一说

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
 
class Base {
private:
    int age = 18;
};
 
int main() {
    Base base;
    int* pInt = (int*)&base;
    printf("%d", pInt[0]); // 输出18
}

尾声

感谢你的阅读, 如有任何问题欢迎来讨论


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-2-11 15:36 被翻身的咸鱼编辑 ,原因: 忘记加标题了QwQ 忘记去掉草稿了 调整了排版
收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 9
活跃值: (130)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
1
2025-2-10 20:10
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2025-2-11 09:37
0
游客
登录 | 注册 方可回帖
返回