
视频元信息
- 时长: 约 1 小时 40 分钟(估计)
内容摘要(TL;DR)
本节课深入计算机内存底层,介绍十六进制表示、指针(地址)、字符串的真实本质(
char *)、malloc/free手动内存管理、valgrind排错、传值与传引用、栈与堆、缓冲区溢出及文件 I/O,为 PSet 4 的图像滤镜(BMP 文件处理)做铺垫。
1. 图像表示与像素艺术 [03:05]
- 图像在计算机中最终是 bit 的网格:以位图为例,每个像素用 0/1 表示黑白,形成如笑脸等图案。
- 彩色图像需要更多字节:通常每个像素用 24 位(3 字节)分别表示 RGB(红、绿、蓝)。
- 课程动机:通过学生用便签纸拼成的像素画(棕榈树小岛)和历届学生的 Google Sheets 像素作品,引出“底层操作数据(包括图像)”的主题。
- 鼓励学生访问
cs50.ly/art自行体验像素画。
2. 十六进制(Hexadecimal) [07:15]
- 引入原因:内存地址、RGB 颜色等常用十六进制表示。
- 定义:基数为 16,数字为
0-9加上A-F(10-15)。 - 数学原理:与十进制/二进制同理,从右往左为 16^0、16^1、16^2…位。
- 关键换算:
- 16 进制
0A= 十进制 10,0F= 15。 10(十六进制)= 16(十进制),不是“十”。- 两位十六进制最大值
FF= 255,即16*15 + 1*15。
- 16 进制
- 为什么用两位十六进制:一个字节(8 bits)正好可以用两位十六进制表示,而二进制需要 8 位 → 更紧凑、易读。
- 歧义消除:十六进制书写时前缀
0x(如0x123),表示后面的数字是十六进制。
3. 地址运算符、指针与 printf 格式代码 [~10:00]
&运算符:取变量地址(“address of”)。*运算符(解引用,dereference):根据地址访问对应内存单元(“go there”)。%p格式代码:打印指针/地址。
代码示例:addresses.c
int n = 50;
printf("%p\n", &n); // 打印变量 n 的地址
int *p = &n; // p 是指向 int 的指针,存 n 的地址
printf("%p\n", p); // 打印 p 的值(即 n 的地址)
printf("%i\n", *p); // 解引用 p,得到 50
- 指针大小:现代 64 位系统上指针为 8 字节(64 位),因为物理内存可能超过 4GB,需要更多位来表示地址。
- 易错点:C 中
*有三种用途:乘法、声明指针、解引用;声明时带类型,解引用时不需要重复类型。
指针指到哪里?
指针本身也是变量,也有自己的地址(存在“指向指针的指针”),但本节课不深入。释放或使用前务必判断是否为空。
4. 字符串的本质:char * [~15:00]
- 现实:C 中并没有
string类型,string是 CS50 库用typedef提供的“训练轮”。 - 真实定义:字符串就是第一个字符的地址,并以
\0(NUL)结尾。 - 内存布局:如
"HI!"占用 4 个字节:H(0x123)、I(0x124)、!(0x125)、\0(0x126)。 printf("%s", s)的工作方式:它把s当作地址,从该地址开始逐字符输出,直到遇到\0。
CS50.h 内部原理
typedef char *string;
这只是为 char * 起了一个别名。
- 从本周起:应使用
char *而不是string,去掉训练轮。
5. 字符串比较:为什么不能直接用 == [~20:00]
- 数字比较:
i == j比较值,正确。 - 字符串比较:
s == t比较的是两个变量中存储的地址,而不是字符内容。 - 即使输入相同文本,两个字符串也会存储在不同的内存位置,所以
==永远 false。 - 正确方法:使用
strcmp(s, t),相等时返回 0。- 内部实现:用循环逐一比较字符,直到
\0,同时支持 < 和 > 判断字母序。
- 内部实现:用循环逐一比较字符,直到
- 打印地址可以看到两个字符串开头不同:
printf("%p\n", s); printf("%p\n", t);
6. 字符串拷贝:malloc 与 free [~25:00]
错误的拷贝
string s = get_string("s: ");
string t = s; // 只是拷贝地址
t[0] = toupper(t[0]); // 同时修改了 s 指向的那个字符串
s和t指向同一块内存 → 一个变了另一个也变。
正确的拷贝
char *t = malloc(strlen(s) + 1); // +1 为 '\0' 预留空间
for (int i = 0, n = strlen(s); i <= n; i++)
t[i] = s[i]; // 含 '\0'
if (strlen(s) > 0)
t[0] = toupper(t[0]);
free(t);
malloc(n):从堆中分配 n 字节,返回首地址;失败时返回NULL。free(p):释放之前分配的块,避免内存泄漏。- 防御性编程:使用
malloc后必须检查是否为NULL;访问字符串前检查长度是否大于 0。
内存泄漏
只
malloc不free会造成内存泄漏。程序退出时系统会自动回收,但长驻服务(服务器、时钟程序)如果不释放内存,最终会耗尽内存崩溃。
7. 使用 Valgrind 检测内存错误 [~32:00]
valgrind是检测内存问题的命令行工具。- 示例:
int *x = malloc(3 * sizeof(int)); x[1] = 72; x[2] = 73; x[3] = 33; // 错误:数组越界,应为 x[0], x[1], x[2] free(x); - 运行
valgrind ./program会报告:Invalid write of size 4(越界写)12 bytes in 1 blocks are definitely lost(内存泄漏)
- 要点:不需要完全读懂所有输出,重点看“Invalid read/write”和“definitely lost”,并关注给出的源码行号。
8. 垃圾值(Garbage Values)与 Binky 动画 [~35:00]
- 垃圾值:未初始化的内存中残留之前程序留下的随机数据。
- 示例中未初始化的
scores[1024]打印出乱七八糟的随机数。 - 危险性:如果对未初始化指针解引用,例如:
int *x; // 未初始化 int *y; x = malloc(sizeof(int)); *x = 42; *y = 13; // y 没有分配内存,包含垃圾地址 → 很可能会崩溃 - Stanford Binky 动画生动展示了指针/内存分配的必要性:
- 分配指针 ≠ 分配被指向的对象(“pointy”)。
- 解引用前必须确保指针指向已分配的有效内存。
9. 交换函数与“传值 vs 传引用” [~40:00]
失败版本(Pass by Value)
void swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
- 在
main中调用swap(x, y)后,x, y不变。 - 原因:
a, b是x, y的拷贝,修改的是swap自己的栈帧。
成功版本(Pass by Address / Reference)
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// 调用:swap(&x, &y);
- 传入的是
x和y的地址,通过解引用真正修改原变量。
理解:传引用本质是传地址值;变量在栈上各有独立帧,只有通过指针才能跨函数修改调用方的内容。
10. 栈与堆(Stack & Heap) [~47:00]
- 内存布局(从上到下):机器代码 → 全局变量 → 堆(向下增长) → 栈(向上增长)。
- 堆(Heap):
malloc/free管理的内存区域;每次malloc取走一部分堆内存。 - 栈(Stack):每次调用函数,压入一个栈帧(frame),包含局部变量、参数、返回地址;函数返回时栈帧被“弹出”。
- 作用域的本质:局部变量存在于当前栈帧中,离开函数即失效(内存内容可能还在,但不安全)。
- 溢出风险:
heap overflow:堆使用过多,向上溢出到栈。stack overflow:函数调用过深(如无限递归),栈向下溢出到堆。- 二者统称缓冲区溢出(buffer overflow)。
11. 真实世界案例:CrowdStrike 事故 [~53:00]
- 事件:CrowdStrike 安全软件更新导致全球系统崩溃,包括航空等行业,损失数百万美元。
- 根因:一个off-by-one 错误——内容解释器期望 20 个值,却越界读取第 21 个值(访问超出数组末尾的内存),触发崩溃。
- 教训:
- C 语言不阻止程序员越界访问内存。
- Python/Java 等语言内置了防护,避免此类错误。
- 更新软件前应充分测试;自动更新有一定风险。
12. 实现 get_int:scanf 的用法与危险 [~56:00]
使用 scanf 读整数
int n;
printf("n: ");
scanf("%i", &n); // 必须传地址,否则无法修改 n
printf("%i\n", n);
用 scanf 读字符串(危险示范)
char *s;
printf("s: ");
scanf("%s", s); // s 未初始化,指向垃圾地址,很可能崩溃
- 问题:不知道用户会输入多少个字符,指针没有分配任何内存。
- 解决方案:使用
get_string(动态扩容)或固定大小数组+限制长度; - 分配固定大小(如
malloc(4))也不安全:用户输入超过 4 字节就会溢出。
为什么 get_string 更安全
get_string会根据用户输入逐字符malloc扩大内存,确保不会越界;自己写scanf极难保证安全。
13. 文件 I/O:通讯录与复制文件 [~60:00]
写入 CSV 文件
FILE *file = fopen("phonebook.csv", "a"); // "w" 会覆盖原文件,"a" 追加
if (file == NULL) {
return 1;
}
char *name = get_string("Name: ");
char *number = get_string("Number: ");
fprintf(file, "%s,%s\n", name, number);
fclose(file);
- 注意:使用文件前必须判断
file != NULL。
实现简易 cp 命令
typedef uint8_t BYTE; // 1 字节
FILE *src = fopen(argv[1], "rb");
FILE *dst = fopen(argv[2], "wb");
BYTE b;
while (fread(&b, sizeof(BYTE), 1, src) != 0) {
fwrite(&b, sizeof(BYTE), 1, dst);
}
fclose(dst);
fclose(src);
- 文件读取函数会自动在文件中“前进”,类似播放进度。
14. PSet 4 预告:BMP 图像滤镜 [~70:00]
- 本周作业将面对 BMP 位图文件,图像是像素的网格。
- 需要实现滤镜:
- 灰度(grayscale):去掉颜色,保留亮度。
- 褐色(sepia):老照片风格。
- 水平翻转(reflect):镜像翻转像素。
- 模糊(blur):通过合并周围像素值降低清晰度。
- 边缘检测(edge detection,可选):找出物体轮廓。
- 核心仍然是操作文件和内存中的字节,与本节内容直接相关。
5. 关键标签/关键词
CS50x,计算机科学导论,Memory,Pointer,十六进制,Hexadecimal,char *,String,malloc,free,Valgrind,Stack,Heap,Buffer Overflow,CrowdStrike,scanf,File I/O,BMP,Pass by Reference,Garbage Values,地址运算符
生成时间:2026-09-11 13:15:29 生成平台:VideoNote www.xygy.top