吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1371|回复: 13
收起左侧

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

  [复制链接]
1024Jessica 发表于 2025-2-10 18:02
本帖最后由 1024Jessica 于 2025-2-11 15:34 编辑

面向对象的本质, 手撕面向对象, 面向对象的底层原理, 面向对象的汇编实现

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

学习目标: 用纯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, 打断点, 开反汇编

1.png

2.png

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

3.png

方法的本质也就是一块填满了汇编代码的代码块, 该代码块其实也是内存块,在汇编中,不区分数据和指令,通通都是位于内存中的二进制数据,关键是你如何去解读这段数据,你用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;
}

4.png

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

// 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.png

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;
}

虚函数表的存储

6.png

虚表的调用

7.png

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

8.png

这里用纯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
}

尾声

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

免费评分

参与人数 5威望 +1 吾爱币 +14 热心值 +5 收起 理由
HengXin666 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
苏紫方璇 + 1 + 10 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
笙若 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
sergin + 1 + 1 谢谢@Thanks!
lswdla + 1 + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

qq465881818 发表于 2025-2-11 08:10
原来阁下也是同时学汇编 c cpp 三门语言的 和我那时候上学的时候一样
 楼主| 1024Jessica 发表于 2025-3-1 20:41
上面有朋友问"学汇编有什么好的教材推荐吗?", 我这边给出我的书单推荐:

汇编语言学习书单推荐:
王爽老师所著的《汇编语言》(汇编入门, 16位汇编)
李忠老师编写的《x86 汇编语言 从实模式到保护模式》(含保护模式与手写系统内核)
罗云彬老师的《Windows 环境下 32 位汇编语言程序设计》(强烈推荐! 讲解 win32 汇编的)
上述书籍我均已通读,内容系统全面,质量上乘,非常推荐。
FlintJie 发表于 2025-2-11 09:52
殇。默语 发表于 2025-2-11 10:42
Mark一下,学习
starW 发表于 2025-2-13 09:15
谢谢分享
kezzyhu 发表于 2025-2-18 20:13
直面本质,赞
zhlking 发表于 2025-2-19 14:22
学习了...
starLightDream 发表于 2025-2-20 17:48
感谢大佬分享。
大佬们,学汇编有什么好的教材推荐吗?
981930674 发表于 2025-2-21 14:27
有用,汇编 C C++,三语互通讲解,很好的示例  。标记一下 语言实现面向对象, 纯C实现多态还有继承, 面向对象的本质, 手撕面向对象  
smallppgirl 发表于 2025-2-28 19:13
可以帮助理解c++的面向对象,理解c++的类
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-7-18 00:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表