视频封面

视频元信息

  • 视频标题:哈佛大学【中英⚡计算机科学导论(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] 等价于 *s
    • s[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 附近]
  • 正确复制步骤:
    1. 用 malloc 分配足够内存:char *t = malloc(strlen(s) + 1);(+1 给 \0)
    2. 用循环或 strcpy 复制内容:strcpy(t, s);
    3. 用完后 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 函数无法交换调用者的变量。

    void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
    }
    
    因为 C 默认按值传递,a、b 是 x、y 的副本,交换副本不影响原值。[时间戳 2450s 附近]

  • 解决方案:传地址(按引用传递)

    void swap(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    // 调用:swap(&x, &y);
    

考点:* 的三种用途

  1. 乘法:a * b
  2. 声明指针:int *p;
  3. 解引用:*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