2026/4/2 17:31:39
网站建设
项目流程
用表格做的网站,怎样设计网页的首页,wordpress数据库无法访问,织梦手机网站怎么修改密码Problem Set 4 概述
本周的作业围绕 内存 主题展开#xff0c;包含音频处理、图像滤镜和文件恢复三类题目#xff1a;
题目难度核心知识点Volume⭐文件 I/O、二进制数据处理Filter-less⭐⭐⭐图像处理、二维数组、结构体Filter-more⭐⭐⭐⭐卷积运算、Sobel 边缘检测Recove…Problem Set 4 概述本周的作业围绕内存主题展开包含音频处理、图像滤镜和文件恢复三类题目题目难度核心知识点Volume⭐文件 I/O、二进制数据处理Filter-less⭐⭐⭐图像处理、二维数组、结构体Filter-more⭐⭐⭐⭐卷积运算、Sobel 边缘检测Recover⭐⭐⭐文件签名、状态机、FAT 文件系统作业页面https://cs50.harvard.edu/x/psets/4/1. Volume - 音频音量调节题目链接https://cs50.harvard.edu/x/psets/4/volume/问题背景WAV files are a common file format for representing audio. WAV files store audio as a sequence of samples: numbers that represent the value of some audio signal at a particular point in time. WAV files begin with a 44-byte header that contains information about the file itself, including the size of the file, the number of samples per second, and the size of each sample. After the header, the WAV file contains a sequence of samples, each a single 2-byte (16-bit) integer representing the audio signal at a particular point in time.从这段描述我们可以得出1. WAV 文件格式WAV 文件由两部分组成部分位置内容Header文件头前 44 字节文件元数据采样率、声道数等Audio Data音频数据44 字节之后实际的音频样本2. 音频采样Audio Samples音频被存储为一系列的数字样本每个样本是一个16-bit signed integer2 字节范围 -32768 到 32767样本值的绝对值越大声音越响实现C 语言知识点我们需要用到的数据类型和函数stdint.h- 固定宽度整数类型int16_t // 16位有符号整数正好是一个音频样本的大小 uint8_t // 8位无符号整数用于读取字节文件操作函数fopen() // 打开文件 fread() // 读取文件 fwrite() // 写入文件 fclose() // 关闭文件实现思路看课程提供的 TODO// TODO: Copy header from input file to output file // TODO: Read samples from input file and write updated data to output file我们需要做两件事复制文件头和处理音频样本。步骤 1: 复制文件头44 字节文件头不需要修改直接从输入文件复制到输出文件// 创建一个 44 字节的缓冲区 uint8_t header[HEADER_SIZE]; // 从输入文件读取 header fread(header, HEADER_SIZE, 1, input); // 写入到输出文件 fwrite(header, HEADER_SIZE, 1, output);步骤 2: 处理音频样本逐个读取样本 → 乘以缩放因子 → 写入输出文件int16_t buffer; // 存储一个样本2 字节 // 循环读取所有样本直到文件末尾 while (fread(buffer, sizeof(int16_t), 1, input) 1) { // 将样本值乘以缩放因子 buffer buffer * factor; // 写入到输出文件 fwrite(buffer, sizeof(int16_t), 1, output); }2. Filter-less - 图像滤镜基础版题目链接https://cs50.harvard.edu/x/psets/4/filter/less/问题背景处理 BMP 图片核心任务是对 BMP 图片应用 4 种滤镜效果选项滤镜效果-gGrayscale灰度转为黑白-sSepia深褐色复古效果-rReflect翻转水平镜像-bBlur模糊柔化图像例如./filter -g input.bmp output.bmp # 应用灰度滤镜理解文件结构项目文件结构 ├── bmp.h → BMP 文件格式定义数据结构 ├── helpers.h → 函数声明接口 ├── helpers.c → 你要实现的函数核心逻辑 └── filter.c → 主程序已完成读取/写入文件bmp.h- 关键数据结构BITMAPFILEHEADER和BITMAPINFOHEADER: BMP 图像的 header 信息长度都是固定的。RGBTRIPLE: 这是本题的关键数据结构表示一个像素的颜色typedef struct { BYTE rgbtBlue; // 蓝色分量 (0-255) BYTE rgbtGreen; // 绿色分量 (0-255) BYTE rgbtRed; // 红色分量 (0-255) } RGBTRIPLE;即我们所熟知的 RGB 三原色注意BMP 文件中存储顺序是 BGR。每个像素由 RGB 三原色组成每个颜色分量范围0无到 255最亮使用BYTEuint8_t表示例如纯红色 {.rgbtBlue 0, .rgbtGreen 0, .rgbtRed 255}白色 {255, 255, 255}黑色 {0, 0, 0}访问某个像素的红色分量image[i][j].rgbtRedfilter.c- 主程序流程该文件是 filter-less 题目的主文件课程已经提供了从读取图像文件到滤镜后输出结果文件的整个过程我们要做的就是分别实现前面提到的 4 种滤镜效果功能。我根据代码简单说下 filter 的流程不必强行理解1. 打开文件与读取元数据打开文件程序打开输入的.bmp文件读取头信息先用fread读取 14 字节到bf文件头再用fread读取 40 字节到bi信息头此时程序知道了图片的宽 (bi.biWidth) 和高 (bi.biHeight)2. 分配内存为了处理图片程序必须把图片从磁盘搬到**内存RAM**里。可以理解为创作图片准备画布// 创建一个二维数组 image[height][width] RGBTRIPLE(*image)[width] calloc(height, width * sizeof(RGBTRIPLE));3. 以像素为单位读取数据可以理解为填充画布。程序使用一个for循环一行一行地读取数据读取像素fread(image[i], ...)把文件里这一行的颜色数据直接填入刚刚申请的内存image[i]中跳过 PaddingBMP 格式要求每一行的字节数必须是 4 的倍数。如果图片的宽度不够 4 的倍数文件里会在行尾填充 0。程序计算出这个 padding并用fseek跳过它bmp.h中使用__attribute__((__packed__))是告诉编译器不需要对结构体做内存对齐。4. Filter这部分是我们需要做的工作。你可以无需理解前面的部分即不必知道文件是哪里来的、怎么变成像素的。你就想象你手里已经有了一个Excel 表格一样的二维数组image里面全是颜色。你的任务就是遍历这个表格修改里面的数字然后收工。需要实现的四个滤镜函数函数声明在helpers.h中我们需要在helpers.c中实现它们。1. Grayscale灰度目标将彩色图片转为黑白图片原理如果一张图片的 RGB 值全部设置为0x00图片则为黑色设置为0xff则为白色RGB 三原色值完全相等时最终呈现的将是黑白色谱中不同深浅的灰色调数值越高 → 更浅的灰色更接近白色数值越低 → 更深的灰色更接近黑色因此将彩色图片转换为黑白图片只需要确保图片的 RGB 三原色值相等为确保新图像的每个像素仍保持与旧图像相同的整体明暗度可取RGB 三色值的平均值来确定新像素应呈现的灰度值核心代码RGBTRIPLE pixel image[i][j]; int average round((pixel.rgbtRed pixel.rgbtGreen pixel.rgbtBlue) / 3.0); // 将 RGB 都设为平均值 image[i][j].rgbtRed average; image[i][j].rgbtGreen average; image[i][j].rgbtBlue average;⚠️注意(R G B) / 3在 C 语言中如果是整数相除会丢弃小数。需要除以3.0算出精确值并用round()四舍五入。2. Sepia深褐色/复古目标给图片添加复古效果公式sepiaRed .393 * originalRed .769 * originalGreen .189 * originalBlue sepiaGreen .349 * originalRed .686 * originalGreen .168 * originalBlue sepiaBlue .272 * originalRed .534 * originalGreen .131 * originalBlue注意事项四舍五入为整数使用round()保证在[0, 255]范围内如果计算结果 255必须强制设为 255Clamping3. Reflect水平翻转目标图片左右镜像翻转实现思路交换每一行的左右像素交换次数为width / 2中间的像素不需要交换核心代码// 临时变量保存左边像素 RGBTRIPLE temp image[i][j]; // 左边 右边 image[i][j] image[i][width - 1 - j]; // 右边 原来的左边 image[i][width - 1 - j] temp;4. Blur模糊- 最难的一关目标让图像变模糊原理Box Blur每个像素的新颜色 它自己周围3×3 九宫格内所有像素包括它自己的平均值核心难点 1原始数据的破坏如果你直接修改image[0][0]那么当你接着计算image[0][1]的平均值时你会用到已经修改过的image[0][0]而不是原始值。这样模糊效果会偏移产生伪影Artifacts。解决方法先创建一个副本数组RGBTRIPLE copy[height][width]把image里的数据完全复制到copy读取copy来计算平均值把计算好的新值写入image核心难点 2边界处理Edge Cases像素位置有效邻居数中间9 个包括自己边缘6 个角落4 个解决方法不要写无数个if来判断是角落还是边缘。使用嵌套循环遍历邻居在循环中检查边界。核心代码逻辑以 Red 通道为例int sumRed 0; int counter 0; // 遍历当前像素(i, j) 周围从 -1 到 1 的范围 for (int di -1; di 1; di) { for (int dj -1; dj 1; dj) { int ni i di; int nj j dj; // 检查 ni 和 nj 是否越界 if (ni 0 ni height nj 0 nj width) { sumRed copy[ni][nj].rgbtRed; counter; // 记录加了几个有效像素 } } } image[i][j].rgbtRed round(sumRed / (float)counter);完整的helpers.c在 filter-less 目录中。3. Filter-more - 图像滤镜进阶版题目链接https://cs50.harvard.edu/x/psets/4/filter/more/除了Edges边缘检测部分其它与 filter-less 相同不再赘述。Edges - 边缘检测与 Blur 类似Edges 也是对 3×3 邻域进行运算。但 Blur 是求平均值而 Edges 使用**卷积核Kernel**进行加权求和。如果你了解过神经网络的卷积Convolution就会对这个不陌生——只是这里的卷积核的值是固定的而神经网络中是训练出来的。理解问题什么是边缘检测Edges边缘检测的操作和神经网络中的卷积Convolution是一样的。事实上Sobel 算子本题使用的算法就是计算机视觉中最古老、最基础的卷积核。在现代的卷积神经网络CNN中网络是通过训练学出来的卷积核而在本题中我们直接使用两个**硬编码Hard-coded**的经典卷积核来提取特征。一、题目分析Sobel 算子这道题的目标是让图像中颜色变化剧烈的地方变亮边缘颜色平坦的地方变暗。它使用了两个 3×3 的矩阵卷积核1. Gx检测垂直边缘从左到右扫描左边是负数右边是正数-1 0 1 -2 0 2 -1 0 1如果左边暗数值小右边亮数值大结果为正差异大如果左右颜色一样抵消为 0无边缘2. Gy检测水平边缘从上到下扫描上面是负数下面是正数-1 -2 -1 0 0 0 1 2 1核心逻辑对于图片中的每一个像素以它为中心取周围 3×3 的范围分别让这 9 个像素与Gx做加权求和点积得到 ValxValx分别让这 9 个像素与Gy做加权求和得到 ValyValy利用勾股定理合并两个方向的梯度MagnitudeValx2Valy2MagnitudeValx2Valy2这就是该像素新的颜色值二、思考方案Mental Model面对比 filter-less 更复杂的逻辑我们需要更严谨的步骤1. 数据的独立性The Copy Problem和 Blur 一样Sobel 算法严重依赖周围像素的原始值。如果你算完了第一行第一列的像素并修改了它当你算第一行第二列时你会用到第一列的新值这会导致误差像雪崩一样扩散解决方案必须创建RGBTRIPLE copy[height][width]全程从copy读数据把结果写入image。2. 边界处理Zero Padding题目规定如果像素在图像边缘超出图像的部分视为纯黑色RGB 均为 0。在神经网络中这叫做Zero Paddingpaddingsame解决方案不要真正地去给数组扩容太麻烦。在循环读取邻居时检查坐标是否越界。如果越界直接认为该邻居的值是 0或者直接跳过不加效果相同。3. 颜色通道独立红、绿、蓝三个通道必须分开计算。你需要算出Gx_red,Gy_red,Gx_green... 等共 6 个中间值最后算出 3 个最终值。三、解决方案与代码结构为了写出整洁的代码不要写一大堆if语句去判断左上角、右下角等 9 种情况。我们要使用滑动窗口的思维。步骤 1: 初始化卷积核为了代码好写先把 Gx 和 Gy 定义成二维数组int Gx[3][3] {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}}; int Gy[3][3] {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};步骤 2: 复制图像RGBTRIPLE copy[height][width]; for (int i 0; i height; i) { for (int j 0; j width; j) { copy[i][j] image[i][j]; } }步骤 3: 遍历每一个像素主循环for (int i 0; i height; i) { for (int j 0; j width; j) { // 这里我们要处理 image[i][j] // 需要变量来累加 Gx 和 Gy 的值 float Gx_red 0, Gx_green 0, Gx_blue 0; float Gy_red 0, Gy_green 0, Gy_blue 0; // ... 接下来进入卷积循环 ... } }步骤 4: 卷积循环核心难点我们需要遍历当前像素周围 3×3 的邻居。这就需要再套两层循环偏移量di和dj从 -1 到 1。// 遍历 3x3 网格 for (int di -1; di 1; di) { for (int dj -1; dj 1; dj) { int ni i di; int nj j dj; // 检查边界只有在图片范围内的像素才算数 // 超出范围的像素视为 0既然是 0加了也没用直接 continue 跳过即可 if (ni 0 ni height nj 0 nj width) { // 获取权重注意 Gx 数组索引是 0,1,2而 di 是 -1,0,1 // 所以索引应该是 [di 1][dj 1] int weightX Gx[di 1][dj 1]; int weightY Gy[di 1][dj 1]; // 累加 Red Gx_red copy[ni][nj].rgbtRed * weightX; Gy_red copy[ni][nj].rgbtRed * weightY; // ... 同理累加 Green 和 Blue ... } } }步骤 5: 合并与封顶Finalize循环结束后你有了Gx_red和Gy_red。现在应用勾股定理// 1. 计算 magnitude int finalRed round(sqrt(Gx_red * Gx_red Gy_red * Gy_red)); // 2. 防止溢出 (Clamping) // 经过卷积运算结果可能会变成很大的数比如 1000必须限制在 255 if (finalRed 255) { finalRed 255; } // ... 同理处理 Green 和 Blue ... // 3. 写入原图 image[i][j].rgbtRed finalRed; // ...四、关键点总结要点说明数据类型累加 Gx 和 Gy 时结果可能是负数也可能很大所以中间变量建议用float或int推荐float因为之后要开根号绝对不要用BYTE否则会溢出回绕索引偏移将物理坐标(di, dj)从{-1, 0, 1}映射到数组索引{0, 1, 2}时记得1头文件用到sqrt和round别忘了在文件头部确认有#include math.hZero Padding通过if检查边界合法的就加非法的边界外就不加相当于加 0这完美符合题目要求这就是最标准的卷积实现。写完这个实际上你手搓了一个微型的卷积神经网络层Conv Layer只是这个卷积核的值是固定的。完整代码在 filter-more 的 中help.c中#include helpers.h #include math.h // Convert image to grayscale void grayscale(int height, int width, RGBTRIPLE image[height][width]) { for (int i 0; i height; i) { for (int j 0; j width; j) { RGBTRIPLE pixel image[i][j]; int average round((pixel.rgbtRed pixel.rgbtGreen pixel.rgbtBlue) / 3.0); image[i][j].rgbtRed average; image[i][j].rgbtGreen average; image[i][j].rgbtBlue average; } } return; } // Reflect image horizontally void reflect(int height, int width, RGBTRIPLE image[height][width]) { for (int i 0; i height; i) { for (int j 0; j width / 2; j) { RGBTRIPLE temp image[i][j]; image[i][j] image[i][width - j - 1]; image[i][width - j - 1] temp; } } return; } // Blur image void blur(int height, int width, RGBTRIPLE image[height][width]) { RGBTRIPLE copy[height][width]; for (int i 0; i height; i) { for (int j 0; j width; j) { copy[i][j] image[i][j]; } } for (int i 0; i height; i) { for (int j 0; j width; j) { int sumRed 0, sumGreen 0, sumBlue 0; int counter 0; for (int di -1; di 1; di) { for (int dj -1; dj 1; dj) { int ni i di; int nj j dj; if (ni 0 ni height nj 0 nj width) { sumRed copy[ni][nj].rgbtRed; sumGreen copy[ni][j].rgbtGreen; sumBlue copy[ni][nj].rgbtBlue; counter; } } } image[i][j].rgbtRed round((float)sumRed / counter); image[i][j].rgbtGreen round((float)sumGreen / counter); image[i][j].rgbtBlue round((float)sumBlue / counter); } } return; } // Detect edges void edges(int height, int width, RGBTRIPLE image[height][width]) { int Gx[3][3] {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}}; int Gy[3][3] {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}}; RGBTRIPLE copy[height][width]; for (int i 0; i height; i) { for (int j 0; j width; j) { copy[i][j] image[i][j]; } } for (int i 0; i height; i) { for (int j 0; j width; j) { float Gx_red 0, Gx_green 0, Gx_blue 0; float Gy_red 0, Gy_green 0, Gy_blue 0; for (int di -1; di 1; di) { for (int dj -1; dj 1; dj) { int ni i di; int nj j dj; if (ni 0 ni height nj 0 nj width) { int weightX Gx[di 1][dj 1]; int weightY Gy[di 1][dj 1]; Gx_red copy[ni][nj].rgbtRed * weightX; Gx_green copy[ni][nj].rgbtGreen * weightX; Gx_blue copy[ni][nj].rgbtBlue * weightX; Gy_red copy[ni][nj].rgbtRed * weightY; Gy_green copy[ni][nj].rgbtGreen * weightY; Gy_blue copy[ni][nj].rgbtBlue * weightY; } } } int finalRed round(sqrt(Gx_red * Gx_red Gy_red * Gy_red)); int finalGreen round(sqrt(Gx_green * Gx_green Gy_green * Gy_green)); int finalBlue round(sqrt(Gx_blue * Gx_blue Gy_blue * Gy_blue)); if (finalRed 255) finalRed 255; if (finalGreen 255) finalGreen 255; if (finalBlue 255) finalBlue 255; image[i][j].rgbtRed finalRed; image[i][j].rgbtGreen finalGreen; image[i][j].rgbtBlue finalBlue; } } return; }4. Recover - JPEG 文件恢复题目链接https://cs50.harvard.edu/x/psets/4/recover/问题背景题目是从内存卡中恢复 JPEG 图片。与前面 Filter 的区别题目类型FilterRecover领域图像处理Image Processing数字取证Digital Forensics输入完整的图像数据一堆无序的二进制数据Raw Data任务修改像素值从中挖掘出有用的文件这道题考察的是你对文件 I/O输入/输出、内存块Buffer以及状态管理的理解。一、核心概念分析1. JPEG 的签名Signatures计算机怎么知道这堆数据是 JPEG 还是 Word 文档靠文件头Headers。JPEG 文件的前 4 个字节是固定的0xff 0xd8 0xff 0xe?其中第 4 个字节可以是0xe0,0xe1,0xe2... 直到0xef。这意味着第 4 个字节的高 4 位必须是1110即十六进制的e。2. 块Block与 FAT 文件系统题目告诉我们这张存储卡使用的是FAT 文件系统。FAT 系统的一个关键特性是它以512 字节为单位存储数据。这意味着我们不需要一个字节一个字节地读我们应该一次读取512 个字节到一个缓冲区Buffer中所有的 JPEG 照片也是以 512B 对齐的开头一定在某一个 512 字节块的起始位置3. 连续存储假设题目为了简化难度假设卡上的照片是连续存储的。也就是说一旦你发现了一个 JPEG 的开头接下来的数据直到发现下一个 JPEG 开头之前都属于这张照片。二、思考方案建立状态机模型这道题最容易写乱的地方在于什么时候打开文件什么时候写入什么时候关闭最好的思考方式是建立一个简单的状态机。根据前面的条件分析我们每次都是读取一个块512B因此需要一个循环不断地从card.raw里读取 512 字节。对于每一个块只有两种情况情况 A这 512 字节是某个新 JPEG 的开头Header特征前 4 个字节符合 JPEG 签名操作如果之前已经打开了一个输出文件说明上一张照片已经读取完毕关闭它创建一个新文件名比如000.jpg,001.jpg打开这个新文件写入 512B 到这个新文件情况 B这 512 字节不是开头是照片的数据部分或者是垃圾数据特征前 4 个字节不符合 JPEG 签名操作如果当前有正在打开的文件说明我们正在恢复一张图片将这 512 字节写入文件如果当前没有打开的文件说明我们还没找到第一张图或者是两张图之间的垃圾数据直接忽略/丢弃它继续读下一个块状态机示意图┌────────────┐ │ START │ (没有打开的文件) └─────┬──────┘ │ │ 发现 JPEG 签名 ↓ ┌────────────┐ │ WRITING │ (正在写入文件) └─────┬──────┘ │ │ 发现新 JPEG 签名 ↓ ┌────────────┐ │ CLOSE_OLD │ (关闭旧文件) │ OPEN_NEW │ (打开新文件) └─────┬──────┘ │ └──────→ 回到 WRITING三、解决方案与代码实现细节1. 定义数据类型为了方便定义一个BYTE类型或者直接用uint8_t需要stdint.htypedef uint8_t BYTE;2. 准备缓冲区需要一个可以存储 512 字节的数组BYTE buffer[512];3. 主循环逻辑使用fread循环读取。fread的返回值是成功读取的项数。FILE *input fopen(argv[1], rb); // 注意使用 rb二进制读模式 FILE *output NULL; // 这是一个关键指针用来指向当前正在写的 JPEG char filename[8]; // 用来存 000.jpg \0 int counter 0; // 记录找到了几张图 // 核心循环只要还能读到 512 字节就继续 // fread 返回成功读取的项数这里我们要求读 512 个 BYTE // 如果成功读到 512 个返回 512读到末尾不够 512 个返回实际读取数量 while (fread(buffer, sizeof(BYTE), 512, input) 512) { // 检查是不是 JPEG 开头 // 第四个字节的位运算解释(buffer[3] 0xf0) 保留高4位把低4位清零 // 如果高4位是 1110 (0xe)结果就是 0xe0 if (buffer[0] 0xff buffer[1] 0xd8 buffer[2] 0xff (buffer[3] 0xf0) 0xe0) { // 1. 如果之前已经在写一张图了先关掉它 if (output ! NULL) { fclose(output); } // 2. 生成新文件名 sprintf(filename, %03i.jpg, counter); // 3. 打开新文件注意使用 wb二进制写模式 output fopen(filename, wb); // 4. 计数器 1 counter; } // 核心写入逻辑 // 只有当 output 不为 NULL 时才写入 // 这巧妙地避开了第一张图之前的垃圾数据因为那时 output 是 NULL if (output ! NULL) { fwrite(buffer, sizeof(BYTE), 512, output); } }4. 关闭所有文件// 关闭最后打开的那个输出文件 —— 循环结束时它肯定还开着 if (output ! NULL) { fclose(output); } // 关闭输入文件 fclose(input);四、常见坑点与注意事项这里常见错误是我写完后让AI帮我review,AI指出我的问题我一并放在这里了。1. 文件模式错误写法正确写法说明fopen(file, r)fopen(file, rb)读取二进制文件必须用rbfopen(file, w)fopen(file, wb)写入二进制文件必须用wb在 Windows 系统上如果不使用b模式可能会导致数据损坏2. fread 返回值// ❌ 错误fread 返回的是成功读取的项数不是 1 while (fread(buffer, sizeof(BYTE), 512, input) 1) // ✅ 正确我们请求读取 512 个 BYTE成功时返回 512 while (fread(buffer, sizeof(BYTE), 512, input) 512)3. 位运算理解(buffer[3] 0xf0) 0xe0是 C 语言中处理二进制标志位的常用技巧0xf0是1111 0000操作会让buffer[3]的低 4 位变成 0这样我们只检查高 4 位是不是e而不管低 4 位是0,1... 还是f4.sprintf的用法sprintf(string, format, variables)是把打印的内容输出到一个字符串里而不是屏幕上。%03i表示打印整数至少占 3 位不足补 0。比如0会变成000,15会变成015。5. 内存泄漏与文件指针确保malloc的都有free虽然这题基本不需要malloc都在栈上确保fopen的都有fclose这题最大的 Bug 源头通常是忘记关闭上一张图片导致所有图片的数据都写到了第一个文件里或者文件打不开。完整代码#include stdio.h #include stdlib.h #include stdint.h // define a new type for uint8_t typedef uint8_t BYTE; int main(int argc, char *argv[]) { // Check if the number of arguments is valid if (argc ! 2) { printf(Usage: ./recover FILE\n); return 1; } // open the raw file (binary read mode) FILE *input fopen(argv[1], rb); if (input NULL) { printf(Could not open the file!\n); return 1; } // define key variables FILE *output NULL; // output file BYTE buffer[512]; // A buffer array with 512B char filename[8]; // using store image with the name like 000.jpg \0 int counter 0; // the number of images while (fread(buffer, sizeof(BYTE), 512, input) 512) { // Check the JPEG signature if (buffer[0] 0xff buffer[1] 0xd8 buffer[2] 0xff (buffer[3] 0xf0) 0xe0) // 第四个字节使用位运算检查高四位是否是1110即0xe { if (output ! NULL) { // Close it if it is writint an image fclose(output); } // Generate a new image sprintf(filename, %03i.jpg, counter); output fopen(filename, wb); if (output NULL) { fclose(input); printf(Could not create file.\n); return 1; } counter; } if (output ! NULL) { fwrite(buffer, sizeof(BYTE), 512, output); } } // Close all files if (output ! NULL) { fclose(output); } fclose(input); return 0; }总结做 Recover 这道题时把自己想象成一个传送带旁边的分拣员传送带每次送来一个盒子512 字节先看盒子开头有没有印着JPEG Start的特殊标记如果有标记 → 封上旧箱子拿个新箱子开始装如果没有标记 → 如果手里有正在装的箱子就往里倒如果手里没箱子就把盒子扔了一直重复直到传送带停下完整的代码在 recover 目录下的 recover.c知识点对照表题目核心知识点课程对应内容Volume文件 I/O、固定宽度整数Week 4: File I/OFilter-less图像处理、二维数组Week 4: Memory, StructsFilter-more卷积运算、边缘检测Week 4: Memory (进阶)Recover文件签名、状态机Week 4: File I/O调试技巧使用 printf 调试在关键位置打印变量值xxd 查看二进制xxd card.raw | head查看文件头check50 测试check50 cs50/problems/2024/x/recover参考资料CS50 Problem Set 4 官方页面BMP 文件格式详解JPEG 文件格式