Reference Counting in C
事情是这样的,Tsoding在他的频道发布了这样一期视频,是关于在C语言中实现一个简易的Rc,并且使用它来实现了一个微型的Lisp解释器。我跟着他的视频敲完了所有代码。但老实说我并没有理解多少,所以在这里写一个博客并且盼望着能够从中学到一些什么
一、Lisp 是什么 1.1 Lisp 概述 Lisp的全称是List Processor(列表处理机),由John McCarthy在1958年创立,是目前仍在使用的第二古老高级编程语言,仅次于Fortran。和我们熟悉的C、Java、Python等语言相比,Lisp的核心差异在于 “程序即数据”(同像性) 这一独特特质,这也是它区别于其他主流语言的核心标志。
1.2 Lisp 核心特征
语法高度统一 :一切皆为S-表达式(列表),摒弃传统中缀表示法。例如加法运算不会写成1 + 2,而是采用前缀表示法的(+ 1 2)。
函数式编程为核心 :递归是核心编程方式,函数在Lisp中是“一等公民”,可作为参数、返回值或变量存储,灵活性极高。
早期特性奠基 :是最早引入动态类型和自动内存管理的语言之一,这两大特性也成为后续编程语言内存管理方案的核心动因。
二、Lisp 底层数据结构 2.1 核心结构:Cons Cell(点对单元) 理解Lisp的关键,在于认清其列表的本质——它并非连续数组,而是由Cons Cell(点对单元) 构成的链表,这是Lisp数据结构的底层基石。每个Cons Cell分为两部分:CAR 指向当前节点的数据(如整数、符号),CDR 指向列表的“剩余部分”。
一个标准的Lisp列表,本质是“嵌套的Cons Cell链条”,且必须以NIL(空列表) 结尾。例如列表(1 2 3)在内存中,就是三个Cons Cell串联而成:
第一个Cons Cell:CAR为1、CDR指向第二个Cons Cell;
第二个Cons Cell:CAR为2、CDR指向第三个Cons Cell;
第三个Cons Cell:CAR为3、CDR指向NIL。
2.2 易混淆概念区分
Cons Cell :底层物理结构,类似一个有两个格子的“盒子”,是构成所有Lisp数据结构的基础单元。
点对(Dotted Pair) :当Cons Cell的CDR指向非列表(也非NIL)时,称为点对,格式如(420 . 69),常用来表示坐标、键值对等二元关系。
标准列表 :只有当Cons Cell链条以NIL结尾时,才是我们常说的“标准列表”,也是Lisp中最常用的数据形式。
这种链表结构赋予Lisp一个重要优势——结构共享 :创建新列表时无需复制已有数据,只需新建Cons Cell并指向已有链表即可,这让数据操作更高效,但也给内存管理带来了一定挑战。
所以说Lisp的列表本质上就是链表
三、Lisp 解释器解析 3.1 Lisp 解释器核心:REPL循环 Lisp解释器的核心是REPL循环 ,简单来说就是四个步骤的循环执行:
3.2 内存管理的必要性 由于Lisp的递归链表操作、结构共享特性,多个列表可能共用同一个Cons Cell节点。若手动调用free(),既无法判断谁该负责释放,也容易出现内存泄漏或重复释放的问题,因此自动内存管理 就成了Lisp解释器的必选项。
首先,在一切开始之前,我们为什么需要用到Rc来管理这一切?
四、Lisp 解释器的核心内存管理方式 引用计数(RC)的核心逻辑很简单:给每个对象(如Lisp的Expr结构体)维护一个计数器,具体操作如下:
优点 :实时性高,内存无人使用时立即回收,无不可预测停顿;实现简单,无需扫描整个内存堆。
缺点 :每次指针赋值都要更新计数器,存在分散性能开销;无法处理循环引用(两个对象互相指向时,计数器永远无法归零,导致内存泄漏)。
所谓弱引用好像就是拿来解决这个问题的
Tsoding并没有完整的实现整个Lisp解释器,而是只简单实现了
这两个部分。
我觉得从Rc的实现来开始切入是相当合适的
1 2 3 4 5 6 7 8 9 10 typedef struct { ptrdiff_t count; void (*destroy)(void *data); } Rc; void *rc_alloc (size_t size, void (*destroy)(void *data)) ;void *rc_acquire (void *data) ;void rc_release (void *data) ;ptrdiff_t rc_count (void *data) ;
这是我们rc.h中的所有的函数声明。
观察Rc类型,我们可以发现其中只简单的包含两个元素,一个类型为pirtdiff_t的count,以及一个void (*)(void *)的函数指针,指向被Rc管理的类型自定义的析构函数。
那么我们是如何通过Rc来管理自定义的的呢,这里我们就用到了C语言的一个特性:内存偏移
在 C 语言中,我们通常认为指针指向的是一块内存的“起点”。但 rc.h 利用了一个空间上的错位:用户看到的起点,其实是数据的中点。
1 2 3 +------------+-----------+ | Rc | struct | +------------+-----------+
我们可以观察rc_aclloc函数的实现:
1 2 3 4 5 6 7 8 9 void *rc_alloc (size_t size, void (*destroy)(void *data)) { Rc *rc = malloc (sizeof (Rc) + size); assert(rc); rc->count = 0 ; rc->destroy = destroy; printf ("[RC] %p allocated\n" , rc); return rc + 1 ; }
我们分配的连续内存中内嵌了 Rc 元数据头,用于存储引用计数和对应的析构函数指针。接口返回的指针直接指向自定义数据类型,调用 rc_alloc 后即可直接操作目标对象,Rc 元数据头作为底层实现细节被完全隐藏,无需显式访问。
然后我们可以来观察rc_release函数的实现
1 2 3 4 5 6 7 8 9 10 11 void rc_release (void *data) { Rc *rc = (Rc *)data - 1 ; rc->count -= 1 ; if (rc->count <= 0 ) { rc->destroy(rc + 1 ); printf ("[RC] %p released\n" , rc); free (rc); } }
这个函数的功能分三步:
通过指针运算取回我们需要的header
减少引用次数
当引用次数<=0时,调用析构函数,最后进行内存释放
我们为什么要这么设计?
这种模式是高度泛用的(Generic),它是 C 语言中模拟“多态析构”的标准工程实践。
这段代码本质上是解耦了内存管理逻辑(Rc)与具体的业务清理逻辑(Lisp) 。
大概就是“我的引用用完了,剩下的内部资源清理,交给你专属的析构函数处理“这个逻辑
而在rc_acquire函数中,我们所做的工作仅仅只取回头部之后将count+1,随后返回原地址
1 2 3 4 5 6 void *rc_acquire (void *data) { Rc *rc = ((Rc *)data - 1 ); rc->count += 1 ; return data; }
可以发现 rc_alloc 并不进行“计数 +1”这个行为 ,它将初始计数设置为 0。真正的“计数 +1”是由 rc_acquire 负责的。
这种设计背后有一套严密的逻辑配合:
1. rc_alloc 的职责:初始化 rc_alloc 的任务是分配内存并设置好“元数据”。
2. rc_acquire 的职责:增加引用 这是真正执行 rc->count += 1 的地方。
当我们希望一个指针正式指向这个对象并共享其所有权时,你必须显式调用 rc_acquire。
3. 为什么要分两步?(配合构造函数) 观察我们的构造函数(如 make_pair),可以发现这种设计的精妙之处:
1 2 3 4 5 6 7 Expr *make_pair (Expr *left, Expr *right) { Expr *expr = alloc_expr(EXPR_PAIR); expr->pair .left = rc_acquire(left); expr->pair .right = rc_acquire(right); return expr; }
关于这个,我们一会会提到的(确信)
总结
rc_alloc :负责 “生”,但不负责“养” ,计数设为 0。
rc_acquire :负责 “养” (增加所有权),计数 +1。
rc_release :负责 “弃” (放弃所有权),计数 -1,并在无人领养(计数归零)时销毁。
这种“由调用者决定何时加 1”的模式,能有效避免在构造复杂树状结构(如 Lisp 列表)时出现计数混乱。
Rc_count函数实际上是不太重要的,它仅仅只是返回当前对象的count而已。
我们刚才花了不少时间,把 rc.h 这个头文件的功能都梳理了一遍。
整体的思路是这样的:
我要做一个 Lisp 解析器 → 可 Lisp 里的列表结构,靠手动管理内存根本不现实 → 所以必须自己搞一套自动内存管理机制 → 于是就实现了 rc.h 这套引用计数库 → 最后再用它来支撑整个解析器的运行。
当然,Tsoding 本人是把这些内容混在一起开发的,中间甚至还重构了两次。他是一边写解析器的功能,一边慢慢完善 rc.h 的逻辑。rc_alloc 最开始甚至还自带 count++ 的逻辑,一直迭代到最后,才变成现在这种谁调用谁释放的样子。
虽说这种边做边改的方式,确实更贴近真实开发的流程。但我必须说实话,看完他那两个小时十八分钟的视频,我人是蒙的。当然这也就是这篇博客诞生的原因
想要实现一个lisp解析器,我们需要些什么?
首先我们应该需要一个数据结构来存储lisp的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 typedef enum { EXPR_NIL, EXPR_SYMBOL, EXPR_INTEGER, EXPR_PAIR, __count_expr_kind, } Expr_Kind; typedef struct Expr Expr ;struct Expr { Expr_Kind kind; static_assert (__count_expr_kind == 4 , "Amount of expression kinds has changed" ); union { const char *symbol; int integer; struct { Expr *left; Expr *right; } pair ; }; };
我并没有学习过Lisp,所以关于这种数据类型,以下是Ai的回答:
在 C 语言这种静态强类型 的语言中,变量的类型在编译时就固定了。但 Lisp 是一种动态类型 语言,一个变量既可以是数字,也可以是字符串,甚至可以是一个列表。
为了用 C 语言模拟出 Lisp 的灵活性,这段代码使用了 标签联合体(Tagged Union,也叫有别联合) 的设计模式。下面我们逐一拆解这段代码的精妙之处,并解释它如何完美映射 Lisp 中的四大核心概念。
一、 Lisp 四大概念在代码中的映射 1. 表达式 (Expression) 在 Lisp 中,“万物皆为表达式”。代码、数据、数字、函数,全都是表达式。
2. 原子 (Atom) 原子是不可分割的最小单元,对应代码中占位单一、不含有子表达式的类型。
3. 列表 (List) 列表是 Lisp 的灵魂,由点对(Cons Cell)链式拼接而成。
4. NIL (空列表) NIL 代表“无”,是列表的终点,也是空列表 '() 的化身。
二、 为什么要这样设计? 除了完美契合 Lisp 哲学,这段代码在 C 语言的工程实践上拉满了细节:
1. enum + union:打造安全的动态类型 C 语言的 union(联合体)允许不同的变量共享同一块内存。但 union 有一个致命缺点:它不知道自己当前存的到底是哪种类型。 如果你存入了一个指针,却按整数去读取,程序就会引发段错误。
2. 匿名联合体 (Anonymous Union) 观察代码,union 后面并没有变量名(没有写 union { ... } data;)。
好处 :在访问数据时,你可以直接写 expr->integer 或 expr->pair.left,而不需要写成冗长的 expr->data.integer。这让代码极其干净。
3. 极致的内存优化 因为使用了 union,struct Expr 的总大小只取决于它内部占用空间最大的那个成员 。
4. 惊艳的编译时防御:__count_expr_kind 与断言 这是最体现作者 C 语言功底的地方:
1 2 3 4 5 6 7 8 9 typedef enum { EXPR_NIL, EXPR_SYMBOL, EXPR_INTEGER, EXPR_PAIR, __count_expr_kind, } Expr_Kind; static_assert (__count_expr_kind == 4 , "Amount of expression kinds has changed" );
设计痛点 :在后续开发中,如果你想给 Lisp 增加一个“浮点数”类型(EXPR_FLOAT),你会在 enum 里插上一行。但很多程序员会忘记去修改 eval 里的 switch-case,或者忘记修改对应的分配函数,导致难以排查的 Bug。
解决方案 :作者在 struct Expr 的定义里直接写了 static_assert。如果你敢在 enum 里多加一个类型,__count_expr_kind 就会变成 5。此时编译器会直接拒绝编译并报错 。它强迫你在增加新类型时,必须顺便把这个断言改成 5。而当你去改这个 5 的时候,你就会猛然醒悟:“哦!我还得去其他地方适配这个新类型!”
有了数据类型之后,我们就可以构造这一切了
在main.c中,我们有四个构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Expr *alloc_expr (Expr_Kind kind) { Expr *expr = rc_alloc(sizeof (Expr), destroy_expr); expr->kind = kind; return expr; } static_assert (__count_expr_kind == 4 , "Amount of expression kinds has changed.Change the ctors" );Expr *make_nil (void ) { return alloc_expr(EXPR_NIL); } Expr *make_integer (int integer) { Expr *expr = alloc_expr(EXPR_INTEGER); expr->integer = integer; return expr; } Expr *make_symbol (const char *symbol) { Expr *expr = alloc_expr(EXPR_SYMBOL); expr->symbol = symbol; return expr; } Expr *make_pair (Expr *left, Expr *right) { Expr *expr = alloc_expr(EXPR_PAIR); expr->pair .left = rc_acquire(left); expr->pair .right = rc_acquire(right); return expr; }
这些函数负责将 rc_alloc 申请的原始内存“格式化”为具体的 Lisp 表达式。
make_nil():创建一个空列表。
make_symbol(const char *name):创建一个符号。
make_integer(int value):创建一个整数。
make_pair(Expr *left, Expr *right):创建一个点对(Cons Cell)。这是唯一一个递归 的构造函数,它会调用 rc_acquire 来接管子节点的所有权。
正是通过对rc_alloc的调用,让rc.h拥有了对于expr类型的管理能力
为什么在make_pair的时候需要接管子节点的所有权?
这是一个非常深刻的问题,触及了 生命周期管理 与 数据结构完整性 的核心。
简单直接的回答是:为了防止“过河拆桥”。
在 C 语言中,如果你创建了一个父节点(Pair),但它不“接管”子节点的所有权,那么子节点随时可能被外界释放,导致父节点手里握着两个指向荒原的野指针 。
我们可以从以下三个逻辑维度来深度拆解这个设计:
1. 逻辑上的“从属关系” 在 Lisp 的列表结构中,一个 Pair 节点是由 left 和 right 组成的复合体。
如果不接管会发生什么?
假设你写了这样一段代码:
1 2 3 4 Expr *a = make_integer(10 ); Expr *p = make_pair (a, make_nil()); rc_release(a);
正是因为 make_pair 内部调用了 rc_acquire(left),使得 a 指向的内存计数变为 2(一份在变量 a 手里,一份在父节点 p 手里)。即便外界释放了 a,计数依然为 1,内存安全无恙。
2. 支撑“递归释放”的连锁反应 你之前在研究 rc_release 时看到了它会调用 rc->destroy(rc + 1)。这个递归清理机制之所以能跑通,全赖 make_pair 当初接管了所有权。
构建时 :make_pair 通过 rc_acquire 建立了“父子契约”。
销毁时 :当父节点 p 被 rc_release 且计数归零时,它的析构函数 destroy_expr 也会被触发。
权力交接 :destroy_expr 内部会对 left 和 right 调用 rc_release。
核心逻辑: 如果你当初没有 acquire(计数没加),现在的 release(计数减一)就会导致子节点被提前错误释放。只有**“生时接管,死时交出”**,才能保证整棵树的内存管理是闭环的。
3. 应对“多重引用”的复杂场景 Lisp 最强大的地方在于数据共享 。
想象一下:一个复杂的表达式 A,既是列表 B 的头部,又是列表 C 的尾部。
这种 “去中心化” 的内存管理,必须依靠每一个 make_pair 在创建时自觉地通过 rc_acquire 宣示主权。
“make_pair 就像是一个领养机构 。当你把两个子节点 left 和 right 交给它组成一个新的家庭(Pair)时,它不能只是简单地‘看着’它们,而是必须通过 rc_acquire 正式办理领养手续(接管所有权)。
这样一来,即便原本的亲生父母(创建子节点的函数)撒手不管了,只要这个家庭还存在,孩子们就是安全的。而当这个家庭最终解散时,它也会负责任地通过析构函数,帮孩子们寻找下一条出路(递归释放)。”
make_pair 接管所有权,是为了实现 “数据结构的自完备性” 。它让每一个 Expr 节点都变成了一个能自感知、自负责的独立生命体,从而构建出 Lisp 那样复杂而稳固的语法树。
有构造,就会有析构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void destroy_expr (void *data) { Expr *expr = data; switch (expr->kind) { case EXPR_NIL: case EXPR_SYMBOL: case EXPR_INTEGER: break ; case EXPR_PAIR: rc_release(expr->pair .left); rc_release(expr->pair .right); break ; case __count_expr_kind: break ; default : { UNREACHABLE("Expr_Kind" ); } } }
这个函数与rc.h进行配合使用,在rc_release中进行对应结构的资源清理,正如前文提到的,我们正式通过这层解耦,实现了对于EXPR_PAIR类型的递归资源释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #define dump_expr(expr) dump_expr_opt(expr, 0) void dump_expr_opt (Expr *expr, int level) { for (int i = 0 ; i < level; ++i) { printf (" " ); } switch (expr->kind) { case EXPR_NIL: { printf ("NIL(%td)\n" , rc_count(expr)); } break ; case EXPR_SYMBOL: { printf ("SYMBOL(%td): %s\n" , rc_count(expr), expr->symbol); } break ; case EXPR_INTEGER: { printf ("INTEGER(%td): %d\n" , rc_count(expr), expr->integer); } break ; case EXPR_PAIR: { printf ("PAIR(%td):\n" , rc_count(expr)); dump_expr_opt(expr->pair .left, level + 1 ); dump_expr_opt(expr->pair .right, level + 1 ); } break ; case __count_expr_kind: break ; default : UNREACHABLE("EXPR_Kind" ); break ; } }
这是一个打印信息的调试函数,这个函数倒是没什么,就仅仅是在类型为EXPR_PAIR时会循环调用他自己而已。并且我们通过宏来模拟了默认参数这个东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Expr *args_to_list (va_list args) { Expr *arg = va_arg(args, Expr *); if (arg->kind == EXPR_NIL) return arg; return make_pair (arg, args_to_list(args)); } #define make_list(...) make_list_impl(NULL, __VA_ARGS__, make_nil()) Expr *make_list_impl (void *first, ...) { va_list args; va_start(args, first); Expr *list = args_to_list(args); va_end(args); return list ; }
这一块其实是很有意思的。
我们为什么需要一个make_list呢?make_pair(make_integer(1),make_pair(make_integer(2),make_pair(make_integer(3), make_nil())));
我相信没有人会想看到这一串破玩意儿的。所以我们构造了args_to_list和make_list_impl这两个函数,以及make_list宏
一、 宏的“预处理陷阱”:强制哨兵机制 在 C 语言中,处理变长参数(va_list)最头疼的问题是:函数不知道参数什么时候结束。 通常做法是要求调用者手动传一个 NULL,但这极易被遗忘。
作者通过宏 make_list 巧妙地解决了这个问题:
它在用户传入的参数 __VA_ARGS__ 后面自动追加 了一个 make_nil()。
无论你写 make_list(a, b) 还是 make_list(a),预处理器都会在末尾补上一个 NIL。
这个补上的 NIL 充当了哨兵(Sentinel) ,告诉后面的解析逻辑:“到这里就结束了”。
二、 为什么非要用递归?防止列表“反过来” 你可能会好奇,为什么要专门写一个递归的 args_to_list,而不是直接用 for 循环遍历参数?
这里涉及到栈与链表的结构冲突:
如果用简单的循环去构造列表,代码逻辑通常是“每拿到一个新元素,就把它插在当前列表的最前面”。
输入 :1, 2, 3
循环过程 :
NIL
make_pair(1, NIL) -> (1)
make_pair(2, (1)) -> (2 1)
make_pair(3, (2 1)) -> (3 2 1)
结果 :顺序完全反了。
而 args_to_list 通过 递归的回溯(Backtracking) 完美保持了顺序:
1 return make_pair (arg, args_to_list(args));
在这个调用中,make_pair 必须等待递归底部的 args_to_list 返回结果后才能执行。
执行逻辑(“先深入,后构造”):
递进阶段 :拿到 1(等着) -> 拿到 2(等着) -> 拿到 3(等着) -> 撞到哨兵 NIL。
回溯阶段 :从 NIL 开始,倒序往回“焊接”:
make_pair(3, NIL)
make_pair(2, (3))
make_pair(1, (2 3))
最终输出 :(1 2 3)。
那么现在我们就剩下main以及eval函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Expr *eval (Expr *expr) { if (expr->kind == EXPR_PAIR) { assert(expr->pair .left->kind == EXPR_SYMBOL); if (strcmp (expr->pair .left->symbol, "+" ) == 0 ) { int result = 0 ; Expr *args = expr->pair .right; while (args->kind != EXPR_NIL) { assert(args->kind == EXPR_PAIR); assert(args->pair .left->kind == EXPR_INTEGER); result += args->pair .left->integer; args = args->pair .right; } return make_integer(result); } else if (strcmp (expr->pair .left->symbol, "swap" ) == 0 ) { Expr *args = expr->pair .right; assert(args->kind == EXPR_PAIR); assert(args->pair .left->kind == EXPR_PAIR); assert(args->pair .right->kind == EXPR_NIL); Expr *arg = args->pair .left; return make_pair (arg->pair .right, arg->pair .left); } else { TODO("Report unknow function error" ); } } else { return expr; } }
eval 函数是解释器的求值入口,它根据表达式的 kind 标签进行逻辑分发:如果是整数等原子类型则直接原样返回;如果是列表(Pair),则默认将第一个元素视为符号指令,后续部分视为参数。在执行 + 指令时,它通过 while 循环遍历参数链表,将内部的整数逐个累加,最后返回一个新的整数对象;在执行 swap 指令时,它则通过重新解构和组合指针,将传入的点对进行左右对调并返回。这种设计通过大量的 assert 确保了输入结构的合法性,并利用底层的 make_ 系列函数在计算过程中生成新的结果对象,是整个解释器从“静态数据”转向“动态计算”的核心环节。
这个其实跟Rc的关系已经不大了,Rc更接近于管理这一切的底层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 int main () { printf ("========== Lisp 解释器功能与内存测试 ==========\n\n" ); printf ("【测试 1: 加法运算 (+ 10 20 30)】\n" ); Expr *add_expr = rc_acquire(make_list( make_symbol("+" ), make_integer(10 ), make_integer(20 ), make_integer(30 ))); printf ("构造表达式: " ); dump_expr(add_expr); printf ("当前引用计数: %td\n" , rc_count(add_expr)); printf ("--- 执行 Eval ---\n" ); Expr *add_result = rc_acquire(eval(add_expr)); printf ("计算结果: " ); dump_expr(add_result); printf ("--- 释放内存 ---\n" ); rc_release(add_expr); rc_release(add_result); printf ("加法测试内存已安全释放\n\n" ); printf ("【测试 2: 结构交换 (swap (69 . 420))】\n" ); Expr *swap_expr = rc_acquire(make_list( make_symbol("swap" ), make_pair (make_integer(69 ), make_integer(420 )))); printf ("原始表达式: " ); dump_expr(swap_expr); printf ("--- 执行 Eval ---\n" ); Expr *swap_result = rc_acquire(eval(swap_expr)); printf ("交换后结果: " ); dump_expr(swap_result); printf ("结果引用计数: %td\n" , rc_count(swap_result)); printf ("--- 验证:释放原始表达式 ---\n" ); rc_release(swap_expr); printf ("原始表达式已释放,结果依然存活: " ); dump_expr(swap_result); printf ("--- 最终清理 ---\n" ); rc_release(swap_result); printf ("所有内存已归还系统\n" ); printf ("\n================ 测试结束 ================\n" ); return 0 ; }
这里放一段ai生成的main以及运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 ========== Lisp 解释器功能与内存测试 ========== 【测试 1: 加法运算 (+ 10 20 30)】 [RC] 000001F17C372460 allocated [RC] 000001F17C372400 allocated [RC] 000001F17C372490 allocated [RC] 000001F17C372760 allocated [RC] 000001F17C3722E0 allocated [RC] 000001F17C372310 allocated [RC] 000001F17C372670 allocated [RC] 000001F17C372220 allocated [RC] 000001F17C3727F0 allocated 构造表达式: PAIR(1): SYMBOL(1): + PAIR(1): INTEGER(1): 10 PAIR(1): INTEGER(1): 20 PAIR(1): INTEGER(1): 30 NIL(1) 当前引用计数: 1 --- 执行 Eval --- [RC] 000001F17C372340 allocated 计算结果: INTEGER(1): 60 --- 释放内存 --- [RC] 000001F17C372460 released [RC] 000001F17C372400 released [RC] 000001F17C372490 released [RC] 000001F17C372760 released [RC] 000001F17C3722E0 released [RC] 000001F17C372310 released [RC] 000001F17C372670 released [RC] 000001F17C372220 released [RC] 000001F17C3727F0 released [RC] 000001F17C372340 released 加法测试内存已安全释放 【测试 2: 结构交换 (swap (69 . 420))】 [RC] 000001F17C372490 allocated [RC] 000001F17C372250 allocated [RC] 000001F17C372640 allocated [RC] 000001F17C372730 allocated [RC] 000001F17C372580 allocated [RC] 000001F17C372790 allocated [RC] 000001F17C372550 allocated 原始表达式: PAIR(1): SYMBOL(1): swap PAIR(1): PAIR(1): INTEGER(1): 69 INTEGER(1): 420 NIL(1) --- 执行 Eval --- [RC] 000001F17C372610 allocated 交换后结果: PAIR(1): INTEGER(2): 420 INTEGER(2): 69 结果引用计数: 1 --- 验证:释放原始表达式 --- [RC] 000001F17C372490 released [RC] 000001F17C372730 released [RC] 000001F17C372580 released [RC] 000001F17C372790 released [RC] 000001F17C372550 released 原始表达式已释放,结果依然存活: PAIR(1): INTEGER(1): 420 INTEGER(1): 69 --- 最终清理 --- [RC] 000001F17C372640 released [RC] 000001F17C372250 released [RC] 000001F17C372610 released 所有内存已归还系统 ================ 测试结束 ================
这个Lisp解释器对于一个没有接触过函数式编程的我来说还是太难理解了,燃尽了。。。。
不过我其实还是很好奇这套gc还可以用在哪些地方上面