如何修复看似不相关的Valgrind条件跳转与内存写入错误?
我正在编写一个C程序,用于判断给定字符串是否为semidrome(由两个及以上回文拼接而成的字符串)。程序功能正常,但始终出现无法理解的Valgrind错误。我刚接触C语言和Valgrind,虽大致了解错误含义,但不清楚这些错误在代码中的触发原因。以下是原始代码、Valgrind报错信息,以及对应的错误分析和修复方案。
原始代码
#include <stdio.h> #include <stdbool.h> #include <stdlib.h> #include <string.h> #include <assert.h> // 判断字符串是否为回文 int isPalindrome(char str[]) { int len = strlen(str); if (len == 1){return 0;} int left = 0; int right = len - 1; while (right > left) { if (str[left++] != str[right--]) { return 0; } } return 1; } bool is_semidrome(char *s) { int len = strlen(s); // 空字符串不是semidrome if(len == 0){return 0;} // 本身是回文则直接是semidrome if(isPalindrome(s)){return 1;} // 用于存储子串的指针 char *palcheck, *palcheckrest; for (int i = 1; i < len; i++){ // 找到与首字符相同的位置,尝试分割出回文子串 if (s[0] == s[i]){ // 分配内存存储前半部分子串 palcheck = malloc((i+2)*sizeof(char)); palcheck = strncpy(palcheck, s, i+1); palcheck[i+1] = '\0'; // 如果前半部分是回文,递归检查剩余部分是否为semidrome if (isPalindrome(palcheck)){ // 分配内存存储剩余子串 palcheckrest = malloc((len - i)*sizeof(char)); for (int j = 0; j < (len - i - 1); j++){ palcheckrest[j] = s[j+i+1]; } palcheckrest[len - i] = '\0'; // 剩余部分是semidrome则返回true if (is_semidrome(palcheckrest)){ free(palcheck); free(palcheckrest); return true; } else{ free(palcheck); free(palcheckrest); // 如果是最后一个索引仍未找到,返回false if(i == len){ return false; } // 否则继续循环 else{ free(palcheck); free(palcheckrest); continue; } } } else { free(palcheck); continue; } } } }
Valgrind报错信息(翻译后)
输入: popeye ==152438== Memcheck,内存错误检测器 ==152438== 版权 (C) 2002-2017,Julian Seward等人,遵循GNU GPL协议 ==152438== 使用Valgrind-3.15.0和LibVEX;重新运行加-h查看版权信息 ==152438== 命令: ./student_answer ==152438== ==152438== 大小为1的非法写入 ==152438== 位于 0x10A3CE: is_semidrome (semidrome.c:39) ==152438== 由 0x10A1EB: main (main.c:11) 调用 ==152438== 地址0x4a73154是大小为4的分配块之后的0字节位置 ==152438== 位于 0x483B7F3: malloc (在/usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so中) ==152438== 由 0x10A377: is_semidrome (semidrome.c:35) 调用 ==152438== 由 0x10A1EB: main (main.c:11) 调用 ==152438== ==152438== 条件跳转或移动依赖于未初始化的值 ==152438== 位于 0x483EF58: strlen (在/usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so中) ==152438== 由 0x10A2B4: is_semidrome (semidrome.c:23) 调用 ==152438== 由 0x10A3DC: is_semidrome (semidrome.c:40) 调用 ==152438== 由 0x10A1EB: main (main.c:11) 调用 ==152438== ==152438== 条件跳转或移动依赖于未初始化的值 ==152438== 位于 0x483EF58: strlen (在/usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so中) ==152438== 由 0x10A230: isPalindrome (semidrome.c:9) 调用 ==152438== 由 0x10A2D3: is_semidrome (semidrome.c:25) 调用 ==152438== 由 0x10A3DC: is_semidrome (semidrome.c:40) 调用 ==152438== 由 0x10A1EB: main (main.c:11) 调用 ==152438== ==152438== ==152438== 堆摘要: ==152438== 退出时正在使用: 0字节,0块 ==152438== 总堆使用: 5次分配,5次释放,分配了8250字节 ==152438== ==152438== 所有堆块均已释放——无内存泄漏可能 ==152438== ==152438== 使用--track-origins=yes查看未初始化值的来源 ==152438== 要查看检测到的和被抑制的错误列表,请重新运行时加: -s ==152438== 错误摘要: 3个上下文共3个错误 (已抑制: 0个,共0个)
错误分析与修复
1. 非法写入(Invalid write of size 1)
原因:第39行palcheckrest[len - i] = '\0';越界写入。palcheckrest分配的大小是len - i字节,合法索引范围是0 ~ (len - i - 1),写入len - i位置超出了分配的内存块。
修复:将该行改为:
palcheckrest[len - i - 1] = '\0';
2. 未初始化值导致的条件跳转
原因:palcheckrest的字符串未正确终止,加上之前的越界写入,导致strlen读取到未初始化的内存区域。
验证修复:修改'\0'的写入位置后,palcheckrest会被正确终止,strlen就能正常工作。
3. 重复释放内存(潜在问题)
原因:在else分支中,已经执行了free(palcheck); free(palcheckrest);,后续又重复执行一次,会导致双重释放错误。
修复:删除重复的free语句,修改后的分支代码:
else{ free(palcheck); free(palcheckrest); // 如果是最后一个索引仍未找到,返回false if(i == len - 1){ return false; } // 否则继续循环 else{ continue; } }
4. 函数缺少返回值(潜在未定义行为)
原因:is_semidrome函数如果循环结束后未找到符合条件的分割方式,没有明确的return语句,会返回随机值,导致未定义行为。
修复:在函数末尾添加return false;,表示未找到符合条件的回文拼接。
5. 无效的索引判断(潜在逻辑问题)
原因:原代码中if(i == len)永远不会成立,因为循环条件是i < len,i的最大值是len-1。
修复:将判断改为i == len -1。
修复后的完整代码
#include <stdio.h> #include <stdbool.h> #include <stdlib.h> #include <string.h> #include <assert.h> // 判断字符串是否为回文 int isPalindrome(char str[]) { int len = strlen(str); if (len == 1){return 0;} int left = 0; int right = len - 1; while (right > left) { if (str[left++] != str[right--]) { return 0; } } return 1; } bool is_semidrome(char *s) { int len = strlen(s); // 空字符串不是semidrome if(len == 0){return false;} // 本身是回文则直接是semidrome if(isPalindrome(s)){return true;} // 用于存储子串的指针 char *palcheck, *palcheckrest; for (int i = 1; i < len; i++){ // 找到与首字符相同的位置,尝试分割出回文子串 if (s[0] == s[i]){ // 分配内存存储前半部分子串 palcheck = malloc((i+2)*sizeof(char)); strncpy(palcheck, s, i+1); palcheck[i+1] = '\0'; // 如果前半部分是回文,递归检查剩余部分是否为semidrome if (isPalindrome(palcheck)){ // 分配内存存储剩余子串 palcheckrest = malloc((len - i)*sizeof(char)); for (int j = 0; j < (len - i - 1); j++){ palcheckrest[j] = s[j+i+1]; } palcheckrest[len - i - 1] = '\0'; // 剩余部分是semidrome则返回true if (is_semidrome(palcheckrest)){ free(palcheck); free(palcheckrest); return true; } else{ free(palcheck); free(palcheckrest); // 如果是最后一个索引仍未找到,返回false if(i == len - 1){ return false; } // 否则继续循环 else{ continue; } } } else { free(palcheck); continue; } } } // 所有分割方式都尝试过,返回false return false; }
内容的提问来源于stack exchange,提问作者Veronika

