C 语言本身没有 public 和 private 这样的关键字。

(图片来源网络,侵删)
C 语言是一种过程式编程语言,它的核心思想是函数和数据结构,它没有像 C++、Java、C# 那样的面向对象编程特性,因此也就没有类的概念,自然也就没有访问修饰符(public, private, protected)。
如何在 C 语言中实现“私有”和“公共”的概念呢?
虽然 C 语言没有内置的访问控制,但我们可以通过一些编程约定和设计模式来模拟出类似的行为,这在编写大型、健壮的 C 库时尤其重要。
下面我将介绍几种从好到更好的方法。
命名约定
这是最简单、最常见的方法。

(图片来源网络,侵删)
- 公共接口:函数名以一个统一的前缀(
lib_)开头,或者干脆不使用特殊前缀,但会对外提供头文件(.h)声明。 - 私有实现:内部使用的函数和数据以一个特殊的前缀(
_或__)开头,表示它们是“内部”的,不应被库的调用者直接使用。
示例:
mylib.h (公共头文件)
// 公共接口,库的使用者应该只包含这个文件 void lib_initialize(void); void lib_do_something(void); void lib_cleanup(void);
mylib.c (私有实现文件)
#include "mylib.h"
#include <stdio.h>
// 私有数据结构
typedef struct {
int internal_state;
} _InternalContext;
// 私有全局变量
static _InternalContext g_context;
// 私有函数,对外不可见
void _internal_helper_function(void) {
printf("This is a private helper function.\n");
g_context.internal_state = 42;
}
// 公共函数的实现
void lib_initialize(void) {
printf("Initializing library...\n");
g_context.internal_state = 0;
_internal_helper_function(); // 私有函数可以被公共函数调用
}
void lib_do_something(void) {
printf("Doing something with internal state: %d\n", g_context.internal_state);
}
void lib_cleanup(void) {
printf("Cleaning up library...\n");
g_context.internal_state = -1;
}
main.c (使用者代码)

(图片来源网络,侵删)
#include "mylib.h"
int main() {
lib_initialize();
lib_do_something();
// 以下代码在编译时不会报错,但在设计上是错误的,因为用户不应该访问
// _internal_helper_function(); // 错误:未声明标识符
// g_context.internal_state = 100; // 错误:未声明标识符
lib_cleanup();
return 0;
}
优点:
- 简单,无需特殊技巧。
- 广泛应用于 C 标准库和许多知名开源项目(如 SQLite)。
缺点:
- 没有真正的强制力:编译器不会阻止你调用
_开头的函数或访问static变量,这只是一个“君子协定”,依赖于开发者的自觉。 - 如果用户错误地包含了
.c文件,这些“私有”符号就会暴露出去。
static 关键字
static 关键字是 C 语言实现封装的核心。
static函数:在一个.c文件中用static修饰的函数,其作用域仅限于该文件内部,其他文件无法看到或调用它,这完美地实现了函数的“私有化”。static全局变量:在一个.c文件中用static修饰的全局变量,其作用域也仅限于该文件内部,避免了与其他文件中的全局变量命名冲突。
上面的 mylib.c 示例已经使用了 static 来隐藏全局变量 g_context,我们也可以用它来隐藏辅助函数。
改进后的 mylib.c:
#include "mylib.h"
#include <stdio.h>
// 私有数据结构
typedef struct {
int internal_state;
} _InternalContext;
// 私有全局变量,static 限制了其作用域在 mylib.c 内部
static _InternalContext g_context;
// 私有函数,static 限制了其作用域在 mylib.c 内部
static void _internal_helper_function(void) {
printf("This is a private helper function.\n");
g_context.internal_state = 42;
}
// 公共函数的实现
void lib_initialize(void) {
printf("Initializing library...\n");
g_context.internal_state = 0;
_internal_helper_function();
}
void lib_do_something(void) {
printf("Doing something with internal state: %d\n", g_context.internal_state);
}
void lib_cleanup(void) {
printf("Cleaning up library...\n");
g_context.internal_state = -1;
}
这个方法比单纯的命名约定更进了一步,因为它利用了编译器的机制来限制访问。
不透明指针
这是 C 语言中实现数据封装最强大、最优雅的方法,常用于构建高质量的库。
思想:
- 公共头文件 (
mylib.h):只定义指向内部结构的指针(struct MyLibContext*),但不定义struct MyLibContext的具体内容。 - 私有实现文件 (
mylib.c):定义struct MyLibContext的具体成员。
这样,库的使用者只能拿到一个指向“黑盒”的指针,他们无法知道盒子内部有什么,也无法直接操作内部成员,只能通过你提供的公共函数来操作这个指针。
示例:
mylib.h (公共头文件)
// 前向声明,告诉编译器 "MyLibContext" 是一个结构体类型 // 但不透露其内部成员 typedef struct MyLibContext MyLibContext; // 公共构造函数 MyLibContext* lib_create_context(void); // 公共操作函数 void lib_initialize(MyLibContext* ctx); void lib_do_something(MyLibContext* ctx); void lib_cleanup(MyLibContext* ctx); // 公共析构函数 void lib_destroy_context(MyLibContext* ctx);
mylib.c (私有实现文件)
#include "mylib.h"
#include <stdio.h>
#include <stdlib.h>
// 这里是真正的“私有”定义
// 用户即使包含了 mylib.h 也看不到这个结构体
struct MyLibContext {
int internal_state;
char* some_other_data;
};
// 私有辅助函数
static void _private_helper(MyLibContext* ctx) {
ctx->internal_state = 100;
printf("Private helper called.\n");
}
// 公共函数的实现
MyLibContext* lib_create_context(void) {
MyLibContext* ctx = (MyLibContext*)malloc(sizeof(MyLibContext));
if (ctx) {
ctx->internal_state = 0;
ctx->some_other_data = NULL;
}
return ctx;
}
void lib_initialize(MyLibContext* ctx) {
if (ctx) {
printf("Initializing context...\n");
_private_helper(ctx);
}
}
void lib_do_something(MyLibContext* ctx) {
if (ctx) {
printf("Doing something with state: %d\n", ctx->internal_state);
}
}
void lib_cleanup(MyLibContext* ctx) {
if (ctx) {
printf("Cleaning up context...\n");
if (ctx->some_other_data) {
free(ctx->some_other_data);
}
ctx->internal_state = -1;
}
}
void lib_destroy_context(MyLibContext* ctx) {
if (ctx) {
lib_cleanup(ctx); // 先清理
free(ctx); // 再释放内存
}
}
main.c (使用者代码)
#include "mylib.h"
#include <stdio.h>
int main() {
// 用户只能拿到一个不透明的指针
MyLibContext* ctx = lib_create_context();
// 用户无法访问 ctx->internal_state,因为编译器不知道它存在
// ctx->internal_state = 999; // 编译错误:请求成员 "internal_state" 是什么东西?
// 用户只能通过公共接口操作
lib_initialize(ctx);
lib_do_something(ctx);
lib_destroy(ctx);
return 0;
}
优点:
- 最强的封装性:使用者完全无法触及内部数据,实现了真正的“私有”。
- 库的作者可以自由修改内部实现:只要公共接口不变,
.c文件中的struct MyLibContext如何修改都不会影响使用者。 - 防止命名空间污染:内部成员的名称不会暴露到全局作用域。
缺点:
- 实现起来稍微复杂一些。
- 每次访问成员都需要一个函数调用,可能会有微小的性能开销(但在现代架构下通常可以忽略不计)。
总结与对比
| 方法 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 命名约定 | _ 或 __ 前缀 |
简单,直观 | 无强制力,依赖开发者自觉 |
static 关键字 |
static 修饰函数/变量 |
编译器支持,作用域严格限制 | 仅限于文件内封装,无法隐藏数据结构 |
| 不透明指针 | 前向声明 + 私有结构体定义 | 最强的封装,实现与接口完全分离,可自由修改内部 | 实现稍复杂,有微小的函数调用开销 |
对于编写一个专业、稳定、可维护的 C 不透明指针是推荐的最佳实践,对于简单的工具或内部项目,命名约定 + static 通常是足够的。
