9763 字
49 分钟
阅读量:

C语言实现面向对象编程(OOP)基础教程

🤖AI 摘要
AI

C语言实现面向对象编程(OOP)基础教程#

本教程面向已掌握C语言基础的读者,希望通过C语言自身机制来理解面向对象编程(OOP)的一些底层实现思路。 我们依次模拟封装、成员函数、this指针、信息隐藏、继承与多态等常见特性,纯用C逐步构建,不求面面俱到,但求每一步都清晰可运行。 全文共六章,每节配有完整的示例代码、分段讲解以及实践中容易踩的坑,希望能为你的OOP学习提供一个参考。

daitcl
/
oop
Waiting for api.github.com...
00K
0K
0K
Waiting...

一、封装成员变量#

抽象:从具体事物中提取共同特征(属性)和共同行为。
封装:将抽象出的数据(成员变量)组合成一个整体——结构体(struct)。

在C语言中,结构体支持数据封装。根据抽象的定义我们通过构建一个 Person 类型的结构体,其中包含年龄姓名

1.1 结构体定义与构造函数#

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 结构体:Person,表示人的基本信息 */
typedef struct Person {
int age_; /**< 年龄 */
char name_[50]; /**< 姓名(最多 49 个有效字符 + '\0') */
} Person;

通过 typedef struct Person {...} Person;,我们将年龄和姓名这两个属性封装成一个新的数据类型,完成了数据层面的封装

为了让使用者更方便、更安全地创建和初始化 Person 对象,我们为其提供专门的构造函数

/**
* @brief 在堆上创建 Person 对象(无参构造)
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) {
return NULL; /* 内存分配失败 */
}
/* 初始化为安全的默认值 */
self->age_ = 0;
self->name_[0] = '\0';
return self;
}
/**
* @brief 在堆上创建 Person 对象,并用指定参数初始化
* @param age 年龄
* @param name 姓名(C 字符串)
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new(); /* 复用无参构造 */
if (self == NULL) {
return NULL;
}
self->age_ = age;
/* 安全复制姓名,防止缓冲区溢出 */
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0'; /* 确保以 '\0' 结尾 */
return self;
}

注意:这两个函数的返回值类型都是 Person*,也就是说它们会返回新构造出来的对象的指针,供外部使用。这也是一种模拟“构造函数返回对象”的惯用写法。

创建对象时,我们可以选择两种不同的方式——栈上直接初始化堆上动态构造

int main(void) {
/* ---------- 方式 1:栈上对象 ---------- */
Person p1 = {18, "张三"}; /* 直接初始化 */
/* ---------- 方式 2:堆上对象 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
free(p2); /* 必须手动释放,防止内存泄漏 */
}
return 0;
}

选择不同的构造方式,决定了对象创建的位置(栈/堆),进而影响了其生命周期管理方式。按需选取即可。

  • 栈上对象 p1:随函数调用自动创建,作用域结束时自动销毁,无需手动干预,适合生命周期短、大小确定的对象。
  • 堆上对象 p2:通过 Person_newWith(内部调用 malloc)在堆上分配,生命周期可控,必须显式 free 释放,适合需要跨函数传递或大小动态变化的对象。

接下来,我们为 Person 添加两个操作数据的函数,以此演示如何通过指针来使用封装好的成员。

/**
* @brief 打印 Person 对象的信息
* @param self 指向 Person 对象的指针
*/
void Person_print(Person* self) {
if (self != NULL) {
printf("年龄:%d,姓名:%s\n", self->age_, self->name_);
}
}
/**
* @brief 让 Person 对象打招呼
* @param self 指向 Person 对象的指针
*/
void Person_greet(Person* self) {
if (self != NULL) {
printf("%s说:你好!\n", self->name_);
}
}
/**
* @brief 销毁堆上创建的 Person 对象(释放内存)
* @param self 指向待销毁对象的指针
*/
void Person_delete(Person* self) {
free(self);
}

在这些函数里,我们显式地传入 Person* self 指针来读取和操作 Person 中的数据。这种写法类似于 C++ 成员函数背后隐藏的 this 指针,也是 C 语言模拟面向对象行为的基础。

调用示例如下:

int main(void) {
/* ---------- 方式 1:栈上对象 ---------- */
Person p1 = {18, "张三"}; /* 直接初始化 */
Person_print(&p1); /* 传递栈对象的地址 */
/* ---------- 方式 2:堆上对象 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
Person_print(p2);
Person_delete(p2); /* 必须手动释放,防止内存泄漏 */
}
return 0;
}

运行结果

Terminal window
年龄:18,姓名:张三
年龄:20,姓名:李四
李四说:你好!

1.2 要点小结#

  • typedef struct 将属性打包,实现数据封装。
  • 函数统一接收 Person* self 指针操作对象(模拟 this)。
  • 堆对象必须 Person_delete,栈对象自动回收。
  • 始终用 strncpy + 手动置 '\0' 防止缓冲区溢出。

1.3 完整代码如下#

/**
* test1.c
* 最简单的 C 语言结构体用法,数据与函数分离。
* 演示栈对象和堆对象的创建、使用与销毁。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 结构体:Person,表示人的基本信息 */
typedef struct Person {
int age_; /**< 年龄 */
char name_[50]; /**< 姓名(最多 49 个有效字符 + '\0') */
} Person;
/**
* @brief 在堆上创建 Person 对象(无参构造)
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) {
return NULL; /* 内存分配失败 */
}
/* 初始化为安全的默认值 */
self->age_ = 0;
self->name_[0] = '\0';
return self;
}
/**
* @brief 在堆上创建 Person 对象,并用指定参数初始化
* @param age 年龄
* @param name 姓名(C 字符串)
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new(); /* 复用无参构造 */
if (self == NULL) {
return NULL;
}
self->age_ = age;
/* 安全复制姓名,防止缓冲区溢出 */
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0'; /* 确保以 '\0' 结尾 */
return self;
}
/**
* @brief 打印 Person 对象的信息
* @param self 指向 Person 对象的指针
*/
void Person_print(Person* self) {
if (self != NULL) {
printf("年龄:%d,姓名:%s\n", self->age_, self->name_);
}
}
/**
* @brief 让 Person 对象打招呼
* @param self 指向 Person 对象的指针
*/
void Person_greet(Person* self) {
if (self != NULL) {
printf("%s说:你好!\n", self->name_);
}
}
/**
* @brief 销毁堆上创建的 Person 对象(释放内存)
* @param self 指向待销毁对象的指针
*/
void Person_delete(Person* self) {
free(self);
}
int main(void) {
/* ---------- 方式 1:栈上对象 ---------- */
/* 这种写法(聚合初始化)要求初始化值的类型、顺序与结构体成员完全一致,且字符串字面量不能超过数组长度。 */
Person p1 = {18, "张三"}; /* 直接初始化 */
Person_print(&p1); /* 传递栈对象的地址 */
/* ---------- 方式 2:堆上对象 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
Person_print(p2);
Person_delete(p2); /* 必须手动释放,防止内存泄漏 */
}
return 0;
}

二、封装成员函数#

在C语言中,函数是全局的。为了让结构体实现“自带”行为,可以在结构体中存放函数指针,这些指针指向具体的操作函数。调用时通过 对象.函数指针(参数) 的语法,模拟C++的成员函数。

2.1 修改结构体,加入函数指针#

首先,我们在 Person 结构体中增加函数指针成员:

/**
* test2.c
* 在结构体中添加函数指针成员,模拟 C++ 的成员函数。
* 每个对象单独存储函数指针(内存开销较大),通过指针调用实现行为绑定。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 前向声明,便于结构体内使用自身类型指针 */
typedef struct Person Person;
/**
* 结构体:Person
* 包含数据成员(age_, name_)和函数指针成员(print_, sayHello_, destroy_)。
*/
struct Person {
int age_; /**< 年龄 */
char name_[50]; /**< 姓名 */
/* 函数指针成员(模拟成员函数) */
void (*print_)(Person* self); /**< 打印信息 */
void (*greet_)(Person* self); /**< 打招呼 */
void (*destroy_)(Person* self); /**< 销毁对象(仅用于堆对象) */
};

注意:因为这些函数指针的参数类型为 Person*,所以需要先用 typedef struct Person Person; 进行前向声明。

接下来,我们需要在构造函数里完成函数指针与具体函数的绑定,这样通过 对象.函数指针(参数) 调用时才知道执行哪个函数。

/**
* @brief 在堆上创建 Person 对象(无参构造)
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
/* 初始化数据成员 */
self->age_ = 0;
self->name_[0] = '\0';
/* 绑定函数指针(每个对象独立存储一份) */
self->print_ = Person_print_impl;
self->greet_ = Person_greet_impl;
self->destroy_ = Person_destroy_impl;
return self;
}
/**
* @brief 在堆上创建 Person 对象,并用指定参数初始化
* @param age 年龄
* @param name 姓名
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new();
if (self == NULL) return NULL;
self->age_ = age;
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0';
return self;
}

由此,我们就可以写出使用函数指针的 main 示例:

int main(void) {
/* ---------- 栈上对象:必须手动初始化函数指针 ---------- */
Person p1; /* 无法再用 {18, "张三"} 聚合初始化,因为结构体含函数指针 */
p1.age_ = 18;
strncpy(p1.name_, "张三", sizeof(p1.name_) - 1);
p1.name_[sizeof(p1.name_) - 1] = '\0';
/* 手动绑定函数指针(若未绑定直接调用会段错误) */
p1.print_ = Person_print_impl;
p1.greet_ = Person_greet_impl;
p1.destroy_ = NULL; /* 栈对象绝不可调用 destroy,设为 NULL 更安全 */
p1.print_(&p1); /* 通过函数指针调用,显式传入 &p1 */
/* ---------- 堆上对象:构造函数已自动绑定 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
p2->print_(p2);
p2->greet_(p2); /* 可以调用多个行为函数 */
p2->destroy_(p2); /* 释放堆内存 */
}
return 0;
}

运行结果

Terminal window
年龄:18,姓名:张三
年龄:20,姓名:李四
李四说:你好!

2.2 要点小结#

  • 函数指针成员void (*print_)(Person* self); 声明了一个指向函数的指针,该函数接受 Person* 且无返回值。

  • 绑定:在构造函数中将全局函数地址赋给指针成员,对象便“拥有”了行为。

  • 调用p->print_(p) 需要显式传入 p 自身,因为函数指针无法自动获取 this

  • 栈对象注意事项

    • 因结构体含函数指针,不便使用聚合初始化,需逐成员赋值。
    • 函数指针必须手动绑定,否则调用会崩溃。
    • destroy_ 应设为 NULL,严禁对栈对象执行 free

2.3 完整代码#

(此处放置完整的、修正后的 test2.c 代码,与上面展示的一致,包括函数实现的原型声明和 _impl 函数的定义。)

/**
* test2.c
* 在结构体中添加函数指针成员,模拟 C++ 的成员函数。
* 每个对象单独存储函数指针(内存开销较大),通过指针调用实现行为绑定。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 前向声明,便于结构体内使用自身类型指针 */
typedef struct Person Person;
/**
* 结构体:Person
* 包含数据成员(age_, name_)和函数指针成员(print_, greet_, destroy_)。
*/
struct Person {
int age_; /**< 年龄 */
char name_[50]; /**< 姓名 */
/* 函数指针成员(模拟成员函数) */
void (*print_)(Person* self); /**< 打印信息 */
void (*greet_)(Person* self); /**< 打招呼 */
void (*destroy_)(Person* self); /**< 销毁对象(仅用于堆对象) */
};
/* 函数实现的原型声明 */
void Person_print_impl(Person* self);
void Person_greet_impl(Person* self);
void Person_destroy_impl(Person* self);
/**
* @brief 在堆上创建 Person 对象(无参构造)
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
/* 初始化数据成员 */
self->age_ = 0;
self->name_[0] = '\0';
/* 绑定函数指针(每个对象独立存储一份) */
self->print_ = Person_print_impl;
self->greet_ = Person_greet_impl;
self->destroy_ = Person_destroy_impl;
return self;
}
/**
* @brief 在堆上创建 Person 对象,并用指定参数初始化
* @param age 年龄
* @param name 姓名
* @return 成功返回指向 Person 的指针,失败返回 NULL
*/
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new();
if (self == NULL) return NULL;
self->age_ = age;
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0';
return self;
}
/**
* @brief 打印 Person 对象信息(函数指针绑定的实现)
* @param self 指向当前对象的指针
*/
void Person_print_impl(Person* self) {
if (self != NULL) {
printf("年龄:%d,姓名:%s\n", self->age_, self->name_);
}
}
/**
* @brief 打招呼的实现
*/
void Person_greet_impl(Person* self) {
if (self != NULL) {
printf("%s说:你好!\n", self->name_);
}
}
/**
* @brief 销毁堆对象的实现
*/
void Person_destroy_impl(Person* self) {
free(self);
}
int main(void) {
/* ---------- 栈上对象:必须手动初始化函数指针 ---------- */
Person p1;
p1.age_ = 18;
strncpy(p1.name_, "张三", sizeof(p1.name_) - 1);
p1.name_[sizeof(p1.name_) - 1] = '\0';
/* 绑定函数指针(若未绑定直接调用会导致段错误) */
p1.print_ = Person_print_impl;
p1.greet_ = Person_greet_impl;
p1.destroy_ = Person_destroy_impl; /* 栈对象通常不应调用 destroy_ */
p1.print_(&p1); /* 通过函数指针调用,需显式传入对象自身 */
/* ---------- 堆上对象:构造函数已自动绑定 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
p2->print_(p2);
p2->destroy_(p2); /* 释放堆内存 */
}
return 0;
}

三、隐藏this指针(仅供学习)#

C++ 的成员函数可以隐式获取 this 指针,调用时无需显式传递对象自身。在 C 语言中,我们可以用一个全局变量来模拟这种机制:在调用成员函数前,先把当前对象的地址存入一个全局指针,函数内部直接访问该全局指针即可。

⚠️ 警告:这种方法非线程安全,且极易因忘记设置全局指针而出错。本节仅用于帮助理解 this 的工作原理,实际项目中切勿使用。工程上的安全做法会在第五章介绍。

3.1 引入全局 this 指针#

先把 Person 中的函数指针改为无参形式,因为它们将依靠全局指针来获取操作对象。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Person Person;
struct Person {
int age_;
char name_[50];
/* 函数指针成员,均为无参函数(内部通过全局 person_this 访问对象) */
void (*print_)(void);
void (*greet_)(void);
void (*destroy_)(void);
};
/* 全局 this 指针,指向当前正在操作的对象 */
Person* person_this = NULL;
/* 函数实现的原型声明 */
void Person_print_impl(void);
void Person_greet_impl(void);
void Person_destroy_impl(void);

上述代码将 print_greet_destroy_ 的参数列表置为 void,意味着调用时不再需要手动传入对象指针。对象本身通过全局变量 person_this 来定位,这就是对 this 指针的模拟。

此外,我们定义一个宏 PERSON_CAST,用来在调用前方便地设置 person_this,同时返回对象指针,以便支持链式调用风格。

/**
* 宏:PERSON_CAST
* 将全局 person_this 设为指定对象,并返回该对象指针。
* 用法:PERSON_CAST(p2)->print_();
*/
#define PERSON_CAST(object) (person_this = (object))

构造函数同样需要适配:在创建对象并绑定函数指针时,自动将 person_this 指向新对象。

/**
* @brief 堆上创建 Person 对象(无参构造)
*/
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
self->age_ = 0;
self->name_[0] = '\0';
/* 绑定无参函数指针 */
self->print_ = Person_print_impl;
self->greet_ = Person_greet_impl;
self->destroy_ = Person_destroy_impl;
person_this = self; /* 使新对象成为当前操作对象 */
return self;
}
/**
* @brief 堆上创建 Person 对象(带参构造)
*/
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new();
if (self == NULL) return NULL;
self->age_ = age;
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0';
return self;
}

函数实现内部直接使用 person_this 来访问成员,不再需要 self 参数:

void Person_print_impl(void) {
if (person_this != NULL) {
printf("年龄:%d,姓名:%s\n", person_this->age_, person_this->name_);
}
}
void Person_greet_impl(void) {
if (person_this != NULL) {
printf("%s说:你好!\n", person_this->name_);
}
}
void Person_destroy_impl(void) {
free(person_this);
/* 注意:这里未将 person_this 置为 NULL,外部需自行管理 */
}

调用示例如下:

int main(void) {
/* ---------- 栈上对象 ---------- */
Person p1;
p1.age_ = 18;
strncpy(p1.name_, "张三", sizeof(p1.name_) - 1);
p1.name_[sizeof(p1.name_) - 1] = '\0';
/* 手动绑定函数指针 */
p1.print_ = Person_print_impl;
p1.greet_ = Person_greet_impl;
p1.destroy_ = NULL; /* 栈对象绝不调用 destroy_ */
person_this = &p1; /* 设置当前操作对象 */
p1.print_(); /* 无需传参,直接调用 */
/* ---------- 堆上对象 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
PERSON_CAST(p2)->print_(); /* 通过宏设置 this 并调用 */
PERSON_CAST(p2)->greet_(); /* 另一个行为 */
PERSON_CAST(p2)->destroy_(); /* 释放堆内存 */
}
return 0;
}

运行结果

Terminal window
年龄:18,姓名:张三
年龄:20,姓名:李四
李四说:你好!

3.2 要点小结#

  • 全局 person_this:扮演了 this 指针的角色,函数内部通过它找到当前对象。
  • 无参函数指针void (*print_)(void) 等不再需要显式传入 Person*,调用更接近 C++ 的语法。
  • PERSON_CAST:一行代码完成“设置当前对象 + 返回对象指针”,模拟 obj->method() 的连贯写法。
  • 栈对象:仍需手动绑定函数指针,且使用前必须将 person_this 指向该对象。切勿调用 destroy_
  • 严重局限:任何时刻只有一个 person_this,嵌套调用或多线程下会完全混乱,详见下一节。

3.3 缺陷一览#

缺陷描述
非线程安全多个线程同时修改全局 person_this,会导致数据错乱或崩溃
不支持重入在一个成员函数内部调用另一个对象的成员函数时,person_this 会被覆盖
极易忘记设置栈对象若忘记写 person_this = &p1;,调用行为函数会操作空指针或错误对象
销毁后悬空destroy_ 释放内存后 person_this 未置 NULL,后续误用造成悬垂指针

请牢记:本方案仅用于教学演示,帮助理解 C++ 中 this 的幕后机制。实际开发中请直接跳至第5章的工程化实现。

3.4 完整代码#

/**
* test3.c
* 引入全局指针 person_this 模拟 C++ 的 this 指针,使成员函数无需显式传参。
* 仅供理解原理,非线程安全,切勿在工程中使用。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Person Person;
struct Person {
int age_;
char name_[50];
/* 函数指针成员,均为无参函数(依赖全局 person_this) */
void (*print_)(void);
void (*greet_)(void);
void (*destroy_)(void);
};
/* 全局 this 指针,指向当前操作的对象 */
Person* person_this = NULL;
/**
* 宏:将全局 person_this 设置为指定对象,并返回该对象指针,支持链式调用。
*/
#define PERSON_CAST(object) (person_this = (object))
/* 函数原型 */
void Person_print_impl(void);
void Person_greet_impl(void);
void Person_destroy_impl(void);
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
self->age_ = 0;
self->name_[0] = '\0';
self->print_ = Person_print_impl;
self->greet_ = Person_greet_impl;
self->destroy_ = Person_destroy_impl;
person_this = self;
return self;
}
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new();
if (self == NULL) return NULL;
self->age_ = age;
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0';
return self;
}
void Person_print_impl(void) {
if (person_this != NULL) {
printf("年龄:%d,姓名:%s\n", person_this->age_, person_this->name_);
}
}
void Person_greet_impl(void) {
if (person_this != NULL) {
printf("%s说:你好!\n", person_this->name_);
}
}
void Person_destroy_impl(void) {
free(person_this);
}
int main(void) {
/* 栈上对象 */
Person p1;
p1.age_ = 18;
strncpy(p1.name_, "张三", sizeof(p1.name_) - 1);
p1.name_[sizeof(p1.name_) - 1] = '\0';
p1.print_ = Person_print_impl;
p1.greet_ = Person_greet_impl;
p1.destroy_ = NULL; /* 栈对象严禁释放 */
person_this = &p1;
p1.print_();
/* 堆上对象 */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
PERSON_CAST(p2)->print_();
PERSON_CAST(p2)->greet_();
PERSON_CAST(p2)->destroy_(); /* 释放堆内存 */
}
return 0;
}

四、隐藏成员变量#

C语言的结构体所有成员默认都是公有的。若想实现私有成员(外部代码无法直接访问),可使用不透明指针技术:在公开接口中只暴露一个 void* 指针,真正的数据定义在源文件内部,外部完全看不到细节。

4.1 使用私有数据结构#

将真正的年龄、姓名放入一个独立的 PersonPrivate 结构体,对外只暴露 void* private_ 指针。同时,函数指针依然使用全局 person_this,不传 self

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 前向声明 */
typedef struct Person Person;
/* 私有数据结构体(定义在 .c 文件内部,此处为演示写在同一文件) */
typedef struct PersonPrivate {
int age_;
char name_[50];
} PersonPrivate;
/* 公有接口结构体 */
struct Person {
void* private_; /* 指向私有数据的指针(不透明) */
/* 无参函数指针,依赖全局 person_this */
void (*print_)(void);
void (*greet_)(void);
void (*destroy_)(void);
void (*setAge_)(int age);
int (*getAge_)(void);
const char* (*getName_)(void);
void (*setName_)(const char* name);
};
/* 全局 this 指针(线程不安全,仅演示用) */
Person* person_this = NULL;
/* 设置当前对象并支持链式调用的宏 */
#define PERSON_CAST(object) (person_this = (object))
/* 函数原型 */
void Person_print_impl(void);
void Person_greet_impl(void);
void Person_destroy_impl(void);
void Person_setAge_impl(int age);
int Person_getAge_impl(void);
const char* Person_getName_impl(void);
void Person_setName_impl(const char* name);
Person* Person_new(void);
Person* Person_newWith(int age, const char* name);
/* ---- 私有数据的分配与释放 ---- */
static PersonPrivate* PersonPrivate_new(void) {
PersonPrivate* priv = (PersonPrivate*)malloc(sizeof(PersonPrivate));
if (priv != NULL) {
priv->age_ = 0;
priv->name_[0] = '\0';
}
return priv;
}
static void PersonPrivate_delete(PersonPrivate* priv) {
free(priv);
}
/* ---- 构造函数 ---- */
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
self->private_ = PersonPrivate_new();
if (self->private_ == NULL) {
free(self);
return NULL;
}
self->print_ = Person_print_impl;
self->greet_ = Person_greet_impl;
self->destroy_ = Person_destroy_impl;
self->setAge_ = Person_setAge_impl;
self->getAge_ = Person_getAge_impl;
self->getName_ = Person_getName_impl;
self->setName_ = Person_setName_impl;
person_this = self; /* 自动设为当前对象 */
return self;
}
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new();
if (self == NULL) return NULL;
PersonPrivate* priv = (PersonPrivate*)self->private_;
priv->age_ = age;
strncpy(priv->name_, name, sizeof(priv->name_) - 1);
priv->name_[sizeof(priv->name_) - 1] = '\0';
return self;
}
/* ---- 行为函数实现(通过全局 person_this 访问对象和私有数据) ---- */
void Person_print_impl(void) {
if (person_this == NULL) return;
PersonPrivate* priv = (PersonPrivate*)person_this->private_;
if (priv != NULL) {
printf("年龄:%d,姓名:%s\n", priv->age_, priv->name_);
}
}
void Person_greet_impl(void) {
if (person_this == NULL) return;
PersonPrivate* priv = (PersonPrivate*)person_this->private_;
if (priv != NULL) {
printf("%s说:你好!\n", priv->name_);
}
}
void Person_destroy_impl(void) {
if (person_this == NULL) return;
/* 先释放私有数据,再释放外壳 */
if (person_this->private_ != NULL) {
PersonPrivate_delete((PersonPrivate*)person_this->private_);
person_this->private_ = NULL;
}
free(person_this);
/* 注意:未将 person_this 置 NULL,外部调用后容易误用 */
}
void Person_setAge_impl(int age) {
if (person_this == NULL) return;
PersonPrivate* priv = (PersonPrivate*)person_this->private_;
if (priv != NULL) priv->age_ = age;
}
int Person_getAge_impl(void) {
if (person_this == NULL) return -1;
PersonPrivate* priv = (PersonPrivate*)person_this->private_;
return priv ? priv->age_ : -1;
}
const char* Person_getName_impl(void) {
if (person_this == NULL) return NULL;
PersonPrivate* priv = (PersonPrivate*)person_this->private_;
return priv ? priv->name_ : NULL;
}
void Person_setName_impl(const char* name) {
if (person_this == NULL || name == NULL) return;
PersonPrivate* priv = (PersonPrivate*)person_this->private_;
if (priv != NULL) {
strncpy(priv->name_, name, sizeof(priv->name_) - 1);
priv->name_[sizeof(priv->name_) - 1] = '\0';
}
}
/* ---- 使用示例 ---- */
int main(void) {
/* ---------- 栈上对象:手动设置私有数据和函数指针 ---------- */
Person p1;
p1.private_ = PersonPrivate_new();
if (p1.private_ == NULL) return 1;
p1.print_ = Person_print_impl;
p1.greet_ = Person_greet_impl;
p1.destroy_ = NULL; /* 栈对象绝不能调用 destroy_ */
p1.setAge_ = Person_setAge_impl;
p1.getAge_ = Person_getAge_impl;
p1.getName_ = Person_getName_impl;
p1.setName_ = Person_setName_impl;
person_this = &p1;
p1.setName_("张三");
p1.setAge_(18);
p1.print_();
/* 释放私有数据(栈对象自身内存不释放) */
PersonPrivate_delete((PersonPrivate*)p1.private_);
p1.private_ = NULL;
/* ---------- 堆上对象:构造函数自动绑定 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
PERSON_CAST(p2)->print_();
PERSON_CAST(p2)->destroy_(); /* destroy_ 会释放私有数据和外壳 */
}
return 0;
}

运行结果

Terminal window
年龄:18,姓名:张三
年龄:20,姓名:李四

4.2 要点小结#

  • 不透明指针 void\* private_:对外隐藏了 PersonPrivate 的成员细节,外部代码无法直接访问年龄或姓名。
  • 私有数据分配:通过 PersonPrivate_new() 在堆上分配,Person 只是保存其地址。栈对象需手动调用分配与释放。
  • 全局 person_this 依然存在:所有无参函数仍依赖它来定位当前对象,带来了第三章的全部局限性。
  • 接口完整:除打印和打招呼外,增加了 setAge_getAge_getName_setName_ 等访问器,模拟了完整的成员函数集。

4.3 注意事项#

缺陷描述
全局 this 不安全非线程安全,且嵌套调用时 person_this 会被覆盖,与第三章相同
栈对象初始化繁琐需要手动分配 private_、逐个绑定函数指针,极易遗漏
destroy 后悬空Person_destroy_impl 未将 person_thisNULL,可能导致悬垂指针
栈对象不宜 destroy栈对象外壳在栈上,free 会导致崩溃,必须将 destroy_ 设为 NULL

本节展示了信息隐藏的基本手段。实际项目中,建议直接阅读第五章,它去掉了全局 this,提供了栈/堆统一的安全初始化方式。

4.4 完整代码#

(上述代码合并即为完整可运行程序,不再单独赘列。注意编译时需包含 stdio.hstdlib.hstring.h。)

五、多文件与显式 this 参数(推荐)#

综合前几章的探索,最安全、可维护、可工程化的方式是:显式传递 self 指针(彻底避免全局变量),结合多文件组织,并支持栈对象和堆对象的统一初始化

  • 数据成员通过不透明指针 PersonPrivate* 隐藏,保护内部细节。
  • 所有函数显式接收 Person* self,完全可重入,线程安全。
  • 栈对象使用 Person_init / Person_cleanup,堆对象使用 Person_newWith / destroy

5.1 头文件 person.h#

/**
* person.h
* Person 类的公有接口声明。
* 采用私有数据封装,支持栈对象初始化与清理。
*/
#ifndef PERSON_H
#define PERSON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 前向声明私有数据结构 */
typedef struct PersonPrivate PersonPrivate;
/**
* 公有接口结构体 Person
* 外部只看到一个不透明指针和一组函数指针。
*/
typedef struct Person {
PersonPrivate* private_; /* 私有数据(不透明) */
/* 行为函数指针,每个都显式接收 Person* self */
void (*print)(struct Person* self);
void (*greet)(struct Person* self);
void (*destroy)(struct Person* self);
void (*setAge)(struct Person* self, int age);
int (*getAge)(struct Person* self);
const char* (*getName)(struct Person* self);
void (*setName)(struct Person* self, const char* name);
} Person;
/* 堆对象构造函数 */
Person* Person_new(void);
Person* Person_newWith(int age, const char* name);
/* 栈对象初始化与清理 */
void Person_init(Person* self);
void Person_cleanup(Person* self);
#endif /* PERSON_H */

5.2 实现文件 person.c#

/**
* person.c
* Person 类的实现。
* 私有数据结构体定义在此,对外不可见。
* 所有成员函数实现均以 static 限定,仅通过公有接口暴露。
*/
#include "person.h"
/* ==================== 私有数据结构体完整定义 ==================== */
struct PersonPrivate {
int age_;
char name_[50];
};
/* ==================== 静态成员函数(内部可见) ==================== */
static void Person_print_impl(Person* self) {
if (self == NULL || self->private_ == NULL) return;
PersonPrivate* priv = self->private_;
printf("年龄:%d,姓名:%s\n", priv->age_, priv->name_);
}
static void Person_greet_impl(Person* self) {
if (self == NULL || self->private_ == NULL) return;
PersonPrivate* priv = self->private_;
printf("%s说:你好!\n", priv->name_);
}
static void Person_destroy_impl(Person* self) {
if (self == NULL) return;
/* 释放私有数据 */
if (self->private_ != NULL) {
free(self->private_);
self->private_ = NULL;
}
free(self); /* 注意:堆对象会被释放,栈对象不应调用此函数 */
}
static void Person_setAge_impl(Person* self, int age) {
if (self == NULL || self->private_ == NULL) return;
self->private_->age_ = age;
}
static int Person_getAge_impl(Person* self) {
if (self == NULL || self->private_ == NULL) return -1;
return self->private_->age_;
}
static const char* Person_getName_impl(Person* self) {
if (self == NULL || self->private_ == NULL) return NULL;
return self->private_->name_;
}
static void Person_setName_impl(Person* self, const char* name) {
if (self == NULL || self->private_ == NULL || name == NULL) return;
strncpy(self->private_->name_, name, sizeof(self->private_->name_) - 1);
self->private_->name_[sizeof(self->private_->name_) - 1] = '\0';
}
/* ==================== 私有数据分配与释放 ==================== */
static PersonPrivate* PersonPrivate_new(void) {
PersonPrivate* priv = (PersonPrivate*)malloc(sizeof(PersonPrivate));
if (priv != NULL) {
priv->age_ = 0;
priv->name_[0] = '\0';
}
return priv;
}
/* ==================== 构造函数(堆对象) ==================== */
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
self->private_ = PersonPrivate_new();
if (self->private_ == NULL) {
free(self);
return NULL;
}
self->print = Person_print_impl;
self->greet = Person_greet_impl;
self->destroy = Person_destroy_impl;
self->setAge = Person_setAge_impl;
self->getAge = Person_getAge_impl;
self->getName = Person_getName_impl;
self->setName = Person_setName_impl;
return self;
}
Person* Person_newWith(int age, const char* name) {
Person* self = Person_new();
if (self == NULL) return NULL;
self->private_->age_ = age;
strncpy(self->private_->name_, name, sizeof(self->private_->name_) - 1);
self->private_->name_[sizeof(self->private_->name_) - 1] = '\0';
return self;
}
/* ==================== 栈对象初始化与清理 ==================== */
void Person_init(Person* self) {
if (self == NULL) return;
self->private_ = PersonPrivate_new();
if (self->private_ == NULL) {
fprintf(stderr, "栈对象私有内存分配失败\n");
return;
}
self->print = Person_print_impl;
self->greet = Person_greet_impl;
self->destroy = NULL; /* 栈对象绝对不能 free 外壳 */
self->setAge = Person_setAge_impl;
self->getAge = Person_getAge_impl;
self->getName = Person_getName_impl;
self->setName = Person_setName_impl;
}
void Person_cleanup(Person* self) {
if (self == NULL) return;
if (self->private_ != NULL) {
free(self->private_);
self->private_ = NULL;
}
/* 不释放 self,它在栈上 */
}

5.3 测试文件 test5.c#

/**
* test5.c
* 演示栈对象与堆对象的统一使用方式。
*/
#include "person.h"
int main(void) {
/* ---------- 栈上对象 ---------- */
Person p1;
Person_init(&p1);
p1.setName(&p1, "张三");
p1.setAge(&p1, 18);
p1.print(&p1); /* 年龄:18,姓名:张三 */
p1.greet(&p1); /* 张三说:你好! */
Person_cleanup(&p1);
/* ---------- 堆上对象 ---------- */
Person* p2 = Person_newWith(20, "李四");
if (p2 != NULL) {
p2->print(p2); /* 年龄:20,姓名:李四 */
p2->destroy(p2); /* 释放私有数据 + 外壳 */
}
return 0;
}

运行结果

Terminal window
年龄:18,姓名:张三
张三说:你好!
年龄:20,姓名:李四

5.4 要点小结#

  • 显式 self 指针:所有函数都接收 Person* self 作为第一个参数,无需全局状态,天然线程安全。

  • 不透明私有数据PersonPrivate 定义在 .c 文件中,外部代码只能通过 setAgegetName 等接口访问,无法直接触碰成员。

  • 栈/堆统一接口

    • 栈对象:声明 Person p1;Person_init(&p1) → 使用 → Person_cleanup(&p1)
    • 堆对象:Person* p = Person_newWith(...) → 使用 → p->destroy(p)
  • 防御性编程:每个函数开头均检查 selfprivate_ 是否为 NULL,避免崩溃。

  • 栈对象 destroy 设为 NULL:在 Person_init 中明确将其置空,防止误调导致对栈内存执行 free

5.5 注意事项#

问题说明
栈对象必须配对必须有 Person_initPerson_cleanup,否则内存泄漏或重复释放
栈对象禁止调用 destroydestroy 内部会 free(self),栈对象调用即崩溃
堆对象销毁后指针悬空p2->destroy(p2)p2 成为悬垂指针,应避免再次使用
多文件编译对外只提供 person.h,私有结构体修改不影响用户代码

5.6 完整项目文件#

  • person.h —— 公有接口(如上)
  • person.c —— 实现(如上)
  • test5.c —— 测试(如上)

编译命令(gcc):

Terminal window
gcc -std=c99 -Wall -Wextra -o test5 test5.c person.c

六、继承与多态#

继承:让新类型(派生类)复用已有类型(基类)的属性和行为。 多态:通过基类接口调用函数时,实际执行的是派生类的版本,即“同一接口,不同表现”。

C 语言没有原生的继承和多态语法,但可以通过结构体嵌套(将基类作为派生类的第一个成员)和函数指针(或虚函数表)来模拟。

6.1 简单继承——每个对象独立函数指针#

最直观的方式是基类中声明函数指针成员,派生类通过包含基类实例“继承”这些指针。构造函数中,派生类可以重写某些函数指针,也可以复用基类的实现。

/**
* test6_inheritance_simple.c
* 每个对象独立存储函数指针,直观但内存开销较大。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------------------------- 基类 Person ---------------------------- */
typedef struct Person {
char name_[50];
int age_;
/* 函数指针成员,显式传递 self(使用 void* 以兼容派生类) */
void (*print_)(void* self);
void (*greet_)(void* self);
} Person;
/* 基类构造函数 */
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
self->age_ = 0;
self->name_[0] = '\0';
/* 函数指针在构造函数中不绑定,由派生类或专用初始化函数处理 */
self->print_ = NULL;
self->greet_ = NULL;
return self;
}
/* 基类行为实现 */
void Person_print_impl(void* self) {
Person* p = (Person*)self;
printf("[Person] 姓名:%s,年龄:%d\n", p->name_, p->age_);
}
void Person_greet_impl(void* self) {
Person* p = (Person*)self;
printf("[Person] 你好,我是%s\n", p->name_);
}
/* 便捷的初始化函数:为基类对象绑定默认行为 */
void Person_init_default(Person* self) {
if (self == NULL) return;
self->print_ = Person_print_impl;
self->greet_ = Person_greet_impl;
}
/* ---------------------------- 派生类 Student ---------------------------- */
/* 结构体嵌套:基类对象作为第一个成员,保证内存兼容 */
typedef struct Student {
Person base_; /* 继承的属性和函数指针 */
char school_[50];
} Student;
/* 派生类构造函数:分配内存并初始化 */
Student* Student_new(void) {
Student* self = (Student*)malloc(sizeof(Student));
if (self == NULL) return NULL;
self->base_.age_ = 0;
self->base_.name_[0] = '\0';
self->school_[0] = '\0';
/* 重写 print_,继承 greet_(绑定基类实现) */
self->base_.print_ = Student_print_impl;
self->base_.greet_ = Person_greet_impl; /* 行为继承 */
return self;
}
void Student_print_impl(void* self) {
Student* s = (Student*)self;
printf("[Student] 姓名:%s,年龄:%d,学校:%s\n",
s->base_.name_, s->base_.age_, s->school_);
}

关键设计Student 的第一个成员是 Person base_。这样,指向 Student 的指针可以安全地转换为 Person*,且访问 base_ 的成员与直接访问 Person 内存完全一致。

使用示例

int main(void) {
/* 创建基类对象并手动绑定函数指针 */
Person* person = Person_new();
Person_init_default(person); /* 绑定默认行为 */
strncpy(person->name_, "Alice", sizeof(person->name_) - 1);
person->name_[sizeof(person->name_) - 1] = '\0';
person->age_ = 30;
person->print_(person);
person->greet_(person);
free(person);
/* 创建派生类对象(构造函数已绑定函数指针) */
Student* student = Student_new();
strncpy(student->base_.name_, "Bob", sizeof(student->base_.name_) - 1);
student->base_.name_[sizeof(student->base_.name_) - 1] = '\0';
student->base_.age_ = 20;
strncpy(student->school_, "XYZ University", sizeof(student->school_) - 1);
student->school_[sizeof(student->school_) - 1] = '\0';
student->base_.print_(student); /* 调用 Student 自己的 print_ */
student->base_.greet_(student); /* 调用继承自 Person 的 greet_ */
free(student);
return 0;
}

运行结果

Terminal window
[Person] 姓名:Alice,年龄:30
[Person] 你好,我是Alice!
[Student] 姓名:Bob,年龄:20,学校:XYZ University
[Person] 你好,我是Bob!

6.1.1 要点小结#

  • 结构体嵌套:派生类把基类作为第一个成员,实现“is‑a”关系。
  • 函数指针重写:派生类在构造函数中将某些函数指针指向自己的实现,其余保留基类实现,即可完成行为继承
  • 内存布局:因为基类在派生类的最前面,(Person*)student 是安全的向上转型。
  • 每个对象独立指针:虽然简单直观,但每个对象都保存一份函数指针(此处 2 个指针 16 字节),对象数量多时内存开销较大。

6.1.2 注意事项#

陷阱后果避免方法
忘记绑定函数指针调用时跳转到 NULL,程序崩溃构造函数中统一绑定,或提供类似 Person_init_default 的初始化函数
错误使用 strcpy名称过长可能溢出 name_[50]统一使用 strncpy + 末尾置 '\0'
向上转型时误解内存若基类不是第一个成员,强制转换后布局错乱始终将基类放在结构体最前面

6.2 虚函数表(vtable)——多态的高效实现#

独立函数指针会为每个对象重复存储相同的函数地址。更好的方式是将这些指针集中放在一个静态常量表里,每个对象只保存一个指向该表的指针。这正是 C++ 虚函数的底层机制。

/**
* test7_vtable.c
* 使用虚函数表(vtable)实现继承与多态。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 前向声明 */
typedef struct Person Person;
typedef struct Student Student;
/* 虚函数表结构体,使用 const 保护 */
typedef struct VTable {
void (*print_)(void* self);
void (*greet_)(void* self);
void (*destroy_)(void* self);
} VTable;
/* 基类 Person */
struct Person {
const VTable* vtable_; /* 指向虚表的指针(不可修改) */
char name_[50];
int age_;
};
/* 派生类 Student */
struct Student {
Person base_; /* 继承 */
char school_[50];
};
/* --------------- 函数实现声明 --------------- */
void Person_print_impl(void* self);
void Person_greet_impl(void* self);
void Person_destroy_impl(void* self);
void Student_print_impl(void* self);
void Student_greet_impl(void* self);
void Student_destroy_impl(void* self);
/* 静态虚表(每个类一个),使用指定初始化器,强健且易读 */
static const VTable Person_vtable = {
.print_ = Person_print_impl,
.greet_ = Person_greet_impl,
.destroy_ = Person_destroy_impl
};
static const VTable Student_vtable = {
.print_ = Student_print_impl,
.greet_ = Student_greet_impl,
.destroy_ = Student_destroy_impl
};
/* --------------- 基类构造函数 --------------- */
Person* Person_new(void) {
Person* self = (Person*)malloc(sizeof(Person));
if (self == NULL) return NULL;
self->vtable_ = &Person_vtable; /* 绑定虚表 */
self->age_ = 0;
self->name_[0] = '\0';
return self;
}
Person* Person_newWith(const char* name, int age) {
Person* self = Person_new();
if (self == NULL) return NULL;
self->age_ = age;
strncpy(self->name_, name, sizeof(self->name_) - 1);
self->name_[sizeof(self->name_) - 1] = '\0';
return self;
}
/* --------------- 派生类构造函数 --------------- */
Student* Student_new(void) {
Student* self = (Student*)malloc(sizeof(Student));
if (self == NULL) return NULL;
self->base_.vtable_ = &Student_vtable; /* 绑定派生类虚表 */
self->base_.age_ = 0;
self->base_.name_[0] = '\0';
self->school_[0] = '\0';
return self;
}
Student* Student_newWith(const char* name, int age, const char* school) {
Student* self = Student_new();
if (self == NULL) return NULL;
self->base_.age_ = age;
strncpy(self->base_.name_, name, sizeof(self->base_.name_) - 1);
self->base_.name_[sizeof(self->base_.name_) - 1] = '\0';
strncpy(self->school_, school, sizeof(self->school_) - 1);
self->school_[sizeof(self->school_) - 1] = '\0';
return self;
}
/* --------------- 函数实现 --------------- */
void Person_print_impl(void* self) {
Person* p = (Person*)self;
printf("[Person] 姓名:%s,年龄:%d\n", p->name_, p->age_);
}
void Person_greet_impl(void* self) {
Person* p = (Person*)self;
printf("[Person] 你好,我是%s\n", p->name_);
}
void Person_destroy_impl(void* self) {
free(self);
}
void Student_print_impl(void* self) {
Student* s = (Student*)self;
printf("[Student] 姓名:%s,年龄:%d,学校:%s\n",
s->base_.name_, s->base_.age_, s->school_);
}
void Student_greet_impl(void* self) {
Student* s = (Student*)self;
printf("[Student] 你好,我是%s,来自%s\n",
s->base_.name_, s->school_);
}
void Student_destroy_impl(void* self) {
free(self);
}

使用示例(多态调用)

int main(void) {
Person* p = Person_newWith("Alice", 30);
Student* s = Student_newWith("Bob", 20, "XYZ University");
/* 通过基类指针调用,实现多态 */
Person* ps = (Person*)s; /* 向上转型 */
p->vtable_->print_(p); /* Person 版本 */
ps->vtable_->print_(ps); /* 多态:实际调用 Student 的 print_ */
ps->vtable_->greet_(ps); /* 多态:实际调用 Student 的 greet_ */
/* 销毁(虚表中有统一的 destroy_) */
p->vtable_->destroy_(p);
ps->vtable_->destroy_(ps); /* 正确释放 Student 内存 */
return 0;
}

运行结果

Terminal window
[Person] 姓名:Alice,年龄:30
[Student] 姓名:Bob,年龄:20,学校:XYZ University
[Student] 你好,我是Bob,来自XYZ University!

6.2.1 要点小结#

  • 虚表 VTable:将函数指针集中存储在一个静态 const 结构体中,每个类一个实例。
  • 对象存储 vtable_ 指针:所有对象共享同一个虚表,内存节省(每个对象仅增加一个指针)。
  • 多态调用:通过 obj->vtable_->print_(obj) 这种“二次跳转”实现晚绑定。
  • 构造时绑定:在 Person_new / Student_new 中将 vtable_ 指向对应虚表,确保对象创建完毕即可使用。
  • const 保护:虚表声明为 static const,防止运行时被意外篡改。

6.2.2 注意事项#

陷阱后果避免方法
虚表未正确初始化vtable_NULL 或野指针构造函数中必须立即绑定虚表
向上转型后误用 free若派生类大小与基类不同,导致内存错误通过虚表内的 destroy_ 释放,保证使用正确的 free
忘记给派生类定义独立的虚表调用的仍是基类函数,丢失多态每个派生类都定义自己的 static const VTable
字符串处理不安全任意一处 strcpy 都可能溢出全项目统一使用 strncpy + 末尾置 '\0'

6.3 多文件#

在真实项目中,我们通常将每个类的声明放在独立的头文件,实现放在对应的源文件,并利用 const 保护虚表。编译时只需链接目标文件,修改私有实现不会影响用户代码。

文件结构示例:

Terminal window
project/
├── person.h
├── person.c
├── student.h
├── student.c
└── main.c

person.h(公有接口,遵循最小包含原则)

#ifndef PERSON_H
#define PERSON_H
#ifdef __cplusplus
extern "C" {
#endif
/* 前向声明,隐藏实现细节 */
typedef struct Person Person;
typedef struct VTable VTable;
struct VTable {
void (*print_)(void* self);
void (*greet_)(void* self);
void (*destroy_)(void* self);
};
struct Person {
const VTable* vtable_;
char name_[50];
int age_;
};
/* 构造函数与公有函数 */
Person* Person_new(void);
Person* Person_newWith(const char* name, int age);
void Person_destroy(Person* self); /* 便捷封装,直接调用虚表销毁 */
#ifdef __cplusplus
}
#endif
#endif /* PERSON_H */

student.h

#ifndef STUDENT_H
#define STUDENT_H
#include "person.h"
typedef struct Student {
Person base_;
char school_[50];
} Student;
Student* Student_new(void);
Student* Student_newWith(const char* name, int age, const char* school);
/* 析构函数可以直接使用 Person_destroy((Person*)student) */
#endif /* STUDENT_H */

person.cstudent.c 的实现与 6.2 节类似,注意将虚表及实现的函数标记为 static,只通过公有接口暴露。

main.c 示例:

#include "person.h"
#include "student.h"
int main(void) {
Person* p = Person_newWith("Alice", 30);
Student* s = Student_newWith("Bob", 20, "XYZ University");
p->vtable_->print_(p); /* Person 版本 */
((Person*)s)->vtable_->print_(s); /* 多态:Student 版本 */
Person_destroy(p);
Person_destroy((Person*)s); /* 正确释放 Student 内存 */
return 0;
}

编译命令(严格模式):

Terminal window
gcc -std=c99 -Wall -Wextra -pedantic -o program main.c person.c student.c

6.4 本章总结#

本章展示了 C 语言模拟继承与多态的两种方式,完成了从基础封装到面向对象编程的完整演进:

  • 简单继承(独立函数指针) 每个对象持有自己的函数指针,直观易懂。派生类通过替换部分指针实现重写,保留其余指针复用基类行为。代价是每个对象多占用函数指针的存储空间。
  • 高效多态(虚函数表) 将函数指针集中存放在静态 const 虚表中,对象只保留一个指向虚表的指针。所有同类对象共享同一虚表,大幅节省内存。调用时通过 obj->vtable_->print_(obj) 实现运行时多态。

关键实践(贯穿全系列):

  1. 基类放在派生类第一个成员——保证内存布局兼容,向上转型安全。
  2. 构造函数中立即绑定虚表或函数指针——对象创建完毕即可用,杜绝野指针。
  3. 虚表用 static const 保护——防止意外修改,语义更清晰。
  4. 始终使用 strncpy + 强制尾 '\0'——杜绝字符串溢出。
  5. 防御式编程——每个行为函数先检查指针有效性。
C语言实现面向对象编程(OOP)基础教程
https://www.daitcc.top/posts/0134f848/
作者
Dait
发布于
2026-04-20
许可协议
CC BY-NC-SA 4.0
如果这篇文章对你有帮助或启发,可以请作者喝杯咖啡 ☕️