前言
写 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 必须有配套的释放函数。