本实验是CMU课程15-213 (csapp) 的第七个课程实验,受限于本人代码能力比较菜鸟,耗时四天多的时间才完成了所有的debug,最终得分88/100。总的来看,这个lab可以算是最难的一个了(个人认为比archlab稍难一些),涉及了内存系统和C指针相关的很多内容,强烈推荐做一做,并且最好独立完成,对代码能力和debug能力都会有不小的提升,对内存系统也会有一个更清晰的认知。
一、实验要求
该实验要求为C程序实现一个动态内存分配器,类似于C标准库中的malloc()/free()/realloc()
,实验在malloclab-handout/mm.c
文件中进行,主要有四个接口需要实现:mm_init()
、mm_malloc()
、mm_free()
、mm_realloc()
,实验要求不允许使用复合数据结构,如数组、结构、联合等,但可以使用指针,因此实验要通过直接操作指针来实现你的内存管理系统,这个实验其实比较类似于一个plus版的数据结构题,实现的思路其实并不是很难懂,但实现过程中针对段错误这样的debug着实很让人头疼,不过若能坚持下来,相信对你的debug能力会是一个不小的提升。另外实验已经为你提供了必要的堆相关函数(原书上也可以看到一部分),用于初始堆、扩展堆和设定堆最大容量,只需要取相关函数拿来用即可(如扩展堆的函数)。
测评:除正确性外,主要从两个性能指标来考虑:空间利用率和吞吐量(课本上提到的相关内容),可以理解成时间和空间两个维度,在保证动态内存分配器正常工作的前提下,时间复杂度需要尽可能小,空间利用率要尽可能大,这既是本实验的测评标准。
建议:至少读完9.9章的全部内容,理解9.9.12隐式空闲链表的实现并完成好家庭作业9.17和9.18的内容再来做会好些(大佬请无视~)。这个lab的调试是一大难点,建议自己实现一些用于调试输出信息的函数,writeup中也有提到,例如在我的源代码文件(源文件链接见文章末尾)中就实现了一个专门用于输出块信息的调试函数,可以放在程序的适当位置来输出某一时刻某个块的信息用于辅助调试。也可以设计更复杂一些的调试函数,我比较懒就没再多写,不得不说调试也真的是一门学问。。。另外虽然建议独立完成lab,但是如果实在做不下去的话参考一下别人的思路也未尝不可,毕竟做lab更重要的是对学到知识的灵活运用,在一些细枝末节上无意义的浪费过多时间并不值得。
二、实现思路
原书列出了几种思路(显式链表,隐式链表,分离适配/伙伴系统),并已经给出了显式链表完整实现,我选择的是显式链表 + 按地址维护 + 最佳适配,基于课本上的隐式链表版本加以改进完善,效率总体而言还不错,当然还有各种各样更高效的思路,如分离适配版本等(C标准库的内存管理器就是基于此存储结构实现的),时间不太富裕没来得及实现(网上看到还有用红黑树完成的大佬拿到了98分),以后有时间还是要回来用不同的思路重新做一做,毕竟这个lab确实很不错。
选用的基本数据结构是双向空闲链表,注意这个链表只存储空闲结点(通过指针相连接),已分配的结点不会被视作位于该空闲链表中,其指针位在写入数据时会被覆盖,结点/块(这两个表达的是一个意思,都是空闲链表的一个基本单位)的基本格式如下:
块头部/脚部的四字节共同表示了大小/分配情况,分配位也作为该结点是否位于空闲链表中的标识。相比原书的隐式链表版本,该版本的每个存储结点需要额外两个指针prev和succ,分别保存空闲链表中自身所在结点位置的前驱和后继节点的地址,两个指针需要16字节(已经保证了8字节对齐),加上头尾块的8字节共24字节,即为最小分配块大小,注意每次返回的块以及对块寻址时指向的都是块头部后的位置,即prev所处的位置,如上图所示,这样做是为了方便和简化各种宏函数的操作,并且对已分配块的写入也是从此位置开始的。
由于实验中涉及到了大量的指针操作和显式类型转换,稍有不慎就会导致一系列的内存和类型错误,因此定义了一些宏用于专门对指针进行操作(其实对我这种代码能力比较菜鸟的人来说依然还是会有很多错误,不过确实会减少很多不必要的错误 = =):
#define WSIZE 4
#define DSIZE 8
#define CHUNKSIZE (1 << 12)
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define PACK(size, alloc) ((size) | (alloc))
#define GET(p) (*(unsigned int *)(p))
#define PUT(p, val) (GET((p)) = (val))
#define GET_SIZE(p) (GET(p) & ~0x7) //查看块大小
#define GET_ALLOC(p) (GET(p) & 0x1) //查看块是否已分配,为1表示已分配
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp) - WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE(((char *)(bp) - DSIZE)))
#define PREV(bp) ((char *)(bp)) //prev pointer
#define SUCC(bp) ((char *)(bp) + DSIZE) //succ pointer
#define GET_PREV(bp) (*(char **)(PREV(bp))) //获取prev指针的值,即prev所指向的地址,相当于*prev
#define GET_SUCC(bp) (*(char **)(SUCC(bp)))
#define PUTP(bp, val) (*(char **)(bp) = (char *)(val)) //设置指针值
#define ADJSIZE(size) (((size) <= 2 * DSIZE) ? 3 * DSIZE : DSIZE * (((size) + (2 * DSIZE - 1)) / DSIZE)) //向上舍入到最近的8的倍数,最低分配16字节的块(算头尾最低24)
与原书版本相比,多了下面六行,其中ADJSIZE()
用于调整最小块大小,其余都是对指针操作的封装,注意PREV()
和GET_PREV()
的不同,前者只是拿到了指针,返回的是prev指针的所处地址(每个链表结点都有自己的地址,其成员也一样),而后者相当于对指针解引用,对该指针存储的值进行寻址,返回的是其prev指向的结点的地址。几个主要接口的实现如下:
1. mm_init() 函数相关实现
这个函数主要用于初始化空闲链表并初始扩展一次堆,依然采用了和原书相同的思路,用序言块和结尾块作为边界,前面加上一个对齐块,序言块的格式与链表结点的格式一模一样,以便于统一操作,大小为24字节,始终已分配,其prev恒为NULL,heap_listp
作为唯一的全局变量始终指向序言块的prev位置,作为需要遍历(如查找,插入)时的起点,分配的尾块大小为0作为唯一标识。初始化后的链表情况大致如下:
分配后还需要做一次堆扩展,经过测试第一次堆扩展分配(1 << 0 ~ 1 << 8) / WSIZE
空间可以获得更高的空间效率,有可能是后面realloc相关测试文件频繁申请释放小空间所导致的。函数代码如下:
static char *heap_listp; //指向序言块
int mm_init(void)
{ /* 初始需要32字节的块,包括填充块4字节、序言块头脚 + prev和succ指针共24字节,尾块4字节 */
if ((heap_listp = mem_sbrk(4 * DSIZE)) == (void *)-1)
return -1; //分配失败
PUT(heap_listp, 0); //对齐块
PUT(heap_listp + WSIZE, PACK(3 * DSIZE, 1)); //序言头
PUTP(heap_listp + 2 * WSIZE, NULL); //序言块prev置空
PUTP(heap_listp + 2 * DSIZE, heap_listp + 4 * DSIZE); //序言块succ = 尾块偏移4字节(尾块的下一个块)
PUT(heap_listp + 3 * DSIZE, PACK(3 * DSIZE, 1)); //序言脚
PUT(heap_listp + 7 * WSIZE, PACK(0, 1)); //尾块
heap_listp += DSIZE; //heap_listp指针移到序言块的prev位置
/* 测试后发现首次分配空间控制在(2^0 ~ 2^8 / WSIZE)可获得更高效率 */
if (extend_heap((1 << 6) / WSIZE) == NULL)
return -1;
return 0;
}
堆扩展函数及其相关代码如下,由于每次扩展都会从尾块后的位置(即mem_brk指针保存的位置,见memlib.c文件)开始,覆盖掉先前的尾块,因此要在每次扩展前找出其succ指向尾块的结点(链表是线性的,因此这个结点是唯一的)保存下来,此处选择了直接遍历查找,并在做完堆扩展后使其指向新的尾块,以保证链表的连贯性,之后再将新扩展出来的块插入到链表中即完成了一次扩展
static void *extend_heap(size_t words)
{ /* 向堆申请空间以扩展空闲链表 */
char *ptr;
size_t size;
void *ptail = find_ptr_tail(); //每次扩展时先获取原先succ指向尾块的块,以便扩展后使其指向新尾块
/* 用一个新的空闲块扩展堆,以字(4bytes)为单位扩展,最小为24字节 */
size = (words < 6) ? 6 * WSIZE : words * WSIZE;
if ((long)(ptr = mem_sbrk(size)) == -1)
return NULL;
PUT(HDRP(ptr), PACK(size, 0)); //新块的header
PUT(FTRP(ptr), PACK(size, 0)); //新块的footer
PUT(HDRP(NEXT_BLKP(ptr)), PACK(0, 1)); //New epilogue header
PUTP(SUCC(ptail), NEXT_BLKP(ptr)); //原先succ指向尾块的指针的succ指向新的尾块
return coalesce(ptr); //为提高空间效率用于合并前后空闲块的函数,后面会讲到
}
inline static void *find_succ_to_tail()
{ /* 找出succ指向尾块的结点并返回其地址 */
void *t = heap_listp;
for (; GET_SIZE(HDRP(GET_SUCC(t))); t = GET_SUCC(t));
return t;
}
2. mm_malloc() 函数相关实现
mm_malloc()
类似于C标准库的malloc()
,分配一块指定大小的空间并返回,在无法分配(内存超限)时返回NULL,此处采用的是最佳适配策略,遍历一遍空闲链表,找出满足申请大小的所有空闲块中最小块的分配,代码实现如下:
void *mm_malloc(size_t size)
{ /* size以字节为单位 */
size_t asize;
size_t extend_size;
char *ptr;
if (size == 0)
return NULL;
asize = ADJSIZE(size); //最小块大小为16字节,加上头尾块的8字节为24字节
/* 采用最佳适配 */
if ((ptr = best_fit(asize)) != NULL) {
place(ptr, asize); //放置块,在适当的时机分割块
return ptr;
}
/* 无剩余空间,扩展堆 */
extend_size = MAX(asize, CHUNKSIZE);
if ((ptr = extend_heap(extend_size / WSIZE)) == NULL)
return NULL;
place(ptr, asize); //放置块,并在适当的时候分割
return ptr;
}
inline static void *best_fit(size_t size)
{ /* 最佳适配 */
void *t, *res = NULL;
for (t = GET_SUCC(heap_listp); GET_SIZE(HDRP(t)); t = GET_SUCC(t))
if (size <= GET_SIZE(HDRP(t)) && (res == NULL || GET_SIZE(HDRP(res)) > GET_SIZE(HDRP(t))))
res = t; //找到大于给定size的最小匹配块
return res;
}
空闲链表用于节省空间的一大策略就是分割块,这可以减少内部碎片。具体来说就是空闲链表中的块并不总是会完美契合所申请的分配大小,当一个块的实际大小超过申请空间一定限度时,就需要对其进行分割,分割出去的块通过继承待分配块的所有指针关系而进入空闲链表,注意在大部分涉及到指针转换操作时都需要特判结尾块的情况,后同,代码如下:
static void place(void *ptr, size_t asize)
{ /* 默认传入的是一个待分配的块,安置并设定该块,在内部碎片大于最小块大小时分割块 */
size_t csize = GET_SIZE(HDRP(ptr)); //获取块实际大小
if (ptr != heap_listp && csize >= 3 * DSIZE + asize) { //分割块,并转移指针至分割后的空闲块上
PUT(HDRP(ptr), PACK(asize, 1));
PUT(FTRP(ptr), PACK(asize, 1));
void *nextp = NEXT_BLKP(ptr); //nextp指向分割出去的块
PUT(HDRP(nextp), PACK(csize - asize, 0));
PUT(FTRP(nextp), PACK(csize - asize, 0));
/* 调整指针 */
PUTP(SUCC(GET_PREV(ptr)), nextp); //令原先指向ptr的块指向分割后的nextp,等同于ptr->prev->succ = nextp
if (GET_SIZE(HDRP(GET_SUCC(ptr)))) //判断ptr->succ是否为结尾块
PUTP(PREV(GET_SUCC(ptr)), nextp);
PUTP(PREV(nextp), GET_PREV(ptr)); //nextp继承原先ptr指向的块
PUTP(SUCC(nextp), GET_SUCC(ptr));
} else { //无法分割,从空闲链表中去除这个元素,表示已分配
PUT(HDRP(ptr), PACK(csize, 1));
PUT(FTRP(ptr), PACK(csize, 1));
del_block(ptr);
}
}
注意到未分割时需要从空闲链表中删除该结点(该结点已被分配),这个操作由del_block
函数实现,具体的操作很简单,把待删除块的指针关系传递给其前驱和后继结点即可,即让被删块的前驱结点的succ指向被删块的后继节点,被删块后继节点的prev指向前驱节点,代码如下:
inline void del_block(void *ptr)
{ /* 从空闲链表中移除块ptr */
PUTP(SUCC(GET_PREV(ptr)), GET_SUCC(ptr)); //ptr->prev->succ = ptr->succ
if (GET_SIZE(HDRP(NEXT_BLKP(ptr)))) //当ptr的下一个块不为尾块时
PUTP(PREV(GET_SUCC(ptr)), GET_PREV(ptr)); //ptr->succ->prev = ptr->prev
}
3. mm_free() 函数相关实现
该函数要将传入的块释放,在此处就是更改标记并插入到空闲链表中,此处为了提高空间利用率,在释放块前后有未分配块时会将其合并以扩大空闲块,在合并时除了修改块头部和脚部以外,还涉及到一系列的指针操作,将其封装到了convert()
函数中,合并和指针操作主要分为以下几种情况来讨论:
- 合并后块:此时需要将后块的指针关系转移到释放块
- 合并前块:此时不需要转移指针关系
- 合并前后块:此时前块和后块一定在空闲链表中相邻,释放块位于两块之间,此时直接将后块的指针关系去除重叠部分后继承到前块上即可
其余见代码
void mm_free(void *ptr)
{ /* 释放ptr指向的块,并前后有未分配块时合并 */
size_t size = GET_SIZE(HDRP(ptr));
PUT(HDRP(ptr), PACK(size, 0));
PUT(FTRP(ptr), PACK(size, 0));
coalesce(ptr);
}
static void *coalesce(void *ptr)
{ /* 合并前后未分配的块 */
size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(ptr)));
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(ptr)));
size_t size = GET_SIZE(HDRP(ptr));
if (prev_alloc && next_alloc) { //前后块都已分配,找到一个合适位置插入该块
insert(ptr);
return ptr;
} else if (prev_alloc && !next_alloc) { //合并后面的块
convert(ptr, NEXT_BLKP(ptr), 0); //转移指针
size += GET_SIZE(HDRP(NEXT_BLKP(ptr)));
PUT(HDRP(ptr), PACK(size, 0));
PUT(FTRP(ptr), PACK(size, 0));
} else if (!prev_alloc && next_alloc) { //合并前面的块
size += GET_SIZE(FTRP(PREV_BLKP(ptr)));
PUT(FTRP(ptr), PACK(size, 0));
PUT(HDRP(PREV_BLKP(ptr)), PACK(size, 0));
ptr = PREV_BLKP(ptr);
} else { //合并前后的块
convert(PREV_BLKP(ptr), NEXT_BLKP(ptr), 1);
size += GET_SIZE(HDRP(PREV_BLKP(ptr))) + GET_SIZE(FTRP(NEXT_BLKP(ptr)));
PUT(HDRP(PREV_BLKP(ptr)), PACK(size, 0));
PUT(FTRP(NEXT_BLKP(ptr)), PACK(size, 0));
ptr = PREV_BLKP(ptr);
}
return ptr;
}
inline static void convert(void *dst, void *src, int cond)
{ /* coalesce专用,转换合并块之间的指针,cond = 0表示释放块合并后块, cond = 1表示释放块合并前后块 */
if (!cond) { //此时为释放块合并后块,对应于coalesce的第一个else if,dst指向释放块,src指向后块
PUTP(SUCC(GET_PREV(src)), dst);
PUT(SUCC(dst), GET_SUCC(src)); //dst->succ = src->succ
PUT(PREV(dst), GET_PREV(src)); //dst->prev = src->prev
} else { //此时为合并释放块的前后块,对应于coalesce的else,dst指向前块,src指向后块
PUTP(SUCC(dst), GET_SUCC(src));
}
if (GET_SIZE(HDRP(GET_SUCC(src)))) //统一处理当src->succ不为结尾块时,令src->succ->prev = dst
PUT(PREV(GET_SUCC(src)), dst);
}
insert()
函数用于将空闲块插入到空闲链表中,由于选用了按地址维护策略,因此该函数会从头开始遍历链表,找出第一个地址大于待插入块的空闲块,将其插入到这个空闲块前面
inline static void insert(void *ptr)
{ /* 按地址顺序维护空闲链表,将ptr插入到空闲链表中的prev_t和t之间, prev_t和t一前一后遍历整个空闲链表 */
void *t = heap_listp, *prev_t; //prev指向空闲链表中t前面的元素
for (; t <= ptr; prev_t = t, t = GET_SUCC(t)); //找到第一个地址比ptr更大的空闲块t
PUTP(SUCC(ptr), t); //将ptr插入到t前面, ptr->succ = t
PUTP(PREV(ptr), prev_t); //ptr->prev = prev_t
PUTP(SUCC(prev_t), ptr); //prev->succ = ptr
if (GET_SIZE(HDRP(t)))
PUTP(PREV(t), ptr); //当t不为结尾块时,t->prev = ptr
}
4. mm_realloc() 函数相关实现
同C标准库的realloc()
,有两个参数void *ptr
和size_t new_size
,要将ptr
指向的块修改为指定的新大小new_size
,通过后面的测试也发现,这个函数是影响空间效率的一个关键因素,只单纯的调用和mm_malloc()
申请新块和mm_free()
释放旧块会导致空间浪费,因此需要做一些优化,针对ptr指向块的原大小(记为old_size
)和new_size
的对比,以及ptr
指向的块的前后块的分配状态,分成了以下几种情况讨论:
- new_size == 0:此时默认为释放
ptr
- ptr == NULL:此时默认为调用
mm_malloc()
创建一个新块并返回 - new_size < old_size:新申请的空间更小,此时可以直接将原块分割,而不必新申请空间
- new_size >= old_size:此时新申请的空间更大,先查看是否
ptr
所指块前后有空闲块且合并后的大小可以满足新申请的空间,若可以则直接合并前/后块,而同样不必直接申请空间 - 若以上条件都不满足,调用
mm_malloc
申请新块,释放旧块
块的复制直接调用库函数memcpy()
即可,需要处理好复制大小,以及做好块头/块脚大小的取舍
void *mm_realloc(void *ptr, size_t new_size)
{
if (new_size == 0) { //new_size为0视作free
mm_free(ptr);
return NULL;
}
if (ptr == NULL)
return mm_malloc(new_size);
void *oldptr = ptr;
void *newptr;
size_t old_size = GET_SIZE(HDRP(ptr));
new_size = ADJSIZE(new_size); //调整新分配块的大小
size_t copySize = (new_size < old_size ? new_size : old_size) - DSIZE; //选取更小的长度作为复制长度,同时去掉头尾块
/* 分以下几种情况操作:新尺寸不变,新尺寸更小,新尺寸更大但前/后有空余块可直接合并,无法合并必须另行分配的情况来讨论 */
if (new_size == old_size) { //新尺寸不变
return ptr;
} else if (new_size + 3 * DSIZE <= old_size) { //新尺寸更小且新旧尺寸距离超过最小块,就地分割
newptr = ptr;
PUT(HDRP(newptr), PACK(new_size, 1));
PUT(FTRP(newptr), PACK(new_size, 1));
oldptr = NEXT_BLKP(ptr);
PUT(HDRP(oldptr), PACK(old_size - new_size, 0));
PUT(FTRP(oldptr), PACK(old_size - new_size, 0));
insert(oldptr); //分割后的块插入空闲链表
return newptr;
} else { //无法分割,查看前后块是否闲置且大小足够,若足够则合并
size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(oldptr)));
size_t prev_size = GET_SIZE(FTRP(PREV_BLKP(oldptr)));
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(oldptr)));
size_t next_size = GET_SIZE(HDRP(NEXT_BLKP(oldptr)));
if (!prev_alloc && prev_size + old_size >= new_size) { //优先合并前面的块,使得空间得以最大利用
old_size += prev_size;
del_block(PREV_BLKP(oldptr));
PUT(FTRP(oldptr), PACK(old_size, 1)); //设定合并后块大小
PUT(HDRP(PREV_BLKP(oldptr)), PACK(old_size, 1));
newptr = PREV_BLKP(oldptr);
memcpy(newptr, oldptr, copySize); //从旧块复制到新块
return place_alloc(newptr, new_size, old_size); //只需要new_size大小,检查是否可以再分割
} else if (!next_alloc && next_size + old_size >= new_size) { //合并后面的块
old_size += next_size;
del_block(NEXT_BLKP(oldptr));
PUT(HDRP(oldptr), PACK(old_size, 1));
PUT(FTRP(oldptr), PACK(old_size, 1));
return place_alloc(oldptr, new_size, old_size);
} else if (!prev_alloc && !next_alloc && next_size + prev_size + new_size >= old_size) {
old_size += prev_size + next_size; //只合并前块或后块无法满足大小,需要同时合并前后块
del_block(PREV_BLKP(ptr));
del_block(NEXT_BLKP(ptr));
PUT(HDRP(PREV_BLKP(ptr)), PACK(old_size, 1));
PUT(FTRP(NEXT_BLKP(ptr)), PACK(old_size, 1));
newptr = PREV_BLKP(ptr);
memcpy(newptr, oldptr, copySize);
return place_alloc(newptr, new_size, old_size);
}
}
/* 无法合并,只能重新分配 */
if ((newptr = mm_malloc(new_size - 8)) == NULL) //进入mm_malloc会再调整一次,因此先减去头尾块大小
exit(1);
memcpy(newptr, oldptr, copySize); //复制内容
mm_free(oldptr); //释放掉先前的指针
return newptr;
}
为了进一步节约空间,在选择了合并前后块的分支后又进行了一次分割判断,由以下函数进行
inline static void *place_alloc(void *ptr, size_t nsize, size_t rsize)
{ /* 对已分配块的放置/分割,realloc专用,nsize为所需大小,rsize为传入块实际大小 */
if (nsize + 3 * DSIZE > rsize) {
return ptr; //无法分割
} else {
PUT(HDRP(ptr), PACK(nsize, 1));
PUT(FTRP(ptr), PACK(nsize, 1));
void *nextp = NEXT_BLKP(ptr); //nextp为分割出来的块
PUT(HDRP(nextp), PACK(rsize - nsize, 0));
PUT(FTRP(nextp), PACK(rsize - nsize, 0));
insert(nextp); //分割出来的新块插入到空闲链表中
return ptr;
}
}
5. 评测 && debug
注:在输入命令评测前记得先把malloclab-handout/config.h
中的宏#define TRACEDIR
后面的参数更改为你电脑上traces
文件夹的路径,以我的为例:
#define TRACEDIR "/home/leo/.vscode-server/code/c/csapp/lab/malloclab/malloclab-handout/traces/"
在Linux shell中键入以下命令即可评测最终得分
make && ./mdriver -V
还有一些命令行参数用于指定评测文件或其他信息可以参考lab的writeup,调试(使用GDB)可以输入下面的命令
make && gdb --args ./mdriver -t traces/ -v
评测结果:
完整代码见下:
https://gitee.com/climberleo/csapp/blob/master/lab/malloclab/malloclab-handout/mm.c
三、总结 && 心得
总得来说这个lab设计得非常棒,CMU的老师们也是非常有心,一路做下来,这个项目最难的地方还是在于对指针的操作,手握着指针,内存会完全裸露在你的面前,这是C这门语言的特性所决定的,而随着代码量和代码复杂度的提高,debug难度也会不断地提高,对内存的任何操作失误都会导致很棘手的麻烦,因此你的所有指针操作都要尽可能的小心谨慎,你要足够的细心,更重要的是要有足够的耐心,能经得住一个又一个讨厌的segmentation fault,或许这也是这个lab想要教给我们的。
能够独立完成一个lab还是比较有成就感的,这提升了我的代码能力,也让我对计算机程序的内存系统有了一个更深入的认识,它不再是一个黑盒,而是一个精密复杂的“字节数组”,但同时也让我认识到自己能力上的不足,还需要不断地精进技术,分数说明了还是有很多需要改进的地方,以后有时间了也要重新做一做,试试不同的思路。最后感谢观看,各位如果有什么建议或者独到的经验也欢迎一起交流分享