找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1846

积分

0

好友

246

主题
发表于 19 小时前 | 查看: 3| 回复: 0

想象一下,你是一名1980年代初的C语言程序员,正埋头开发一套图形界面库。今天的工作是处理圆形、矩形、三角形这三种基本图形,而每种图形都需要实现两个核心操作:绘制和计算面积。

你端起咖啡抿了一口,开始在键盘上敲下第一行代码。

最初的实现非常直接,但也非常笨重:

void draw_circle(Circle* c)
{
    ...
}

void draw_rectangle(Rectangle* r)
{
    ...
}

// 使用时需要判断类型
void draw_shape(void* shape, int type){
if (type == CIRCLE) {
        draw_circle((Circle*)shape);
    } else if (type == RECTANGLE) {
        draw_rectangle((Rectangle*)shape);
    } else if (type == TRIANGLE) {
        draw_triangle((Triangle*)shape);
    }
}

硬编码类型判断的噩梦

第二天,产品经理说要加个梯形。你只好打开项目中每一处调用 draw_shape 的代码,小心翼翼地加上新的 else if 分支。

第三天,需求又变成了椭圆。你不得不重复一遍上述操作。

很快,一个让人头疼的问题清晰地浮现出来:如何用同一套代码处理不同类型的图形? 你盯着屏幕上日益臃肿的 if-else 链条,仿佛看到了未来它膨胀到200行的样子。你意识到,这条路走不通了

必须找到一种更优雅、更统一的方式来处理所有类型。

把函数指针定义在结构体中

你记起了C语言里的“函数指针”。能不能把函数指针直接塞进结构体里呢?说干就干,你写出了下面这段代码:

typedef struct {
void (*draw)(void*);
double (*area)(void*);
} Shape;

typedef struct {
    Shape base;
double radius;
} Circle;

void draw_circle(void* obj){
    Circle* c = (Circle*)obj;
printf("Drawing circle with radius %.2f\n", c->radius);
}

double calc_circle_area(void* obj){
    Circle* c = (Circle*)obj;
return 3.14 * c->radius * c->radius;
}

Circle c;
c.base.draw = draw_circle;
c.base.area = calc_circle_area;
c.radius = 5.0;

// 调用时不再需要判断类型!
c.base.draw(&c);

成功了!现在所有图形都能通过一个统一的 Shape 基类指针来调用:

Shape* shapes[3] = {&circle.base, &rect.base, &triangle.base};
for (int i = 0; i < 3; i++) {
    shapes[i]->draw(shapes[i]);  // 统一的接口!
}

然而,这只是解决了第一个问题。

致命缺陷暴露

几周后,你的同事急匆匆打来电话:“程序又崩了,快来看看!” 你查日志、加断点,最终定位到问题源头:一位实习生写的代码忘记初始化函数指针了。

Circle c1, c2, c3;
c1.base.draw = draw_circle;
c1.base.area = calc_circle_area;
// 实习生忘记初始化c2的函数指针了!
c2.radius = 10.0;
c2.base.draw(&c2);  // 程序崩溃!尝试调用空指针

更糟糕的情况是,函数指针有时会被错误地设置:

Circle c;
c.base.draw = draw_rectangle;  // 编译器不会报错
c.base.area = calc_circle_area;
c.radius = 5.0;
c.base.draw(&c);  // 运行时崩溃,类型不匹配

你盯着代码恍然大悟:函数指针不应该由用户手动设置,它应该由“类型”本身自动决定。 每个 Circle 对象的 draw 指针都应该是 draw_circle,这是 Circle 类型的固有属性,为何要让程序员在每次创建对象时都手动设置一遍?

初始化函数自动设置

你想到了一个改进方案:提供专门的初始化函数,在创建对象时自动设置正确的函数指针。

void init_circle(Circle* c, double radius){
    c->base.draw = draw_circle;      // 自动设置
    c->base.area = calc_circle_area; // 自动设置
    c->radius = radius;
}

void init_rectangle(Rectangle* r, double w, double h){
    r->base.draw = draw_rectangle;
    r->base.area = calc_rectangle_area;
    r->width = w;
    r->height = h;
}

// 使用
Circle c;
init_circle(&c, 5.0);  // 再也不会忘记设置函数指针了
c.base.draw(&c);

新方案部署后,之前频繁出现的“忘记设置指针”问题果然消失了。

新问题:内存不够用了

安稳日子没过多久,测试工程师指着监控面板上的内存占用曲线问你:“为什么创建10000个圆形对象会占用这么多内存?”

你深入排查后发现了新问题:虽然每个 Circle 对象只有两个函数指针,但关键在于,这10000个对象的函数指针都指向完全相同的两个函数(draw_circlecalc_circle_area)。为什么要把相同的内容存储一万份?

一个新的想法开始萌芽:既然所有同类型对象的函数指针都一样,能不能让它们共享同一份?

函数表的诞生

凌晨两点,灵感如闪电般击中了你:为什么不把函数指针单独存成一张表,让所有同类型的对象都指向这张共享的表呢?

你立刻开始重构代码:

// 虚函数表(所有Circle对象共享)
typedef struct {
void (*draw)(void*);
double (*area)(void*);
} Function_Table;

Function_Table circle_ftable = {
    .draw = draw_circle,
    .area = calc_circle_area
};

Function_Table rectangle_ftable = {
    .draw = draw_rectangle,
    .area = calc_rectangle_area
};

// 每个对象只存储一个指向共享表的指针
typedef struct {
    Function_Table* ftable;  // 指向共享的函数表
double radius;
} Circle;

typedef struct {
    Function_Table* ftable;
double width;
double height;
} Rectangle;

void init_circle(Circle* c, double radius){
    c->ftable = &circle_ftable;  // 指向共享表
    c->radius = radius;
}

void init_rectangle(Rectangle* r, double w, double h){
    r->ftable = &rectangle_ftable;
    r->width = w;
    r->height = h;
}

// 调用时通过表指针间接跳转
Circle c;
init_circle(&c, 5.0);
c.ftable->draw(&c);

经过测试,效果惊人:现在一万个 Circle 对象只需要额外存储一个 ftable 指针,相比之前在每个对象内存储两个函数指针的方案,内存节省了近80KB!

而且,处理不同类型的图形变得前所未有的简洁:

void draw_all(Shape** shapes, int count) {
for (int i = 0; i < count; i++) {
        shapes[i]->ftable->draw(shapes[i]);
    }
}

支持继承和方法覆盖

新方案赢得了团队的一致好评。但几天后,产品经理带着新需求来了:“我们需要一种‘可填充圆形’,绘制时先画轮廓再填充颜色。”

你发现可以复用 Circle 的大部分逻辑,只需重写 draw 函数。于是,你尝试创建新的 FilledCircle

// 覆盖draw方法,复用area方法
void draw_filled_circle(void* obj){
    Circle* c = (Circle*)obj;
    draw_circle(obj);         // 先调用父类方法
printf("Filling with color\n");  // 再增加填充逻辑
}

Function_Table filled_circle_ftable = {
    .draw = draw_filled_circle,  // 覆盖draw方法
    .area = calc_circle_area     // 复用area方法
};

typedef struct {
    Circle base;  // 继承Circle的所有数据成员
} FilledCircle;

void init_filled_circle(FilledCircle* fc, double r){
    fc->base.ftable = &filled_circle_ftable;  // 指向新的函数表
    fc->base.radius = r;
}

通过让 FilledCircle “继承” Circle 的数据结构,并让其 ftable 指针指向一个修改了 draw 条目的新表,你巧妙地实现了方法的覆盖和复用。

C++的语法糖

1983年,你把这套精妙的机制展示给了一位名叫 Bjarne Stroustrup 的同事。他看完后评价道:“想法非常棒!但用起来太繁琐了。每次都要手动定义 ftable、在 init 函数里设置指针、调用时还要写 obj->ftable->func(obj)……能不能让编译器自动完成这些呢?

几个月后,他拿着设计好的新语法来找你:

class Shape {
public:
virtual void draw() = 0;     // 编译器会为此生成vtable槽位
virtual double area() = 0;
};

class Circle : public Shape {
private:
double radius;
public:
    Circle(double r) : radius(r) {}  // 构造函数中,编译器会自动设置vtable指针

void draw() override {           // 编译器会自动更新vtable中的对应条目
printf("Drawing circle with radius %.2f\n", radius);
    }

double area() override {
return 3.14 * radius * radius;
    }
};

// 使用
Circle c(5.0);
c.draw();  // 编译器在背后将其翻译为 c.vtable->draw(&c)

看到这段代码,你瞬间明白了:C++ 中神秘的 virtual 关键字和 vtable(虚函数表),本质上就是将你在C语言中手动实现的函数表机制自动化、标准化了。Bjarne 为这套机制赋予了“虚函数”和“多态”这些高大上的名字,但其核心思想,早已在你那杯凌晨的咖啡中酝酿成形。

这段从硬编码类型判断到共享函数表,最终被C++吸纳成为语言特性的演进历程,深刻揭示了计算机基础中抽象与自动化的力量。如果你对这类底层机制和演进故事感兴趣,欢迎来云栈社区的C++板块继续深入探讨。




上一篇:MoonBit在日本开发者社区走红:高性能与Wasm应用实践解析
下一篇:Node.js多线程编程:使用worker_threads处理CPU密集型任务
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-6 22:32 , Processed in 0.519292 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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