2026 年 11 月 07 日
在 C++ 和 Rust 这样的语言里,一个对象在退出其所属作用域时,其占用的资源通常可被自动释放。C 语言没有这样的机制,对象的资源无论是分配还是释放,皆需手工完成。不过,基于 m4 语言,可以在 C 语言的外围,为对象构造生命域,当对象退出其所属生命域时,便自动释放其资源。
不知 m4 者,可阅读《m4 的咒语》; 对 m4 有所了解但缺乏实践经验者,可阅读《用 m4 武装 C》; 有 m4 实践经验者,若想有所精进,可阅读《GNU m4 技法集录》以及本文。
C 语言的每个作用域可以包含 1 个或多个生命域。每个生命域都要维护一个表达生命域深度的宏变量,该变量为
define(`_life_n_', 0)生命域的开始标记是 _life_start_,其定义为,
define(`_life_start_', `define(`_life_n_', incr(_life_n_))')生命域的结束标记是 _life_stop_,其定义为
define(`_life_stop_', `define(`_life_n_', decr(_life_n_))')以下文本可用于验证生命域开始和结束标记能否维护生命域深度,
if (1) {
// _life_start_
_life_n_
if (1) {
// _life_start_
_life_n_
// _life_stop_
}
_life_n_
// _life_stop_
}展开结果为
if (1) {
//
1
if (1) {
//
2
//
}
1
//
}
需要进入生命域的对象,其内存必须由 _malloc
宏分配。_malloc 的定义为
define(`_malloc', `$2 $1 = malloc($3)pushdef(`_free_'_life_n_, `free($1);')')以下代码
// _life_start_
_malloc(a, int *, sizeof(int));
_malloc(b, double *, sizeof(double));
indir(`_free_'_life_n_)popdef(`_free_'_life_n_)
indir(`_free_'_life_n_)popdef(`_free_'_life_n_)
// _life_stop_上述代码的展开结果为
//
int * a = malloc(sizeof(int));
double * b = malloc(sizeof(double));
free(b);
free(a);
// 由于生命域中每个对象的 free
语句已经存入了宏定义栈,释放生命域中每个对象的方法,调出位于栈顶的
free 语句,然后将其宿主宏从栈中弹出,该过程由
_exec_free_ 宏完成,
define(`_exec_free_', `indir(`_free_'_life_n_)popdef(`_free_'_life_n_)')_free 宏递归运行,每次递归皆会调用
_exec_free_ 输出一条 free
语句,直至宏定义栈为空,
define(`_free_', `ifdef(`_free_'_life_n_, `_exec_free_$0')')以下代码构造了两层生命域,用于验证 _free_ 宏,
// _life_start_
_malloc(a, int *, sizeof(int));
_malloc(b, double *, sizeof(double));
if (1) {
// _life_start_
_malloc(c, int *, sizeof(int));
_malloc(d, double *, sizeof(double));
_free_
// _life_stop_
}
_free_
// _life_stop_上述代码的展开结果为
//
int * a = malloc(sizeof(int));
double * b = malloc(sizeof(double));
if (1) {
//
int * c = malloc(sizeof(int));
double * d = malloc(sizeof(double));
free(d);free(c);
//
}
free(b);free(a);
// 你会发现除了 free 语句不能换行,并无其他问题。让
free 换行并不困难,只需将 _free_ 定义为
define(`_free_', `ifdef(`_free_'_life_n_, `_exec_free_
$0')')只是增加换行后,第一条 free 语句之前的缩进在后续
free 语句里不会出现,因为 _free_
宏并不知道它前面的缩进多长。不过,基于生命域的深度值能够构造缩进,但前提是需要固定每一层生命域的缩进长度。这只是一个想法,我无意实现,因为这些
free 语句位于同一行,对于 C 语言而言是合法的。
_free_ 不需要我们手工调用,可以将其植入
_life_stop_ 宏,由后者调用,
define(`_life_stop_', `_free_`'define(`_life_n_', decr(_life_n_))')只是 _life_stop_
不能再像前面的示例里那样放在注释里了,例如
// _life_start_
_malloc(a, int *, sizeof(int));
_malloc(b, double *, sizeof(double));
if (1) {
// _life_start_
_malloc(c, int *, sizeof(int));
_malloc(d, double *, sizeof(double));
_life_stop_ //
}
_life_stop_ //展开结果为
int * a = malloc(sizeof(int));
double * b = malloc(sizeof(double));
if (1) {
//
int * c = malloc(sizeof(int));
double * d = malloc(sizeof(double));
free(d);free(c); //
}
free(b);free(a); //不过,从形式上看,// _life_start_ 和
_life_stop_ // 也能构成一种对称结构。
若是写一个程序,将 C 代码里表达作用域的花括号全部替换为
_life_start_ 和
_life_stop_,这意味着我们拥有了一个隐形的 C
内存管理层。这样的脚本,之前我已经让 DeepSeek 写出了,见「用 m4 武装 C |
§3. 作用域」,对该文中的 trace-scopes.awk 脚本只需稍加改动,
# trace-scopes.awk
# 用法:awk -f trace-scopes.awk foo-meta.c > foo-meta-scoped.c
#
# 功能:
# 1. 将 C 代码中的作用域花括号 { } 替换为 _life_start_ / _life_stop_
# 2. 跳过字符串、字符字面量、行注释、块注释中的花括号
# 3. 跳过预处理指令(# 开头,含 \ 续行)中的花括号
# 4. 处理行尾反斜杠续行(行注释、字符串、字符字面量)
# 5. 对 _malloc 的托管变量名做跨作用域重名检查,警告输出到 stderr
#
# 约定:
# - _malloc 的调用不跨行
# - _obj_life 的变量名重名不做检查,靠约定
# - 字符串/字符字面量跨行仅通过行尾 \ 续行实现
BEGIN {
state = "normal" # normal | string | char | line_comment | block_comment
in_pp = 0 # 是否在预处理指令中
pp_cont = 0 # 预处理指令是否续行
scope_depth = 0 # 与 m4 的 _life_n_ 同步
}
{
line = $0
n = length(line)
cont = (n > 0 && substr(line, n, 1) == "\\")
# 预处理行检测:仅在 normal 状态、非续行中、行首(去空白)为 # 时进入
if (state == "normal" && !pp_cont) {
tmp = line
sub(/^[ \t]+/, "", tmp)
if (substr(tmp, 1, 1) == "#") in_pp = 1
}
if (in_pp) {
print line
if (cont) {
pp_cont = 1
} else {
in_pp = 0
pp_cont = 0
}
next
}
out = ""
for (i = 1; i <= n; i++) {
c = substr(line, i, 1)
c2 = substr(line, i + 1, 1)
if (state == "normal") {
if (c == "/" && c2 == "/") {
out = out c c2; i++; state = "line_comment"
} else if (c == "/" && c2 == "*") {
out = out c c2; i++; state = "block_comment"
} else if (c == "\"") {
out = out c; state = "string"
} else if (c == "'") {
out = out c; state = "char"
} else if (c == "{") {
out = out "_life_start_"
scope_depth++
} else if (c == "}") {
out = out "_life_stop_"
# 清理当前作用域的托管变量记录
for (k in managed) {
split(k, parts, SUBSEP)
if (parts[1] + 0 == scope_depth) delete managed[k]
}
scope_depth--
} else {
# 检测 _malloc 变量名,用于重名检查
prev = (i > 1) ? substr(line, i - 1, 1) : " "
if (prev !~ /[A-Za-z0-9_]/ && c == "_" && substr(line, i, 7) == "_malloc") {
j = i + 8
while (j <= n && (substr(line, j, 1) == " " || substr(line, j, 1) == "\t")) j++
k = j
while (k <= n && substr(line, k, 1) ~ /[A-Za-z0-9_]/) k++
name = substr(line, j, k - j)
if (name != "") {
for (d = 1; d <= scope_depth; d++) {
if ((d, name) in managed) {
printf "warning: managed variable \"%s\" at depth %d shadows depth %d\n", \
name, scope_depth, d > "/dev/stderr"
break
}
}
managed[scope_depth, name] = 1
}
}
out = out c
}
} else if (state == "string" || state == "char") {
out = out c
if (c == "\\") {
i++; out = out substr(line, i, 1)
} else if (state == "string" && c == "\"") {
state = "normal"
} else if (state == "char" && c == "'") {
state = "normal"
}
} else if (state == "line_comment") {
out = out c
} else if (state == "block_comment") {
out = out c
if (c == "*" && c2 == "/") {
out = out c2; i++; state = "normal"
}
}
}
print out
# 行注释在行尾结束,除非有 \ 续行
if (state == "line_comment" && !cont) state = "normal"
}便可用于将 C 的作用域替换为生命域。例如,假设 foo.c 的内容为
int main(void) {
_malloc(a, int *, sizeof(int));
_malloc(b, double *, sizeof(double));
if (1) {
_malloc(c, int *, sizeof(int));
_malloc(d, double *, sizeof(double));
}
return 0;
}可见 foo.c 中并无生命域标记。使用 trace-scopes.awk 处理 foo.c,
$ awk -f trace-scopes.awk foo.c
结果为
int main(void) _life_start_
_malloc(a, int *, sizeof(int));
_malloc(b, double *, sizeof(double));
if (1) _life_start_
_malloc(c, int *, sizeof(int));
_malloc(d, double *, sizeof(double));
_life_stop_
return 0;
_life_stop_只需重新定义 _life_start_ 和
_life_stop_,
define(`_life_start_', `define(`_life_n_', incr(_life_n_)){')
define(`_life_stop_', `_free_}define(`_life_n_', decr(_life_n_))')亦即令生命域标记的展开结果含有花括号,用于还原 C 的作用域。于是,上述 awk 脚本的处理结果最终会被展开为
int main(void) {
int * a = malloc(sizeof(int));
double * b = malloc(sizeof(double));
if (1) {
int * c = malloc(sizeof(int));
double * d = malloc(sizeof(double));
free(d);free(c);}
return 0;
free(b);free(a);}虽然代码并不好看,但是这些代码是让 C 编译器看的,你无需在意它是否美好。
前文已经实现了一个基于 m4 的隐形的 C 内存管理层。不过,需要考虑
return,break,continue,goto
等跳转语句,它们可以让程序流从 C
的当前作用域中跳出,导致当前生命域无法终止,内存出现泄漏。
return
语句的问题可以解决,只需在一个递归过程中,调用各层生命域所保存的
free
语句即可。只是在此过程中,生命域栈会被逐层清空,需要将每个被调用的
free
语句存入一个临时栈,最后基于该临时栈恢复生命域栈,该过程由
_exec_return 宏实现,
define(`_exec_return',
`ifdef(`_free_'$1,
`pushdef(`_free_tmp_'$1, defn(`_free_'$1))dnl 保存 free 语句
indir(`_free_'$1)popdef(`_free_'$1)$0($1)', dnl 执行 free 语句
`ifelse(eval(`$1 > 0 '), 1, `$0(decr($1))')'dnl 递归遍历
)')从各层临时栈中恢复生命域栈的过程由 _recover_life_stack
完成,
define(`_recover_life_stack',
`ifdef(`_free_tmp_'$1,
`pushdef(`_free_'$1, defn(`_free_tmp_'$1))popdef(`_free_tmp_'$1)$0($1)',
`ifelse(eval($1 < _life_n_), 1, `$0(incr($1))')')')_return 宏在完成调用各层 free
语句后,恢复生命栈,最后展开为 return 语句,
define(`_return',
`do {_exec_return(_life_n_)_recover_life_stack(1)dnl
return $1;} while (0)')break 和 continue
语句的生命域问题不好解决,因为与它们配合使用的
for,while 以及 switch
等语句的作用域实际上是同一个作用域在时间作用下的叠加结构,而生命域没有时间。此外,在循环语句中,break
和 continue
可能会在嵌套的条件语句里出现,需要为其寻找最近的有效生命域层,但最近的有效生命域层可能位于循环语句之外。最难以解决的是
goto
语句,在该语句面前,函数内部的作用域相当于不存在,故而生命域也不存在。对于这些语句的生命域维护,最好的方案是不解决,而是给出一个普适的约定,对于任一对象,若确定其所有权需要转移到其他作用域,则其内存资源只能用
C
原生的机制管理。本质上,break,continue
以及 goto 会对一些对象产生了所有权转移效用。
一些对象的内存是由特定函数分配的,例如动态数组、链表、树等容器对象,通常有专用的分配和释放函数,需要定义一个更具一般性的内存分配宏
_obj_life,
define(`_obj_life', `$1`'pushdef(`_free_'_life_n_, `$2;')')例如,假设 List 是链表类型,构造链表对象的函数是
list_new,释放链表的函数是 lise_free,则
_obj_life 的用法为
{
List *a = _obj_life(list_new(), list_free(a));
}上述代码的展开结果为
{
List *a = list_new();
list_free(a);}还有一种情况,在嵌套的 C
作用域里,内层作用域的对象可以与外层作用域的对象重名,而
_malloc 宏所保存的 free
语句基于对象名释放内存,对于正常的生命域而言,不会出现问题,但是若是在内层作用域里调用了
_return
宏,便会出现对象被重复释放内存的情况。为此,还需作一个约定,在嵌套的作用域中,不要创建同名对象。
除了上述的 trace-scopes.awk 脚本,我将 C 语言生命域所需的所有的 m4 代码汇总为 c-life.m4 文件,
divert(-1)
define(`_life_n_', 0)
define(`_life_start_', `define(`_life_n_', incr(_life_n_)){')
define(`_malloc', `$2 $1 = malloc($3)pushdef(`_free_'_life_n_, `free($1);')')
define(`_exec_free_', `indir(`_free_'_life_n_)popdef(`_free_'_life_n_)')
define(`_free_', `ifdef(`_free_'_life_n_, `_exec_free_`'$0')')
define(`_life_stop_', `_free_}define(`_life_n_', decr(_life_n_))')
define(`_exec_return',
`ifdef(`_free_'$1,
`indir(`_free_'$1)pushdef(`_free_tmp_'$1, defn(`_free_'$1))popdef(`_free_'$1)$0($1)',
`ifelse(eval(`$1 > 0 '), 1, `$0(decr($1))')')')
define(`_recover_life_stack',
`ifdef(`_free_tmp_'$1,
`pushdef(`_free_'$1, defn(`_free_tmp_'$1))popdef(`_free_tmp_'$1)$0($1)',
`ifelse(eval($1 < _life_n_), 1, `$0(incr($1))')')')
define(`_return',
`_exec_return(_life_n_)_recover_life_stack(1)`'return $1')
define(`_obj_life', `$1`'pushdef(`_free_'_life_n_, `$2;')')
divert(0)dnl为演示 C 生命域的用法,构造 foo-meta.c 文件,其内容为
#include <stdio.h>
int main(void) {
_malloc(a, int *, sizeof(int));
_malloc(b, double *, sizeof(double));
List *foo = _obj_life(list_new(), list_free(foo));
for (size_t i = 0; i < 10; i++) {
_malloc(c, float *, i * sizeof(float));
}
_return(0);
}执行以下命令,可将 foo-meta.c 转化为 foo.c 文件,
$ awk -f trace-scopes.awk foo-meta.c | m4 c-life.m4 - > foo.c
foo.c 的内容为
#include <stdio.h>
int main(void) {
int * a = malloc(sizeof(int));
double * b = malloc(sizeof(double));
List *foo = list_new();
for (size_t i = 0; i < 10; i++) {
float * c = malloc(i * sizeof(float));
free(c);}
do {list_free(foo);free(b);free(a);;return 0;} while(0);
list_free(foo);free(b);free(a);}