C语言实现面向对象编程(OOP)基础教程
C语言实现面向对象编程(OOP)基础教程
本教程面向已掌握C语言基础的读者,希望通过C语言自身机制来理解面向对象编程(OOP)的一些底层实现思路。 我们依次模拟封装、成员函数、this指针、信息隐藏、继承与多态等常见特性,纯用C逐步构建,不求面面俱到,但求每一步都清晰可运行。 全文共六章,每节配有完整的示例代码、分段讲解以及实践中容易踩的坑,希望能为你的OOP学习提供一个参考。
一、封装成员变量
抽象:从具体事物中提取共同特征(属性)和共同行为。
封装:将抽象出的数据(成员变量)组合成一个整体——结构体(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;}运行结果:
年龄: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;}运行结果:
年龄: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;}运行结果:
年龄: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;}运行结果:
年龄: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_this 置 NULL,可能导致悬垂指针 |
| 栈对象不宜 destroy | 栈对象外壳在栈上,free 会导致崩溃,必须将 destroy_ 设为 NULL |
本节展示了信息隐藏的基本手段。实际项目中,建议直接阅读第五章,它去掉了全局
this,提供了栈/堆统一的安全初始化方式。
4.4 完整代码
(上述代码合并即为完整可运行程序,不再单独赘列。注意编译时需包含 stdio.h、stdlib.h、string.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;}运行结果:
年龄:18,姓名:张三张三说:你好!年龄:20,姓名:李四5.4 要点小结
-
显式
self指针:所有函数都接收Person* self作为第一个参数,无需全局状态,天然线程安全。 -
不透明私有数据:
PersonPrivate定义在.c文件中,外部代码只能通过setAge、getName等接口访问,无法直接触碰成员。 -
栈/堆统一接口:
- 栈对象:声明
Person p1;→Person_init(&p1)→ 使用 →Person_cleanup(&p1)。 - 堆对象:
Person* p = Person_newWith(...)→ 使用 →p->destroy(p)。
- 栈对象:声明
-
防御性编程:每个函数开头均检查
self和private_是否为NULL,避免崩溃。 -
栈对象
destroy设为NULL:在Person_init中明确将其置空,防止误调导致对栈内存执行free。
5.5 注意事项
| 问题 | 说明 |
|---|---|
| 栈对象必须配对 | 必须有 Person_init 和 Person_cleanup,否则内存泄漏或重复释放 |
| 栈对象禁止调用 destroy | destroy 内部会 free(self),栈对象调用即崩溃 |
| 堆对象销毁后指针悬空 | p2->destroy(p2) 后 p2 成为悬垂指针,应避免再次使用 |
| 多文件编译 | 对外只提供 person.h,私有结构体修改不影响用户代码 |
5.6 完整项目文件
person.h—— 公有接口(如上)person.c—— 实现(如上)test5.c—— 测试(如上)
编译命令(gcc):
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;}运行结果:
[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;}运行结果:
[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 保护虚表。编译时只需链接目标文件,修改私有实现不会影响用户代码。
文件结构示例:
project/├── person.h├── person.c├── student.h├── student.c└── main.cperson.h(公有接口,遵循最小包含原则)
#ifndef PERSON_H#define PERSON_H
#ifdef __cplusplusextern "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.c 和 student.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;}编译命令(严格模式):
gcc -std=c99 -Wall -Wextra -pedantic -o program main.c person.c student.c6.4 本章总结
本章展示了 C 语言模拟继承与多态的两种方式,完成了从基础封装到面向对象编程的完整演进:
- 简单继承(独立函数指针) 每个对象持有自己的函数指针,直观易懂。派生类通过替换部分指针实现重写,保留其余指针复用基类行为。代价是每个对象多占用函数指针的存储空间。
- 高效多态(虚函数表)
将函数指针集中存放在静态
const虚表中,对象只保留一个指向虚表的指针。所有同类对象共享同一虚表,大幅节省内存。调用时通过obj->vtable_->print_(obj)实现运行时多态。
关键实践(贯穿全系列):
- 基类放在派生类第一个成员——保证内存布局兼容,向上转型安全。
- 构造函数中立即绑定虚表或函数指针——对象创建完毕即可用,杜绝野指针。
- 虚表用
static const保护——防止意外修改,语义更清晰。 - 始终使用
strncpy+ 强制尾'\0'——杜绝字符串溢出。 - 防御式编程——每个行为函数先检查指针有效性。