恢复

要解决的问题
为了准备这道题,我们过去几天在校园里拍摄了许多照片,所有这些照片都以 JPEG 格式保存在数码相机的存储卡上。不幸的是,我们不知怎么把它们全删了!谢天谢地,在计算机世界中,”已删除”的意思往往不是”已删除”,而更像是”被遗忘”。尽管相机坚持说存储卡现在是空的,但我们相当确定这不完全是真的。确实,我们希望(呃,期望!)你能编写一个程序来为我们恢复这些照片!
在名为 recover 的文件夹中的 recover.c 文件中,编写一个程序来从存储卡中恢复 JPEG 图片。
Distribution Code
对于这个问题,你将扩展由 CS50 工作人员提供的代码功能。
下载分发代码
登录 cs50.dev,点击你的终端窗口,然后执行 cd 本身。你应该会发现你的终端窗口的提示符类似于下面所示:
$
接下来执行
wget https://cdn.cs50.net/2026/x/psets/4/recover.zip
以便将一个名为 recover.zip 的 ZIP 文件下载到你的 codespace 中。
然后执行
unzip recover.zip
来创建一个名为 recover 的文件夹。你不再需要这个 ZIP 文件,因此你可以执行
rm recover.zip
在提示符处回复”y”然后按回车键,删除你刚下载的 ZIP 文件。
现在输入
cd recover
然后按回车键,进入(即打开)该目录。你的提示符现在应类似于以下内容。
recover/ $
单独执行 ls,你应该会看到两个文件:recover.c 和 card.raw。
背景
尽管 JPEG 比 BMP 复杂得多,但 JPEG 有”签名”——可以将其与其他文件格式区分开来的字节模式。具体来说,JPEG 的前三个字节是
0xff 0xd8 0xff
从左到右,从第一个字节到第三个字节。与此同时,第四个字节可以是 0xe0、0xe1、0xe2、0xe3、0xe4、0xe5、0xe6、0xe7、0xe8、0xe9、0xea、0xeb、0xec、0xed、0xee 或 0xef 中的一个。换句话说,第四个字节的前四个比特是 1110。
很有可能,如果你在已知存储照片的介质(例如我的存储卡)上发现这四个字节的模式,它们就标示着 JPEG 的开始。公平地说,你可能纯粹偶然地在某个磁盘上遇到这些模式,因此数据恢复并非一门精确科学。
幸运的是,数码相机倾向于将照片连续存储在存储卡上,即每张照片都紧接在前一张拍摄的照片之后存储。因此,一个 JPEG 的开始通常标志着另一个 JPEG 的结束。然而,数码相机通常使用 FAT 文件系统初始化存储卡,其”块大小”为 512 字节(B)。这意味着这些相机只以 512 B 为单位向这些存储卡写入数据。因此,一张 1 MB(即 1,048,576 B)的照片在存储卡上占用 1048576 ÷ 512 = 2048 个”块”。但一张小一个字节的照片(即 1,048,575 B)也是如此!磁盘上浪费的空间称为”松弛空间”。法医调查人员经常检查松弛空间以寻找可疑数据的残留。
所有这些细节的含义是,你(调查员)可能可以编写一个程序来遍历我的存储卡副本,寻找 JPEG 的签名。每当你找到签名时,你就可以打开一个新文件进行写入,并开始用我的存储卡中的字节填充该文件,只有当你遇到另一个签名时才关闭该文件。此外,你不必一次读取一个字节的存储卡数据,而可以一次读取 512 个字节到缓冲区中以提高效率。由于 FAT 的存在,你可以确信 JPEG 的签名将是”块对齐”的。也就是说,你只需要在一个块的前四个字节中查找这些签名。
当然,要认识到 JPEG 可以跨越连续的块。否则,没有 JPEG 可以大于 512 B。但 JPEG 的最后一个字节可能不会恰好落在块的末尾。回想一下松弛空间的可能性。但不用担心。因为这张存储卡在我开始拍照时是全新的,制造商很可能已经将其”清零”(即用 0 填充),在这种情况下,任何松弛空间都将被 0 填充。那些末尾的 0 出现在你恢复的 JPEG 中没有关系;它们仍然应该是可查看的。
现在,我只有一张存储卡,但你们有很多人!因此,我创建了这张卡的”法医镜像”,将其内容逐字节地存储在一个名为 card.raw 的文件中。为了不让你不必要地浪费时间遍历数百万个 0,我只镜像了存储卡的前几兆字节。但你最终应该会发现该镜像包含 50 个 JPEG。
规格说明
实现一个名为 recover 的程序,从法医镜像中恢复 JPEG 图片。
- 在名为
recover的目录中的recover.c文件中实现你的程序。 - 你的程序应接受恰好一个命令行参数,即从中恢复 JPEG 的法医镜像的名称。
- 如果程序未以恰好一个命令行参数执行,它应提醒用户正确的用法,并且
main应返回1。 - 如果法医镜像无法打开以进行读取,你的程序应告知用户这一情况,并且
main应返回1。 - 你生成的文件应各自命名为
###.jpg,其中###是一个三位十进制数字,从第一个图片的000开始递增计数。 - 你的程序如果使用
malloc,则不得泄漏任何内存。
提示
在编写更多代码之前先写一些伪代码
如果不知道如何解决较大的问题,可以将其分解为你可能可以先解决的小问题。例如,这道题实际上只是少数几个问题:
- 接受一个命令行参数:存储卡的名称
- 打开存储卡
- 当存储卡中仍有数据可读时
- 从数据中创建 JPEG
让我们写一些伪代码作为注释,提醒你做这些事情:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Accept a single 命令行参数
// Open the memory card
// While there's still data left to read from the memory card
// Create JPEGs from the data
}
将伪代码转换为代码
首先,考虑如何接受一个命令行参数。如果用户误用了程序,你应该告诉他们程序的正确用法。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Accept a single 命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// Open the memory card
// While there's still data left to read from the memory card
// Create JPEGs from the data
}
现在你已经检查了正确的用法,可以打开存储卡了。请记住,你可以使用 fopen 以编程方式打开 card.raw,如下所示。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Accept a single 命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// Open the memory card
FILE *card = fopen(argv[1], "r");
// While there's still data left to read from the memory card
// Create JPEGs from the data
}
当然,你应该检查文件是否正确打开!如果没有,请告知用户并退出程序:这部分我们留给你来完成。
接下来,你的程序应该从你打开的存储卡中读取数据,直到没有数据可读为止。在此过程中,你的程序应该从 card.raw 中恢复每个 JPEG,将每个 JPEG 作为单独的文件存储在你的当前工作目录中。
首先考虑如何从头到尾读取 card.raw。回想一下,要从文件中读取数据,你需要将数据临时存储在一个”缓冲区”中。再回想一下,card.raw 以 512 字节的块存储数据。因此,你可能需要创建一个 512 字节的缓冲区,以便在顺序读取数据时存储数据块。一种方法是使用 stdint.h 中的 uint8_t 类型,该类型精确存储 8 比特(1 字节)。该类型之所以称为 uint8_t,是因为它存储一个需要 8 比特空间(即一个字节)的无符号/正/非负整数。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Accept a single 命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// Open the memory card
FILE *card = fopen(argv[1], "r");
// Create a buffer for a block of data
uint8_t buffer[512];
// While there's still data left to read from the memory card
// Create JPEGs from the data
}
不过,在这里使用 512 作为”魔术数字”可能不是最好的做法。很有可能你可以进一步改进这个设计!
现在,考虑如何从存储卡中读取数据。根据其手册页面,fread 返回它已读取的字节数,鉴于 card.raw 包含一定数量的 512 字节块,它应该返回 512 或 0。为了从 card.raw 中读取每个块,在用 fopen 打开后,使用如下循环应该就足够了。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Accept a single 命令行参数
if (argc != 2)
{
printf("Usage: ./recover FILE\n");
return 1;
}
// Open the memory card
FILE *card = fopen(argv[1], "r");
// Create a buffer for a block of data
uint8_t buffer[512];
// While there's still data left to read from the memory card
while (fread(buffer, 1, 512, card) == 512)
{
// Create JPEGs from the data
}
}
这样,一旦 fread 返回 0(这实际上就是 false),你的循环就会结束。
最后,由你来决定如何在继续从 card.raw 读取数据的同时以编程方式创建 JPEG。为此,你可能会发现下面的讲解视频对你很有用。
请记住,你的程序应该通过将每个文件命名为 ###.jpg 来为其输出的文件编号,其中 ### 是从 000 开始递增的三位十进制数字。请熟悉 sprintf,并注意 sprintf 在内存中的某个位置存储一个格式化字符串。考虑到 JPEG 文件名的规定格式 ###.jpg,你应该为这个字符串分配多少字节?(别忘记 NUL 字符!)
要检查你的程序生成的 JPEG 是否正确,只需双击查看一下!如果每张照片看起来都完整无损,你的操作很可能成功了!
当然,请记得对用 fopen 打开的每个文件调用 fclose!
保持工作目录整洁
你的代码初稿生成的 JPEG 很可能是不正确的。(如果你打开它们却看不到任何东西,它们很可能不正确!)执行以下命令来删除当前工作目录中的所有 JPEG。
rm *.jpg
如果你不想被提示确认每次删除,请改为执行下面的命令。
rm -f *.jpg
不过要小心使用 -f 开关,因为它会在不提示你的情况下”强制”删除。
讲解视频
如何测试
运行程序
./recover card.raw
正确性
check50 cs50/problems/2026/x/recover
代码风格
style50 recover.c
如何提交
在终端中执行以下命令提交你的作业,并按提示完成操作。
submit50 cs50/problems/2026/x/recover