C 语言函数设计模式实战经验

前言

写 C 代码时最头疼的问题之一:这个函数到底应该怎么定义?

  • 返回值还是 void?
  • 调用者给缓冲区,还是函数自己 malloc?
  • 参数怎么传?指针还是值?
  • 什么时候需要 free?

这篇文章总结了我写 RFID(射频标签)仓库管理系统·项目时积累的实际经验,告诉你每种场景下该怎么选。


一、函数设计的核心问题

一个函数本质上是:输入 → 处理 → 输出

复制代码
输入(参数) →  函数(处理逻辑)  →  输出(返回值/参数)

决定函数怎么定义,只需要回答一个问题:

调用者能不能提前知道输出数据的大小?


二、三种设计模式

模式 1:调用者提供缓冲区

适用场景: 输出大小是固定的,或者可以安全地预估一个上限。

复制代码
// 写法:void 函数,调用者传一个"空盒子"进来
void get_json_str(const char *json, const char *key,
                  char *out, int out_size)
{
    // 把值写入 out 数组
    out[i] = '\0';
}

// 调用者:
char batch_no[64];    // 调用者自己准备盒子(大小固定)
get_json_str(cmd, "batch_no", batch_no, sizeof(batch_no));
// 用完不需要 free(栈数组自动释放)

什么时候用这种:

场景 例子 上限
从 JSON 取一个字段值 批次号、仓库名 不会超过 64 字节
格式化一个字符串 时间戳 YYYY-MM-DD HH:MM:SS 固定 19 字节
复制一个已知长度的字符串 EPC、TID 12 字节原始数据

优点:

  • 无动态内存分配,不会内存泄漏
  • 速度快(栈操作)
  • 调用者完全控制内存

缺点:

  • 如果预估太小,数据会被截断
  • 调用者要确保缓冲区够大

实际经验: 能猜到上限的就用模式 1。比如批次号我们在代码里生成的,最长就是 RK20260610-999,14 字节,64 绰绰有余。


模式 2:函数返回申请好的内存

适用场景: 输出大小不确定,无法提前预估。

注意:调用者必须负责释放

复制代码
// 写法:返回 malloc 出来的指针
char **db_get_batch_list(int *count)
{
    BatchListData bd = {0};

    // 取数据库数据(数据库大小未知)
    sqlite3_exec(g_db, sql, callback, &bd, &err_msg);
    // 回调里不断 realloc + strdup

    *count = bd.count;
    return bd.list;     // 返回堆上的内存
}

// 调用者:
char **list = db_get_batch_list(&count);
// 用完必须释放!
db_free_str_list(list, count);

什么时候用这种:

场景 例子 为什么不能固定
从数据库查所有批次 批次列表 不知道用户入库了多少次
从数据库查所有标签 标签列表 每批次的标签数不固定
SD卡里的文件列表 文件名数组 不知道有多少个文件

优点:

  • 灵活,多少数据都装得下
  • 不浪费内存(按需分配)

缺点:

  • 调用者必须记得 free,否则内存泄漏
  • 代码中要有配套的释放函数(如 db_free_str_list
  • 性能略低(堆分配比栈慢)

实际经验: 凡是跟"列表"沾边的,几乎都用模式 2。你不知道数据库里有多少条记录。


模式 3:内部 malloc + 内部 free(自包含)

适用场景: 数据只在函数内部使用,不返回给调用者。

复制代码
void tcp_send_batch_list(char **list, int count)
{
    // 内部 malloc
    int buf_size = 128;
    for(int i = 0; i < count; i++)
        buf_size += strlen(list[i]) + 16;

    char *buf = (char *)malloc(buf_size);

    // 拼 JSON
    int pos = snprintf(buf, buf_size, "{\"cmd\":\"batch_list\",...");
    for(...) { ... }

    // 发完就 free
    send(s_client_fd, buf, pos, 0);
    free(buf);  // 内部释放,调用者不用操心
}

什么时候用这种:

场景 例子 原因
拼一个临时的 JSON 发送 tcp_send_batch_list 发完就没用了,不需要返回
读一个文件内容并处理 配置文件解析 读出来处理完就丢
临时格式化一段日志 日志输出 日志行长度不定

原因:一次拼完整发出去,网络效率更高

如果不拼完整,逐个发:

复制代码
// ❌ 错误做法:查到一条发一条
回调第1次 → tcp_send("RK20260610-003")
回调第2次 → tcp_send("RK20260610-002")
回调第3次 → tcp_send("RK20260610-001")

// QT 收到 3 条消息:
// {"cmd":"tag_found","epc":"RK20260610-003"}
// {"cmd":"tag_found","epc":"RK20260610-002"}
// {"cmd":"tag_found","epc":"RK20260610-001"}
// → 3 次 TCP 发送,3 次系统调用,网络利用率低

拼完整一次发:

复制代码
// ✅ 一次拼好,一次发送
{"cmd":"batch_list","count":3,"list":["RK20260610-003","RK20260610-002","RK20260610-001"]}
// → 1 次 TCP 发送,1 次系统调用

性能的关键:TCP 发送次数

每次调用 send() 都是一次系统调用(用户态 → 内核态)。发送 N 个小包比发送 1 个大包慢得多。

优点:

  • 调用者最省心(不用管内存)
  • 封装性好(内存管理对调用者透明)

缺点:

  • 函数内部做了 malloc 和 free,如果逻辑复杂容易漏 free
  • 调试时较难追踪内存

实际经验: 临时容器(如拼 JSON 发 TCP)都用模式 3。注意不要在中间 return 的地方忘了 free。


三、怎么判断用哪种?决策树

复制代码
你写一个函数
    │
    ├─ 输出数据需要返回给调用者吗?
    │   │
    │   ├─ 不需要 → 模式 3(内部 malloc + 内部 free)
    │   │   例子:tcp_send_batch_list
    │   │
    │   └─ 需要 →
    │       │
    │       ├─ 最大大小可以提前知道吗?
    │       │   │
    │       │   ├─ 知道(如最多 64 字节)→ 模式 1(调用者给缓冲区)
    │       │   │   例子:get_json_str
    │       │   │
    │       │   └─ 不知道(如列表数量不定)→ 模式 2(返回 malloc 内存)
    │       │       例子:db_get_batch_list
    │       │
    │       └─ 配套:必须有对应的释放函数!

四、配套的释放函数

使用模式 2 时,必须提供一个配套的释放函数。

复制代码
// 申请函数:
char **db_get_batch_list(int *count);

// 配套释放函数(必须成对出现):
void db_free_str_list(char **list, int count)
{
    if(!list) return;
    for(int i = 0; i < count; i++) {
        if(list[i]) free(list[i]);  // 释放每个 strdup 的字符串
    }
    free(list);  // 释放指针数组本身
}

为什么不能直接用 free(list)

因为 list 里的每个元素(list[i])都是 strdup 出来的,需要逐个 free,最后再 free 指针数组本身。

复制代码
// 错误:只 free 数组,不 free 里面的字符串
free(list);  // list[i] 指向的内存就泄漏了!

// 正确:先逐个 free 字符串,再 free 数组
for(int i = 0; i < count; i++)
    free(list[i]);
free(list);

五、实际项目中的函数一览

从RFID 项目里挑几个例子:

函数 模式 为什么这样选
get_json_str(json, key, out, size) 1 JSON 值长度有限,64 够用
db_save_inbound(batch_no, ..., epc_list, tid_list, count) 1 输入参数都是调用者提供的
db_get_batch_list(&count) 2 不知道有多少个批次
db_get_batch_item_count(batch_no) 1 返回一个整数,直接用 return
tcp_send_tag(epc, tid) 3 内部拼 JSON 发完就丢
tcp_send_batch_list(list, count) 3 同上
scan_export_tag_list(epc_list, tid_list, max) 1 调用者提供数组,最多 1024 个

六、常见错误

错误 1:模式 2 忘记 free

复制代码
char **list = db_get_batch_list(&count);
// 用完忘了 db_free_str_list(list, count);
// → 内存泄漏!每次查询泄漏一批字符串

补救: 每次写模式 2 的函数时,立刻写配套的释放函数,养成习惯。

错误 2:模式 1 缓冲区太小

复制代码
char buf[10];
get_json_str(json, "batch_no", buf, sizeof(buf));
// 如果 batch_no = "RK20260610-001"(14 字符),buf[10] 不够!
// 数据被截断成 "RK2026061"

补救: 缓冲区留够余量。64 通常够,不够就 128。

错误 3:模式 3 中途 return 忘了 free

复制代码
void func() {
    char *buf = malloc(1024);
    if(某条件) {
        return;  // ❌ 忘了 free(buf)!泄漏
    }
    free(buf);
}

补救: 用 goto 统一释放:

复制代码
void func() {
    char *buf = malloc(1024);
    if(某条件) {
        goto cleanup;
    }
    // 正常处理...
cleanup:
    if(buf) free(buf);
}

七、总结

模式 适用 谁分配 谁释放 典型函数名
1 大小固定 调用者 调用者(自动或手动) xxx_str, xxx_copy, xxx_get
2 大小不定 函数内部 调用者 xxx_list, xxx_create, xxx_load
3 用完即丢 函数内部 函数内部 xxx_send, xxx_write, xxx_log

一句话经验:

能猜上限的用模式 1,猜不到用模式 2,只管干活不返回的用模式 3。模式 2 必须有配套的释放函数。

相关推荐
sitellla2 小时前
Pydub:用 Python 处理音频,不写废话
开发语言·python·其他·音视频
xingyuzhisuan2 小时前
缓存命中率提升方案:从 30% 优化至 82% 全流程优化记录
java·开发语言·缓存·ai
郑洁文2 小时前
基于Python的恶意流量监测系统的设计与实现
开发语言·python
AI玫瑰助手2 小时前
Python流程控制:for循环与range函数的搭配使用
开发语言·python·信息可视化
anew___2 小时前
2026年Python爬虫技术完全指南:从入门到实战
开发语言·爬虫·python
Penfy_Z2 小时前
【Python LLM 调用踩坑】Connection error 终极解决方案!npm 代理导致阿里云通义千问接口连接失败
开发语言·python·npm
星辰徐哥2 小时前
Python AI基础:Python面向对象编程
开发语言·人工智能·python
小宁爱Python2 小时前
Python 依赖管理神器:requirements.txt 从安装到实战全指南
开发语言·python
俊俊谢2 小时前
[python]FastAPI + 自建SSE 踩坑全记录
开发语言·python·fastapi