前言
此文章仅用于记录个人学习cs61c过程中对课程内容的理解,如果你在学习过程中遇到了困难可以参考参考,但是本文并不能充当原课程的替代品
由于水平有限/语文功底不行,文章可能存在诸多谬误,欢迎指正
另外,因为一些原因(懒/学过),稿主省略了部分内容
不推荐单纯的收藏/粘贴复制到笔记中,个人看来这样学习是效率最低的
数字表示
- 采样与模拟;位可以编码anything
- 进制转换;进制适用性
- 二进制加法;负数表示;bias encoding
C与Memory Management
#1: Abstraction
高级编程语言–汇编语言–机器语言–硬件架构描述–逻辑门描述
C的特点
- 足够接近底层,能够充分利用底层特点
- 编译语言
- 将C代码直接翻译为基于机器的机器语言:编译后链接
- 编译&运行快速
- 基于架构,无法跨平台
- 预处理.c–>.i
- 充分相信程序员…也因此有很多风险
指针、数组、字符串
指针
形形色色的指针:万能指针void*、函数指针、空指针
C是pass-by-value型语言
数组
内存是一个巨大的数组,C数组是一个连续的内存块
建议写法:
int a[ARRAY_SIZE];
指针几乎和数组名相等,不同点在于:increment、declaration
数组非常原始:
- 无越界监察
- 数组作为参数就是一个指针
- 函数返回的指针有可能无法使用——作为局部变量被摧毁
字符串
C字符串就是个字符数组后面加个’\0’,而strlen(str)返回的长度不包含末尾的’\0’
字对齐
现代计算机内存
现代计算机的内存是可以寻址的,每个字节8bit,且有独特的地址
64bit机器的字长为8B,指针的大小也是8B
内存分配与字对齐
指针告诉了编译器每次获取多少bit的内存——而字对齐的获取方式往往是最快速的
结构体的大小就采用字对齐的方式进行分配:
struct foo {
int32_t a;
char b;
struct foo *c;
}//12B
字节序:大端&小端
——高位编址/低位编址
将0x1234abcd写入到以0x0000开始的内存中,则结果为:
address | big-endian | little-endian |
---|---|---|
0x0000 | 0x12 | 0xcd |
0x0001 | 0x34 | 0xab |
0x0002 | 0xab | 0x34 |
0x0003 | 0xcd | 0x12 |
note:一个地址存一个字节,2位16进制数才是一个字节
若不注意字节序的话,传递一个0x12345678可能会被翻译为0x78563412
目前小端为主流,因为这样的话,数据类型转换不用考虑地址的问题(?)
比特序:MSB&LSB
Most Significant Bit/Least Significant Bit
CPU存储字节内部的8个bit同样存在顺序。如,存储0xB4(10110100):
MSB | 10110100 |
---|---|
LSB | 00101101 |
MSB的CPU从左往右读,LSB的CPU从右往左读——而这并不会造成混乱,因为都是把CPU读出来的正确的数传递给对方
程序地址空间
从上至下分别为:堆栈、堆、静态区、代码区(text/code)
- 堆栈存放函数的局部变量,初始时在栈顶,向下扩展
- 堆存放malloc分配的内存,自动进行resize,初始时临近静态区,向上扩展
- 静态区存放静态变量,大小始终不变
- 代码区存放你的代码,在程序一开始运行时加载,大小始终不变
这些内存自动进行管理,无需担心;堆的变量可能需要手动free
地址从上到下为:0xFFFF FFFF ~ 0x0000 0000,其中0x00000000不可写不可读,可定义NULL指针
堆栈
每次调用一个函数就有一个stack frame被创建,每个stack frame包含:
- 返回时回到的地址/调用该函数的地址
- 参数
- 存放局部变量的空间
stack是一个连续的内存空间,通过stack pointer指向;函数返回时,stack pointer指向上一个stack frame
后入先出
栈里头传递指针是ok的,但是传递局部变量的指针绝对不行
堆
程序员管理的内存,分配时需指定需要分配的字节数,必须手动free
——非连续性的分配:否则可能会出现两块已分配内存间隔非常远的情况
建议使用sizeof,不同的机器int大小可能不同——产生bug
typedef struct { ... } TreeNode;
TreeNode *tp = (TreeNode *) malloc(sizeof(TreeNode));
int *ptr = (int *) malloc(20*sizeof(int));
... // check for NULL
free(ptr); // implicit typecast to (void *)
// void *malloc(size_t n)
// void free(void *ptr)
注意,free时一定要确保free正确的地址
realloc
时常检查NULL是必须的——因为当你耗尽内存的时候,返回的指针是NULL
——堆数据可能会被分配到新地址,原数据会被复制过去
// void *realloc(void *ptr, size_t size)
int *ip; ip = (int *) malloc(10*sizeof(int));
… … … // check for NULL, set 10 ints
ip = (int *) realloc(ip, 20*sizeof(int));
// contents of first 10 elements retained
… … … // check for NULL
realloc(ip,0); // equivalent to free(ip)
堆的多种bug
- 内存泄漏(忘记free):程序可能会变得越来越慢直到崩溃
- free后使用:读取到错误信息(如果被占用)/非法修改数据
- 二次free:free后使用bug/使堆数据崩溃(?)
- 忘记realloc可能会移动数据
- free堆栈数据,free非malloc产生的指针,malloc后忘记类型转换,不检查NULL…
内存管理的实现
我们希望实现快速的malloc和free,最小化的内存开销,同时避免内存的碎片化使用(块大小104,但是只是用了100)
K&R Implementation
每个堆块都有一个header,记录了块的大小、指向下一个块的指针
所有的free过的block被保存在一个循环链表中——而被分配的block的指针域被弃用
malloc
在空闲的block中寻找一个足够大的块——若不存在则向操作系统请求内存——还是不够,就会失败
如果有很多足够大的块该如何选择?
- best-fit:选择最小的
- first-fit:选择第一个找到的——足够快,但是有很多碎片
- next-fit:选择 从上一次分配空闲的地方开始查找到的 第一个块
free
会检查相邻的块是否也free,是的话就会融合为一个更大的块;否则直接加入free list
链表示例
struct Node {
char *data;
struct Node *next;
};
int main() {
struct Node *head = NULL;
add_to_front(&head, "abc");
… // free nodes, strings here…
}
void add_to_front(struct Node **head_ptr, char *data) {
struct Node *node = (struct Node *) malloc(sizeof(struct Node));
node->data = (char *) malloc(strlen(data) + 1); // 字符串需要+1
strcpy(node->data, data); // strcpy also copies null terminator
node->next = *head_ptr;
*head_ptr = node;
}
后记
以上就是本文的全部内容啦,欢迎大家交流