CS50 Pset4 Recover任务:JPEG识别计数异常求助
问题背景
正在完成CS50课程Pset4的Recover任务,需要从课程提供的card.raw文件中识别隐藏的JPEG文件。已知该文件恰好包含50个JPEG,但编写的统计程序输出计数结果为860,明显不符合预期。
JPEG文件的签名规则:
- 前三个字节为
0xff 0xd8 0xff - 第四个字节十六进制值介于
0xe0到0xef之间(即前4位为1110,对应十进制14)
原代码
#include <stdio.h> #include <stdlib.h> #include <string.h> // Variable used for bits reading when looking for JPEGs files int msb = 8, lsb = 4; int main(int argc, char *argv[]) { // Find JPEG char *temp = malloc(3); int *temp1 = malloc(1); int counter = 0; while (fread(temp, 3, 1, card)) { if(strcmp(temp, "0xff 0xd8 0xff")) { fread(temp1, 1, 1, card); int result = (*temp1 >> lsb) & ~(~0 << (msb-lsb+1)); if(result == 14) { fseek(card, 508, SEEK_CUR); counter++; } } } printf("counter = %i\n", counter); }
问题排查与修复
你的核心逻辑方向是对的,但代码里有几个关键错误导致计数严重偏高:
1. 错误使用strcmp匹配二进制签名
strcmp是用来比较ASCII字符串的,但你用它来对比temp里存储的二进制字节数据,这完全不匹配。比如字符串"0xff 0xd8 0xff"是由'0'、'x'、'f'等ASCII字符组成的,和JPEG签名的二进制字节0xff、0xd8、0xff没有任何关系。
同时,strcmp的返回值是0表示两个字符串相等,你写的if(strcmp(...))会在不相等的时候执行代码块,逻辑完全反了。
修复方式:直接逐个字节对比二进制值:
if (temp[0] == 0xff && temp[1] == 0xd8 && temp[2] == 0xff)
2. 未定义文件指针card
代码里直接使用了card这个文件指针,但既没有通过fopen打开文件,也没有定义这个变量。必须先检查命令行参数,然后打开文件:
if (argc != 2) { fprintf(stderr, "Usage: %s image\n", argv[0]); return 1; } FILE *card = fopen(argv[1], "rb"); if (card == NULL) { fprintf(stderr, "Could not open %s.\n", argv[1]); return 1; }
注意要以二进制模式"rb"打开,避免系统自动转换换行符干扰二进制数据。
3. 循环读取逻辑错误
当前循环每次读取3字节,如果这3字节不是JPEG签名,文件指针会直接前进3字节,这会跳过中间可能存在的签名位置(比如签名的起始位置在第2字节,你读了1-3字节后,下次读4-6字节,就错过了2-4字节的签名)。
正确的做法是:不匹配时回退2字节,让下次循环从下一个字节开始读取:
// 不匹配时,回退2字节 fseek(card, -2, SEEK_CUR);
4. 第四个字节的判断可以简化
你用移位和掩码的方式判断前4位,虽然逻辑正确,但可以更直观:直接用掩码0xf0提取前4位,判断是否等于0xe0:
if ((*temp1 & 0xf0) == 0xe0)
5. 不必要的动态内存分配
对于3字节和1字节的小缓冲区,完全不需要用malloc,直接用栈上的数组/变量即可,避免内存泄漏:
unsigned char temp[3]; // 用unsigned char避免符号位干扰 unsigned char temp1;
6. fseek的错误使用
你找到签名后跳过508字节,假设每个JPEG占512字节,但实际上JPEG可能由多个512字节块组成,而且这样跳转会因为错误匹配的假签名被计数,导致总数偏高。如果只是计数,应该继续从当前位置往后找下一个签名,而非直接跳转(示例代码中保留跳转仅为适配你当前的计数需求,实际恢复JPEG时不能这么做)。
修正后的示例代码
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { // 检查命令行参数 if (argc != 2) { fprintf(stderr, "Usage: %s image\n", argv[0]); return 1; } // 打开文件 FILE *card = fopen(argv[1], "rb"); if (card == NULL) { fprintf(stderr, "Could not open %s.\n", argv[1]); return 1; } unsigned char temp[3]; unsigned char temp1; int counter = 0; while (fread(temp, 3, 1, card) == 1) { // 匹配前三个字节 if (temp[0] == 0xff && temp[1] == 0xd8 && temp[2] == 0xff) { // 读取第四个字节 if (fread(&temp1, 1, 1, card) == 1) { // 判断第四个字节的前4位 if ((temp1 & 0xf0) == 0xe0) { counter++; // 仅用于计数场景:跳过当前块剩余字节避免重复计数 fseek(card, 508, SEEK_CUR); } else { // 第四个字节不匹配,回退1字节 fseek(card, -1, SEEK_CUR); } } } else { // 前三个字节不匹配,回退2字节 fseek(card, -2, SEEK_CUR); } } printf("counter = %i\n", counter); // 关闭文件 fclose(card); return 0; }
新手友好提示
- 二进制文件操作一定要用
"rb"(读)或"wb"(写)模式,否则Windows系统会把0x0a转换为0x0d0a,破坏二进制数据。 - 用
unsigned char存储二进制字节,避免符号位导致的数值错误(比如0xff作为signed char会被解析为-1)。 - 每次调用文件操作函数(
fread、fseek等)最好检查返回值,确保操作成功。 - 动态分配的内存记得用
free释放,虽然小程序可能不会有问题,但养成好习惯很重要。
内容的提问来源于stack exchange,提问作者Freddy

