
引言
很多人觉得 C 语言不支持面向对象编程。严格来说,C 确实没有 class、virtual、private 这些关键字。但如果你看过 Linux 内核、FreeRTOS、GLib 或者各种嵌入式设备框架的源码,你会发现它们全都在用 C 做"面向对象"——而且做得非常优雅。
为什么要用 C 模拟面向对象?有两个原因:
- 理解 C++ 的底层原理。C++ 的 class、继承、虚函数在编译后到底变成了什么?答案是:和你用 C 手动实现的东西几乎一模一样。理解了 C 版本,你就真正理解了 C++
- 在纯 C 项目中获得 OOP 的好处。嵌入式领域大量项目只用 C(编译器限制、代码体积、实时性要求),但仍然需要封装、多态、可替换等设计能力
本文是"深入理解 C++"系列的第一篇。我们从最底层出发——用纯 C 实现封装、继承、多态,然后对照 C++ 编译器的实际输出,揭示 C++ 面向对象的底层真相。
封装:struct + 函数指针
C++ 的 class 本质上是什么
C++ 中一个简单的类:
class Uart {
public:
void init(int baudrate);
void send(const char *data, int len);
private:
int fd;
int baudrate;
};
编译后,这个 class 在内存中就是一个 struct。成员函数不存在于对象内部——它们和普通函数一样放在 .text 段,只是编译器自动在第一个参数位置插入了 this 指针。
也就是说,uart.init(115200) 在编译后变成了类似 Uart_init(&uart, 115200) 的调用。
用 C 实现封装
理解了这个本质,用 C 实现封装就很自然了:
// uart.h
typedef struct {
int fd;
int baudrate;
} Uart;
void uart_init(Uart *self, int baudrate);
void uart_send(Uart *self, const char *data, int len);
// uart.c
#include "uart.h"
void uart_init(Uart *self, int baudrate)
{
self->baudrate = baudrate;
self->fd = -1;
// 配置硬件...
}
void uart_send(Uart *self, const char *data, int len)
{
// 使用 self->fd 发送数据...
}
使用方式:
Uart uart1;
uart_init(&uart1, 115200);
uart_send(&uart1, "Hello", 5);
和 C++ 版本对比:
Uart uart1;
uart1.init(115200);
uart1.send("Hello", 5);
区别仅在语法糖——uart1.init(115200) 编译后本质就是 Uart::init(&uart1, 115200)。
内存布局完全相同
用 sizeof 和 offsetof 验证:
// C 版本
printf("sizeof(Uart) = %zu\n", sizeof(Uart)); // 8
printf("offset of fd = %zu\n", offsetof(Uart, fd)); // 0
printf("offset of baudrate = %zu\n", offsetof(Uart, baudrate)); // 4
// C++ 版本(没有虚函数时)
printf("sizeof(Uart) = %zu\n", sizeof(Uart)); // 8
printf("offset of fd = %zu\n", offsetof(Uart, fd)); // 0
printf("offset of baudrate = %zu\n", offsetof(Uart, baudrate)); // 4
结果完全一致。没有虚函数的 C++ class 和 C struct 的内存布局是一样的。
访问控制:static 模拟 private
C++ 的访问控制
C++ 用 private/protected/public 控制成员的可访问性。但这只是编译期检查——编译器在编译时拒绝对 private 成员的直接访问,运行时没有任何保护。如果你用指针偏移强行访问 private 成员,程序照样能跑。
C 的替代方案
C 没有 private 关键字,但有 static——将函数或变量的可见性限制在当前文件内。效果甚至比 C++ 的 private 更强,因为 static 在链接层面就不可见了,而 C++ 的 private 在链接层面仍然是导出符号。
// uart.c
#include "uart.h"
// "private" 函数——外部文件无法调用
static void configure_baudrate(Uart *self, int baudrate)
{
// 写寄存器...
self->baudrate = baudrate;
}
// "private" 变量——外部文件无法访问
static int error_count = 0;
// "public" 函数——头文件中声明,外部可用
void uart_init(Uart *self, int baudrate)
{
configure_baudrate(self, baudrate);
error_count = 0;
}
头文件只暴露"公有"接口,.c 文件中的 static 函数和变量就是"私有"的。
不透明指针(Opaque Pointer)
如果你想把 struct 的成员也藏起来(不在头文件中暴露),可以使用不透明指针模式:
// uart.h —— 只声明类型,不定义结构体内容
typedef struct Uart Uart;
Uart *uart_create(int baudrate);
void uart_destroy(Uart *self);
void uart_send(Uart *self, const char *data, int len);
// uart.c —— 结构体定义只在 .c 中
#include "uart.h"
#include <stdlib.h>
struct Uart {
int fd;
int baudrate;
char rx_buffer[256];
};
Uart *uart_create(int baudrate)
{
Uart *self = (Uart *)malloc(sizeof(Uart));
self->baudrate = baudrate;
self->fd = -1;
return self;
}
void uart_destroy(Uart *self)
{
free(self);
}
外部代码只持有 Uart * 指针,完全不知道里面有什么成员——比 C++ 的 private 隐藏得更彻底。修改 struct 内部成员不需要重新编译依赖它的代码(只需重新编译 uart.c),这就是 pimpl(pointer to implementation) 模式在 C 中的原生版本。
继承:结构体嵌套
C++ 继承的内存布局
C++ 继承在内存层面的实现非常直接——子类对象的开头就是父类对象:
class Device {
public:
const char *name;
int type;
};
class Uart : public Device {
public:
int baudrate;
int data_bits;
};
Uart 对象在内存中的布局:
偏移 成员
0 name ← Device 部分
8 type ← Device 部分
12 baudrate ← Uart 新增
16 data_bits ← Uart 新增
子类对象的前半部分就是一个完整的父类对象。这意味着 Uart* 可以安全地转换为 Device*——指针地址不需要调整。
用 C 实现继承
理解了内存布局,C 的实现方式就是把"父类"struct 作为"子类"struct 的第一个成员:
// 基类
typedef struct {
const char *name;
int type;
} Device;
// 子类——Device 作为第一个成员
typedef struct {
Device base; // "继承" Device
int baudrate;
int data_bits;
} UartDevice;
// 另一个子类
typedef struct {
Device base; // "继承" Device
int mode;
int frequency;
} SpiDevice;
关键在于"第一个成员"这个约束。C 标准保证结构体第一个成员的地址和结构体本身的地址相同,因此:
UartDevice uart_dev;
Device *dev = (Device *)&uart_dev; // 安全!指向同一地址
这个指针转换和 C++ 的向上转型(upcast)在机器码层面完全等价——都是零开销的地址传递。
使用"继承"
void uart_device_init(UartDevice *self, const char *name, int baudrate)
{
// 初始化"父类"部分
self->base.name = name;
self->base.type = 1;
// 初始化"子类"部分
self->baudrate = baudrate;
self->data_bits = 8;
}
// 接受"父类"指针的通用函数
void device_print_info(Device *dev)
{
printf("Device: %s, type: %d\n", dev->name, dev->type);
}
// 使用
UartDevice uart;
uart_device_init(&uart, "UART0", 115200);
// 向上转型——UartDevice* → Device*
device_print_info((Device *)&uart);
Linux 内核中大量使用这种模式。比如 struct cdev 嵌入了 struct kobject,struct net_device 嵌入了 struct device。内核还提供了 container_of 宏来做反向转换(从父类指针找回子类指针):
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
// 从 Device* 反推 UartDevice*
Device *dev = get_some_device();
UartDevice *uart = container_of(dev, UartDevice, base);
多态:函数指针表(vtable)
多态是面向对象中最强大的特性——通过基类指针调用函数,实际执行的是子类的实现。C++ 用虚函数实现,底层靠的是 vtable(虚函数表)。
C++ 虚函数的底层实现
class Animal {
public:
virtual void speak() = 0;
virtual void eat(const char *food) = 0;
const char *name;
};
编译器为 Animal 类生成了一个隐藏的 vtable:
Animal 的 vtable:
[0] → Animal::speak (纯虚,无实现)
[1] → Animal::eat (纯虚,无实现)
每个 Animal 对象的开头都有一个隐藏的指针 vptr,指向对应类的 vtable:
Animal 对象内存布局:
偏移 0: vptr → 指向 vtable
偏移 8: name
当子类 override 虚函数时,子类有自己的 vtable:
class Dog : public Animal {
public:
void speak() override { printf("Woof!\n"); }
void eat(const char *food) override { printf("Dog eats %s\n", food); }
};
Dog 的 vtable:
[0] → Dog::speak
[1] → Dog::eat
调用 animal->speak() 时,编译器生成的代码实际上是:
// animal->speak() 的真实执行过程
animal->vptr[0](animal); // 从 vtable 取出函数指针,传入 this
用 C 实现多态
理解了 vtable 的原理,用 C 手动实现就是水到渠成的事:
// "虚函数表"——函数指针结构体
typedef struct {
void (*speak)(void *self);
void (*eat)(void *self, const char *food);
} AnimalOps;
// "基类"
typedef struct {
const AnimalOps *ops; // vptr —— 指向虚函数表
const char *name;
} Animal;
// 通过基类指针调用"虚函数"
void animal_speak(Animal *animal)
{
animal->ops->speak(animal);
}
void animal_eat(Animal *animal, const char *food)
{
animal->ops->eat(animal, food);
}
然后是"子类"的实现:
// Dog "子类"
typedef struct {
Animal base; // "继承" Animal
int energy;
} Dog;
static void dog_speak(void *self)
{
Dog *dog = (Dog *)self;
printf("%s: Woof!\n", dog->base.name);
}
static void dog_eat(void *self, const char *food)
{
Dog *dog = (Dog *)self;
dog->energy += 10;
printf("%s eats %s, energy=%d\n", dog->base.name, food, dog->energy);
}
// Dog 的 vtable(全局唯一,只读)
static const AnimalOps dog_ops = {
.speak = dog_speak,
.eat = dog_eat,
};
void dog_init(Dog *dog, const char *name)
{
dog->base.ops = &dog_ops; // 设置 vptr
dog->base.name = name;
dog->energy = 100;
}
// Cat "子类"
typedef struct {
Animal base;
int mood;
} Cat;
static void cat_speak(void *self)
{
Cat *cat = (Cat *)self;
printf("%s: Meow!\n", cat->base.name);
}
static void cat_eat(void *self, const char *food)
{
Cat *cat = (Cat *)self;
cat->mood += 5;
printf("%s eats %s, mood=%d\n", cat->base.name, food, cat->mood);
}
static const AnimalOps cat_ops = {
.speak = cat_speak,
.eat = cat_eat,
};
void cat_init(Cat *cat, const char *name)
{
cat->base.ops = &cat_ops;
cat->base.name = name;
cat->mood = 50;
}
多态调用
int main(void)
{
Dog rex;
Cat whiskers;
dog_init(&rex, "Rex");
cat_init(&whiskers, "Whiskers");
// 向上转型,存入基类指针数组
Animal *animals[] = {
(Animal *)&rex,
(Animal *)&whiskers,
};
// 多态调用——同一个接口,不同的行为
for (int i = 0; i < 2; i++) {
animal_speak(animals[i]);
animal_eat(animals[i], "kibble");
}
return 0;
}
输出:
Rex: Woof!
Rex eats kibble, energy=110
Whiskers: Meow!
Whiskers eats kibble, mood=55
animal_speak 内部执行 animal->ops->speak(animal)——如果 animal 指向 Dog,就调用 dog_speak;指向 Cat,就调用 cat_speak。这和 C++ 虚函数的运行时分发机制完全相同。
对比 C++ 等价代码
class Animal {
public:
virtual void speak() = 0;
virtual void eat(const char *food) = 0;
const char *name;
};
class Dog : public Animal {
public:
Dog(const char *n) : energy(100) { name = n; }
void speak() override { printf("%s: Woof!\n", name); }
void eat(const char *food) override {
energy += 10;
printf("%s eats %s, energy=%d\n", name, food, energy);
}
private:
int energy;
};
class Cat : public Animal {
public:
Cat(const char *n) : mood(50) { name = n; }
void speak() override { printf("%s: Meow!\n", name); }
void eat(const char *food) override {
mood += 5;
printf("%s eats %s, mood=%d\n", name, food, mood);
}
private:
int mood;
};
int main() {
Dog rex("Rex");
Cat whiskers("Whiskers");
Animal *animals[] = { &rex, &whiskers };
for (auto a : animals) {
a->speak();
a->eat("kibble");
}
}
C++ 版本更简洁,但编译器在背后做的事情和我们 C 版本手动做的一模一样——生成 vtable、设置 vptr、通过函数指针分发调用。
构造与析构:init/deinit 函数
C++ 构造函数做了什么
C++ 构造函数看起来很神奇——对象一创建就自动初始化。但编译器做的事情很朴素:
- 分配内存(栈上自动分配,堆上通过
operator new) - 设置 vptr(如果有虚函数)
- 调用父类构造函数
- 初始化成员变量
- 执行构造函数体
析构函数反过来:
- 执行析构函数体
- 析构成员变量(逆序)
- 调用父类析构函数
- 释放内存(堆对象通过
operator delete)
C 中的等价实现
// 栈上对象——手动 init/deinit
Dog rex;
dog_init(&rex, "Rex"); // "构造"
// ... 使用 ...
dog_deinit(&rex); // "析构"
// 堆上对象——手动 create/destroy
Dog *rex = dog_create("Rex"); // malloc + init
// ... 使用 ...
dog_destroy(rex); // deinit + free
完整实现:
// "构造函数"
void dog_init(Dog *self, const char *name)
{
// 2. 设置 vptr
self->base.ops = &dog_ops;
// 3+4. 初始化成员
self->base.name = name;
self->energy = 100;
}
// "堆构造"
Dog *dog_create(const char *name)
{
Dog *self = (Dog *)malloc(sizeof(Dog));
if (self) {
dog_init(self, name);
}
return self;
}
// "析构函数"
void dog_deinit(Dog *self)
{
// 释放动态分配的资源
// (此例中没有,但真实项目可能有 buffer、句柄等)
}
// "堆析构"
void dog_destroy(Dog *self)
{
if (self) {
dog_deinit(self);
free(self);
}
}
RAII 的替代方案
C++ 的 RAII(Resource Acquisition Is Initialization)利用构造/析构自动管理资源。C 中没有自动析构,常见的替代方案:
方案 1:goto 清理模式
int process_data(void)
{
int ret = -1;
char *buf = malloc(1024);
if (!buf) goto cleanup;
FILE *fp = fopen("data.bin", "rb");
if (!fp) goto cleanup_buf;
// 正常处理...
ret = 0;
cleanup_file:
fclose(fp);
cleanup_buf:
free(buf);
cleanup:
return ret;
}
方案 2:GCC __attribute__((cleanup))
GCC 提供了一个扩展,可以在变量离开作用域时自动调用清理函数——非常接近 RAII:
static void auto_free(void *p) {
free(*(void **)p);
}
void example(void)
{
__attribute__((cleanup(auto_free))) char *buf = malloc(1024);
// buf 在离开作用域时自动 free
}
Linux 内核、systemd 等项目广泛使用这个特性。
完整实例:用 C 实现一个"动物"类层次
将前面的知识整合成一个完整的、可编译运行的例子。
头文件定义
// animal.h
#ifndef ANIMAL_H
#define ANIMAL_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ========== "基类" Animal ========== */
typedef struct Animal Animal;
// vtable
typedef struct {
void (*speak)(Animal *self);
void (*eat)(Animal *self, const char *food);
void (*print_info)(Animal *self);
void (*deinit)(Animal *self);
} AnimalOps;
struct Animal {
const AnimalOps *ops; // vptr
const char *name;
int age;
};
// "基类"公有方法
void animal_speak(Animal *self);
void animal_eat(Animal *self, const char *food);
void animal_print_info(Animal *self);
void animal_deinit(Animal *self);
/* ========== "子类" Dog ========== */
typedef struct {
Animal base; // 继承 Animal
int energy;
const char *breed;
} Dog;
void dog_init(Dog *self, const char *name, int age, const char *breed);
Dog *dog_create(const char *name, int age, const char *breed);
void dog_destroy(Dog *self);
/* ========== "子类" Cat ========== */
typedef struct {
Animal base; // 继承 Animal
int mood;
int indoor;
} Cat;
void cat_init(Cat *self, const char *name, int age, int indoor);
Cat *cat_create(const char *name, int age, int indoor);
void cat_destroy(Cat *self);
#endif // ANIMAL_H
实现文件
// animal.c
#include "animal.h"
/* ========== Animal "基类"方法 ========== */
void animal_speak(Animal *self)
{
if (self && self->ops && self->ops->speak)
self->ops->speak(self);
}
void animal_eat(Animal *self, const char *food)
{
if (self && self->ops && self->ops->eat)
self->ops->eat(self, food);
}
void animal_print_info(Animal *self)
{
if (self && self->ops && self->ops->print_info)
self->ops->print_info(self);
}
void animal_deinit(Animal *self)
{
if (self && self->ops && self->ops->deinit)
self->ops->deinit(self);
}
/* ========== Dog 实现 ========== */
static void dog_speak_impl(Animal *self)
{
Dog *dog = (Dog *)self;
printf("[%s] Woof! Woof!\n", dog->base.name);
dog->energy -= 5;
}
static void dog_eat_impl(Animal *self, const char *food)
{
Dog *dog = (Dog *)self;
dog->energy += 20;
printf("[%s] *happily eats %s* (energy: %d)\n",
dog->base.name, food, dog->energy);
}
static void dog_print_info_impl(Animal *self)
{
Dog *dog = (Dog *)self;
printf("Dog{name=%s, age=%d, breed=%s, energy=%d}\n",
dog->base.name, dog->base.age, dog->breed, dog->energy);
}
static void dog_deinit_impl(Animal *self)
{
// 清理 Dog 特有资源(此例中无)
}
// Dog 的 vtable
static const AnimalOps dog_ops = {
.speak = dog_speak_impl,
.eat = dog_eat_impl,
.print_info = dog_print_info_impl,
.deinit = dog_deinit_impl,
};
void dog_init(Dog *self, const char *name, int age, const char *breed)
{
self->base.ops = &dog_ops; // 设置 vptr
self->base.name = name;
self->base.age = age;
self->breed = breed;
self->energy = 100;
}
Dog *dog_create(const char *name, int age, const char *breed)
{
Dog *self = (Dog *)malloc(sizeof(Dog));
if (self) dog_init(self, name, age, breed);
return self;
}
void dog_destroy(Dog *self)
{
if (self) {
animal_deinit((Animal *)self);
free(self);
}
}
/* ========== Cat 实现 ========== */
static void cat_speak_impl(Animal *self)
{
Cat *cat = (Cat *)self;
if (cat->mood > 30)
printf("[%s] Purrrr~\n", cat->base.name);
else
printf("[%s] Hiss!\n", cat->base.name);
}
static void cat_eat_impl(Animal *self, const char *food)
{
Cat *cat = (Cat *)self;
if (strcmp(food, "fish") == 0) {
cat->mood += 20;
printf("[%s] *loves the fish!* (mood: %d)\n",
cat->base.name, cat->mood);
} else {
cat->mood += 5;
printf("[%s] *reluctantly eats %s* (mood: %d)\n",
cat->base.name, food, cat->mood);
}
}
static void cat_print_info_impl(Animal *self)
{
Cat *cat = (Cat *)self;
printf("Cat{name=%s, age=%d, mood=%d, indoor=%s}\n",
cat->base.name, cat->base.age, cat->mood,
cat->indoor ? "yes" : "no");
}
static void cat_deinit_impl(Animal *self)
{
// 清理 Cat 特有资源
}
static const AnimalOps cat_ops = {
.speak = cat_speak_impl,
.eat = cat_eat_impl,
.print_info = cat_print_info_impl,
.deinit = cat_deinit_impl,
};
void cat_init(Cat *self, const char *name, int age, int indoor)
{
self->base.ops = &cat_ops;
self->base.name = name;
self->base.age = age;
self->mood = 50;
self->indoor = indoor;
}
Cat *cat_create(const char *name, int age, int indoor)
{
Cat *self = (Cat *)malloc(sizeof(Cat));
if (self) cat_init(self, name, age, indoor);
return self;
}
void cat_destroy(Cat *self)
{
if (self) {
animal_deinit((Animal *)self);
free(self);
}
}
主程序:多态演示
// main.c
#include "animal.h"
void feed_all(Animal *animals[], int count, const char *food)
{
printf("--- Feeding time: %s ---\n", food);
for (int i = 0; i < count; i++) {
animal_eat(animals[i], food);
}
printf("\n");
}
int main(void)
{
// 创建不同类型的动物
Dog rex;
dog_init(&rex, "Rex", 3, "Labrador");
Cat *whiskers = cat_create("Whiskers", 5, 1);
Cat *shadow = cat_create("Shadow", 2, 0);
// 存入基类指针数组——向上转型
Animal *animals[] = {
(Animal *)&rex,
(Animal *)whiskers,
(Animal *)shadow,
};
int count = sizeof(animals) / sizeof(animals[0]);
// 多态:打印所有动物信息
printf("=== Animal Info ===\n");
for (int i = 0; i < count; i++) {
animal_print_info(animals[i]);
}
printf("\n");
// 多态:所有动物说话
printf("=== Roll Call ===\n");
for (int i = 0; i < count; i++) {
animal_speak(animals[i]);
}
printf("\n");
// 多态:喂食
feed_all(animals, count, "kibble");
feed_all(animals, count, "fish");
// 析构
cat_destroy(whiskers);
cat_destroy(shadow);
// rex 是栈上对象,不需要 free
return 0;
}
输出:
=== Animal Info ===
Dog{name=Rex, age=3, breed=Labrador, energy=100}
Cat{name=Whiskers, age=5, mood=50, indoor=yes}
Cat{name=Shadow, age=2, mood=50, indoor=no}
=== Roll Call ===
[Rex] Woof! Woof!
[Whiskers] Purrrr~
[Shadow] Purrrr~
--- Feeding time: kibble ---
[Rex] *happily eats kibble* (energy: 115)
[Whiskers] *reluctantly eats kibble* (mood: 55)
[Shadow] *reluctantly eats kibble* (mood: 55)
--- Feeding time: fish ---
[Rex] *happily eats fish* (energy: 130)
[Whiskers] *loves the fish!* (mood: 75)
[Shadow] *loves the fish!* (mood: 75)
feed_all 函数只知道 Animal *,完全不知道传进来的是 Dog 还是 Cat。但通过 animal->ops->eat(animal, food) 调用了正确的实现——这就是多态。
C++ 编译器到底做了什么
前面我们用 C 手动实现了 vtable。那 C++ 编译器自动生成的 vtable 到底长什么样?让我们一探究竟。
查看 vtable 布局
GCC 提供了 -fdump-lang-class 选项(旧版本用 -fdump-class-hierarchy)来输出类的内存布局和 vtable:
g++ -fdump-lang-class -c animal.cpp
输出(简化):
Vtable for Animal
Animal::_ZTV6Animal: 4 entries
0 (int (*)(...))0 # offset to top
8 (int (*)(...))(& _ZTI6Animal) # typeinfo
16 (int (*)(...))__cxa_pure_virtual # Animal::speak [pure]
24 (int (*)(...))__cxa_pure_virtual # Animal::eat [pure]
Vtable for Dog
Dog::_ZTV3Dog: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI3Dog)
16 (int (*)(...))Dog::speak # override
24 (int (*)(...))Dog::eat # override
Class Dog
size=24 align=8
base size=24 base align=8
Dog (0x...) 0
vptr=((& Dog::_ZTV3Dog) + 16) # vptr 指向 vtable 第 3 项
Animal (0x...) 0
primary-for Dog (0x...)
几个关键信息:
- vtable 前面有两个隐藏条目:
offset to top(多重继承时使用)和typeinfo(RTTI 运行时类型信息)。vptr实际指向 vtable 的第三项(跳过这两个) - Dog 的 vtable 在
speak和eat的位置放了 Dog 自己的实现 - Animal 的 vtable 在对应位置放了
__cxa_pure_virtual(纯虚函数占位符,调用会 abort)
用 sizeof 验证
#include <cstdio>
#include <cstddef>
class Base {
public:
int x;
};
class BaseVirtual {
public:
virtual void func() {}
int x;
};
int main() {
printf("sizeof(Base) = %zu\n", sizeof(Base)); // 4
printf("sizeof(BaseVirtual) = %zu\n", sizeof(BaseVirtual)); // 16
}
Base 只有一个 int,大小 4 字节。BaseVirtual 多了一个隐藏的 vptr(8 字节指针)+ 对齐填充,变成了 16 字节。
这就是 C++ 虚函数的唯一运行时开销——每个有虚函数的对象多一个指针大小的空间。调用虚函数比普通函数多一次内存间接寻址(取 vptr → 取函数地址 → 调用)。
对比手写 C 和 C++ 编译输出
// C 版本的 Animal
typedef struct {
const void **ops; // 我们手写的 vptr
const char *name;
int age;
} Animal_C;
// sizeof = 24(64 位系统:8+8+4+padding4)
// C++ 版本的 Animal
class Animal_CPP {
public:
virtual void speak() = 0;
virtual void eat(const char *) = 0;
const char *name;
int age;
};
// sizeof = 24(64 位系统:vptr8+name8+age4+padding4)
大小完全一致。我们手动写的 C 版本和 C++ 编译器自动生成的版本,在内存层面是等价的。
实际项目中的应用
"C 语言面向对象"不是学术游戏——它在大量生产级项目中被广泛使用。
Linux 内核:file_operations
Linux 的"一切皆文件"哲学,底层就是一个 vtable:
struct file_operations {
struct module *owner;
loff_t (*llseek)(struct file *, loff_t, int);
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
// ... 更多操作
};
不同的设备驱动(字符设备、块设备、网络设备)各自实现自己的 file_operations,注册到 VFS 中。用户态调用 read(),VFS 通过 f->f_op->read() 分发到具体驱动——这就是多态。
q_device 框架
在之前的文章中我们分析过的 q_device 框架,本质也是同样的模式:
struct q_device_ops {
int (*init)(q_device_t *dev);
int (*read)(q_device_t *dev, int pos, const void *buffer, int size);
int (*write)(q_device_t *dev, int pos, const void *buffer, int size);
int (*control)(q_device_t *dev, int cmd, void *args);
// ...
};
struct q_device {
const char *name;
const struct q_device_ops *dops; // vptr
void *argv;
struct q_device *next;
};
q_device_ops 就是 vtable,dops 就是 vptr。BSP 层的 I2C、SPI、UART 各自实现自己的 ops,上层通过 q_device_write 统一调用——不关心底层是什么总线。
FreeRTOS 的 task 和 queue
FreeRTOS 的内部数据结构也大量使用类似模式。StaticTask_t 嵌入了各种链表节点、状态标志,不同类型的任务通过函数指针(task function)实现不同的行为。Queue 的底层实现也用了类似的策略——同一套 API 根据 queue type 分发到不同的操作实现。
GLib 的 GObject
GNOME 桌面环境的基础库 GLib 实现了一套完整的 C 语言对象系统——GObject。它支持:
- 单继承和接口(interface)
- 属性系统(property)
- 信号系统(signal,类似 Qt 的 signal/slot)
- 引用计数内存管理
GObject 证明了即使是复杂的面向对象系统,纯 C 也完全可以实现。GTK 图形界面库就建立在 GObject 之上。
总结与系列预告
C 实现 OOP 的本质
| OOP 概念 | C++ 语法 | C 实现方式 | 底层本质 |
|---|---|---|---|
| 封装 | class { } | struct + 函数第一参数 self | struct 聚合数据,函数接收指针 |
| 访问控制 | private / public | static + 不透明指针 | 链接可见性 / 类型隐藏 |
| 继承 | class Dog : public Animal | 子类 struct 首成员为父类 struct | 内存布局嵌套 |
| 多态 | virtual 函数 | 函数指针表(ops struct) | vtable + vptr |
| 构造 | 构造函数 | xxx_init() / xxx_create() | 内存分配 + 成员初始化 + vptr 设置 |
| 析构 | 析构函数 | xxx_deinit() / xxx_destroy() | 资源释放 + 内存回收 |
优缺点
优点:
- 一切都是显式的,没有编译器"魔法",调试时所见即所得
- 编译后的代码体积和运行开销可以精确控制
- 不需要 C++ 运行时(异常处理、RTTI 等),适合资源受限环境
- 与 C 生态完全兼容,无 ABI 问题
缺点:
- 手动维护 vtable、init/deinit 调用,代码量更大
- 没有编译器帮你检查类型安全(指针强转没有编译期保护)
- 没有自动析构(RAII),资源泄漏风险更高
- 没有模板/泛型,代码复用靠
void *和宏
系列预告
本文揭示了 C++ 面向对象在底层的真实面貌。后续文章将继续深入 C++ 的其他特性:
- 第二篇:模板与泛型 —— C++ 模板在编译期做了什么?和 C 的
void */ 宏泛型有何区别? - 第三篇:异常与 RTTI ——
try/catch的零开销异常模型、dynamic_cast和typeid的底层实现 - 第四篇:STL 容器 ——
vector、map、string的内存布局和性能特征
理解了底层原理,你使用 C++ 时就不是在"记语法",而是在"做设计选择"——知道每个特性的代价,才能做出最优的权衡。

