
视频元信息
- 视频标题:哈佛大学【中英⚡计算机科学导论(2026)|CS50x Introduction to Computer Science】 p05 P6 Lecture 4 - Memory
- 时长:约 1 小时 20 分钟(根据转录内容推断)
内容摘要(TL;DR)
本周“拆掉CS50库的训练轮”,深入计算机内存底层:从像素与十六进制讲起,引入指针、地址、& 与 * 运算符;揭示字符串本质是 char*;通过 swap、字符串复制、Valgrind、堆栈溢出等示例讲解内存管理与常见错误;最后介绍文件 I/O,实现持久化存储和文件复制,为后续图像滤镜项目做铺垫。
一、图像与像素:计算机如何存储图片
- 图像的本质是像素网格:任何屏幕上的图像都由成千上万个微小“点”(像素)组成。分辨率 = 水平点数 × 垂直点数,乘积决定图像占用的字节数(KB/MB 等)。[时间戳 100.9s]
- 1-bit 图像:用 0 表示黑色,1 表示白色。例如用 0/1 组成的网格可以拼出一张笑脸。现代图像使用 16/24/更多位表示每个像素的颜色。
- RGB 颜色模型:红(Red)、绿(Green)、蓝(Blue)三种颜色组合出所有颜色。Photoshop 中黑色 =
000000,白色 =FFFFFF,红色 =FF0000,绿色 =00FF00,蓝色 =0000FF。[时间戳 260.1s] - 255 与 FF 的关系:8 位二进制最大可表示 255,而十六进制
FF正好等于 255。一个十六进制位可表示 4 个二进制位,所以两个十六进制位 = 1 字节。
考点:十六进制(Hexadecimal)
- 基数为 16,数字包括
0-9和A-F(A=10, B=11, ..., F=15)。- 计数示例:
0F= 15,10= 16,FF= 255。- 十六进制常用于表示内存地址和颜色,因为 1 个十六进制位 = 4 bits,2 个 = 1 字节,非常简洁。
理解:像素、RGB、十六进制这些概念是理解内存和文件操作的基础。图像文件本质上就是按顺序存储的像素颜色值,后续处理图像就是在修改这些字节。
二、内存地址与指针入门
- 内存就像编号的格子:每个字节都有一个地址,程序员通常用十六进制表示地址(如
0x123)。为了避免歧义,十六进制数前缀0x表示“后面是十六进制”。[时间戳 707.8s] &运算符(取地址):&n返回变量n在内存中的地址。%p格式码:用于打印地址(指针),而不是%i。- 指针(Pointer):一种变量,存储的是另一个变量的地址,而不是普通数值。
- 声明:
int *p;表示p是一个指向整数的指针(存储整数的地址)。 - 赋值:
p = &n;把n的地址存入p。 - 解引用:
*p表示“去p中存储的地址处取值”。[时间戳 1120.9s]
- 声明:
易错点:指针声明语法
- 规范写法:
int *p;(星号靠近变量名)- 也可以写成
int* p;或int * p;,但推荐第一种。- 缺少
*会导致类型不匹配错误:incompatible pointer to integer conversion——右侧是地址,左侧却是普通整型变量。
- 指针大小:在大多数 64 位系统上,指针占 8 字节(64 位),因为需要能寻址超过 4GB 的内存。
理解:指针可以理解为“藏宝图”,它告诉你值存放在哪里,而不是值本身。用箭头表示指针指向目标,比具体地址更直观。
三、字符串的真相:char *
- 复习字符串存储:字符串
"HI!"在内存中占 4 字节:H、I、!、\0(空字符)。字符串以\0结尾,因此知道起始地址就能找到整个字符串。[时间戳 1250s 附近] - 字符串变量
s的实质:s并不是“整个字符串”,而是字符串第一个字符的地址(char *)。 - CS50 库的“白色谎言”:
string类型实际上是typedef char *string;,即string就是char*的同义词。去掉 CS50 库后,必须直接用char *s声明字符串。[时间戳 1370s 附近] - 数组语法与指针算术等价:
s[0]等价于*ss[1]等价于*(s+1)s[2]等价于*(s+2)- 数组下标是“语法糖”,底层就是指针加偏移量。
考点:指针算术
- 对指针加 1,移动的是“一个所指向类型的大小”。对
char*加 1 移动 1 字节,对int*加 1 移动 4 字节。printf("%s", s)内部会自动从s地址开始逐个字符打印直到\0;printf("%c", *s)只打印第一个字符。
思考:理解字符串即指针后,为什么 == 不能直接比较字符串就清楚了——它比较的是地址,而不是内容。这解释了为什么上周必须用 strcmp。
四、字符串比较与 strcmp
- 错误示例:
if (s == t)比较的是两个字符串的起始地址,即使内容相同,地址也不同,结果永远为“不同”。 - 正确做法:使用
strcmp(s, t)逐字符比较,返回 0 表示相等。 - 原因:
get_string为每次输入分配独立内存,即使输入相同内容,地址也不同。
理解:strcmp 内部就是循环逐字符比较,直到遇到 \0 或发现差异。
五、字符串复制:为什么直接赋值不行
- 直接赋值的问题:
char *t = s;只是把s的地址复制给t,两个指针指向同一块内存。修改t[0]也会改变s[0]。[时间戳 1640s 附近] - 正确复制步骤:
- 用
malloc分配足够内存:char *t = malloc(strlen(s) + 1);(+1给\0) - 用循环或
strcpy复制内容:strcpy(t, s); - 用完后
free(t);
- 用
malloc与free:malloc(n)向操作系统申请n字节内存,返回首字节地址。free(p)释放之前分配的内存,防止内存泄漏。- 需要
#include <stdlib.h>。
易错点:忘记复制
\0
- 如果循环条件写成
i < strlen(s),会漏掉空字符,导致字符串没有终止符。- 应使用
i <= strlen(s)或先int n = strlen(s);再i <= n。- 更稳妥:直接用
strcpy(t, s);。易错点:内存泄漏(Memory Leak)
- 用
malloc分配的内存必须用free释放,否则程序长期运行会越来越慢。- Valgrind 工具可以检测内存泄漏。
NULL检查:malloc失败时返回NULL(地址 0),使用前必须检查。get_string也可能返回NULL。
理解:NULL 是特殊地址 0x0,人类约定该地址不存放有效数据,用来表示“出错了”。
六、Valgrind 与垃圾值
- Valgrind:内存错误检测工具,比
debug50更底层。运行方式:valgrind ./program。[时间戳 2150s 附近] - 它能检测:
- 非法读写(如数组越界):
Invalid write of size 4 - 内存泄漏:
definitely lost: 12 bytes in 1 blocks - 使用未初始化的值
- 非法读写(如数组越界):
- 垃圾值(Garbage Values):未初始化的变量/内存中残留的旧数据。例如声明
int scores[1024];但不赋值,打印出来的是随机垃圾值。 - 错误指针解引用:如果指针未初始化就
*y = 13;,会访问非法地址导致崩溃。斯坦福大学的“Binky”粘土动画生动演示了这一概念。
考点:Valgrind 输出解读
- 关注
Invalid read/write of size N和definitely lost行,它们会提示出错的文件和行号。- 修复后再次运行,应看到
All heap blocks were freed -- no leaks are possible和ERROR SUMMARY: 0 errors。
七、函数传参:按值传递与按引用传递
-
问题:经典的
swap函数无法交换调用者的变量。因为 C 默认按值传递,void swap(int a, int b) { int temp = a; a = b; b = temp; }a、b是x、y的副本,交换副本不影响原值。[时间戳 2450s 附近] -
解决方案:传地址(按引用传递)
void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } // 调用:swap(&x, &y);
考点:
*的三种用途
- 乘法:
a * b- 声明指针:
int *p;- 解引用:
*p = 42;根据上下文区分,这是 C 语言初学者最易混淆的点。
- 内存布局:程序运行时,内存分为:
- 机器代码(程序指令)在顶部
- 全局变量在下方
- 堆(Heap):
malloc从这里分配内存,向下增长 - 栈(Stack):函数调用和局部变量使用,向上增长
- 堆和栈相向生长,可能碰撞导致堆溢出/栈溢出。
理解:栈溢出(Stack Overflow)一词正源于此——递归过深或函数调用过多会耗尽栈空间。
八、scanf 与获取用户输入
-
scanf基本用法:int n; scanf("%i", &n); // 必须传地址!scanf需要知道把输入值写到哪个地址,所以必须用&n。[时间戳 2750s 附近] -
获取字符串的陷阱:
char *s; scanf("%s", s); // 错误!s 未初始化,指向垃圾地址s没有分配内存,scanf会把输入写入未知位置,导致崩溃或未定义行为。 -
解决方案:
- 使用
char s[4];分配栈上数组(但长度固定,不安全) - 使用 CS50 库的
get_string(内部动态分配,自动扩展) - 自己用
malloc分配足够空间
- 使用
易错点:
scanf字符串必须预先分配内存
- 如果使用
char *s而不初始化,Valgrind 会报告Use of uninitialised value of size 8。- 固定大小数组可能被超长输入溢出(缓冲区溢出)。
理解:这就是为什么 CS50 库仍然有用——get_string 自动处理动态内存分配,避免缓冲区溢出。两周后转向 Python 也正是因为这类问题已被高级语言抽象掉。
九、文件 I/O:持久化数据
-
为什么需要文件 I/O:之前所有程序的数据在程序结束后丢失。文件 I/O 允许把数据保存到硬盘/SSD(持久存储),而不是易失的 RAM。[时间戳 3100s 附近]
-
核心函数(来自
<stdio.h>):fopen(filename, mode):打开文件,返回FILE *指针。模式"w"写入(覆盖)、"a"追加、"r"读取、"b"二进制模式。fprintf(file, format, ...):写入文件。fscanf(file, format, ...):从文件读取。fclose(file):关闭文件,保存数据。
-
示例:电话本 CSV
FILE *file = fopen("phonebook.csv", "a"); if (file == NULL) { return 1; } fprintf(file, "%s,%s\n", name, number); fclose(file);"w"会覆盖原文件,"a"追加。 -
文件复制程序(cp):用
fread和fwrite逐字节复制,实现类似进度条的效果。typedef unsigned char byte; byte b; while (fread(&b, sizeof(byte), 1, src) != 0) { fwrite(&b, sizeof(byte), 1, dst); }
考点:
fopen返回值必须检查
- 如果文件无法打开,
fopen返回NULL,不检查就使用会导致崩溃。- 任何涉及指针的调用都应检查是否为
NULL。
理解:文件 I/O 中指针是必需的——FILE * 是指向文件结构体的指针,没有指针就无法表达“当前文件位置”等状态。
十、下周预告:图像滤镜与 BMP 文件
- BMP 文件:位图文件,本质是像素网格(类似课前提到的便利贴猫图)。每个像素由 RGB 值组成。
- 你将实现:
- 灰度滤镜:把 RGB 值改为黑白灰
- 棕褐色调:模拟老照片
- 反射:沿垂直轴翻转像素
- 模糊:每个像素与周围像素混合
- 边缘检测:找出图像中物体的轮廓
- 这些任务需要你遍历文件中的每个字节并修改,正是本周指针、内存和文件 I/O 知识的综合应用。
思考:滤镜本质上就是对二维数组(像素矩阵)的数学变换。理解了内存布局,就能理解为什么图像文件可以逐字节操作。
关键标签
CS50 #内存 #指针 #十六进制 #字符串 #malloc #Valgrind #文件IO #栈 #堆 #字符串复制 #scanf #文件持久化 #图像滤镜 #BMP
生成时间:2026-09-02 15:40:25 生成平台:VideoNote