C 程序中的边缘案例,用于查找和替换文本文件中的单词

Edge case in C program to find and replace words from a text file

我是 C 的新手,如果能帮助我修复程序中的错误,我将不胜感激。

我发现了一个边缘案例,但我不太确定如何解决它。

目前,该函数将查找并替换给定文本文件中的单词和单词中的单词。例如,将 'water' 更改为 'snow' 会将字符串 'waterfall' 更改为 'snowfall'。这是预期的结果。

然而,当我输入'waterfalls'来改变单词'waterfall'时,程序似乎陷入了死循环。我不太清楚为什么,但如果有人能指出正确的方向,我将不胜感激。

这是我的代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#define BUFFER_SIZE 20

void replaceWord(char *str, const char *oldWord, const char *newWord)
{
    char *position, buffer[BUFFER_SIZE];
    int index, oldWordLength;
    oldWordLength = (long)strlen(oldWord);
    while ((position = strstr(str, oldWord)) != NULL)
    {
        strcpy(buffer, str);
        index = position - str;
        str[index] = '[=10=]';
        strcat(str, newWord);
        strcat(str, buffer + index + oldWordLength);
    }
}

int main()
{
    char msg[100] = "This is some text with the word snowfall to replace";
    puts(msg);
    replaceWord(msg, "snowfall", "snowfalls");
    puts(msg);
    return 0;
}

好的。首先,您的缓冲区大小严重不足。这个:

char buffer[BUFFER_SIZE];

是最终成为原始消息的 full-string 副本的目标。但在 main 中,原始消息:

char msg[100] = "This is some text with the word snowfall to replace";

51 个字符宽(不包括终止符)。那是行不通的,运行通过调试地址清理程序(或理想情况下的常规调试器)将显示这一点。 :

==1==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd578054d4 at pc 0x7f7457e4c846 bp 0x7ffd57805460 sp 0x7ffd57804c10
WRITE of size 52 at 0x7ffd578054d4 thread T0
    #0 0x7f7457e4c845  (/opt/compiler-explorer/gcc-11.2.0/lib64/libasan.so.6+0x55845)
    #1 0x4012a7 in replaceWord /app/example.c:15
    #2 0x401592 in main /app/example.c:27
    #3 0x7f7457c2c0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
    #4 0x40112d in _start (/app/output.s+0x40112d)

Address 0x7ffd578054d4 is located in stack of thread T0 at offset 52 in frame
    #0 0x4011f5 in replaceWord /app/example.c:9

所以这显然不是什么好事。通过增加缓冲区大小来解决这个问题将“有效”,但要真正做到这一点 source/target 缓冲区(在你的函数中它们是同一个)应该有它的完整可写宽度(包括 space 作为终结符)作为参数提供(你绝对应该这样做。

其次,执行此操作的代码:

while ((position = strstr(str, oldWord)) != NULL)

总是从输入字符串的 开头 开始搜索 oldWord。这是错误的(好吧,恰好 一次;第一次通过;之后,它是错误的)。考虑一下:

i love c++

假设我查看或 i 并且我想将其替换为 is。我会在这里找到它:

i love c++
^

替换我正在构建的新字符串后如下所示:

is love c++

那么您知道从哪里开始下一次搜索?您从原始字符串 开始 的位置开始,加上 替换 字符串值的长度。原来在pos 0,替换的长度是2,所以我们从pos 2开始下一次搜索。

is love c++
  ^

请注意,当你做所有事情时,这会变得更加复杂 in-place(例如,没有中间缓冲区),但这似乎不是你现在的目标,而且可能会关闭你的雷达。因此,一个 not-very-efficient,但实用的方法是:

  1. 从字符串的开头开始 (src = str)
  2. 搜索旧词,从 src 开始。
  3. 如果找到,复制原始字符串 up-to,但不包括旧词到缓冲区。
  4. 将替换字符串附加到缓冲区。
  5. 追加原始字符串的剩余部分将旧词传递到缓冲区。
  6. 将缓冲区复制回源字符串。
  7. 重新定位 src 为替换词的长度 过去 从 (3)
  8. 中找到的旧词的原始位置
  9. 循环回到(2),直到不再找到旧词。

如我所说;效率不高,但很容易理解。在代码中看起来像这样。请注意缓冲区大小显着增加,并用于在 main 中声明临时缓冲区和数组。 这还是不好,但这是你带来的,所以我坚持下去。我敦促您考虑使用动态内存管理来执行此算法,或者 size-restrictions 作为附加参数传递:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#define BUFFER_SIZE 100

void replaceWord(char *str, const char *oldWord, const char *newWord)
{
    char buffer[ BUFFER_SIZE ];
    char *src = str;
    char *oldpos = NULL;

    size_t lenOldWord = strlen(oldWord);
    size_t lenNewWord = strlen(newWord);

    while ((oldpos = strstr(src, oldWord)) != NULL)
    {
        // 1. copy everything up to the old word.
        // 2. append the new word
        // 3. copy the remainder of source string *past* the old word
        // 4. copy back to the original string.
        memcpy(buffer, str, (size_t)(oldpos - str));
        memcpy(buffer + (oldpos - str), newWord, lenNewWord);
        strcpy(buffer + (oldpos - str) + lenNewWord, oldpos + lenOldWord);
        strcpy(str, buffer);

        // the new starting point will be the previous discovry
        //  location plus the length of the new word.
        src = oldpos + lenNewWord;
    }
}

int main()
{
    char msg[BUFFER_SIZE] = "This is some text with the word snowfall to replace";
    puts(msg);
    replaceWord(msg, "snowfall", "snowfalls");
    puts(msg);
    return 0;
}

输出

This is some text with the word snowfall to replace
This is some text with the word snowfalls to replace

我强烈建议您在调试器中 运行 并逐步观察它是如何工作的。它将帮助您了解您在哪里遗漏了 start-search-here 逻辑。我更强烈地建议你,作为一个练习,解决这个算法的明显漏洞。想想可以轻松调用未定义行为的方法(提示:简短、常见的旧词,非常 非常 长的替换词),以及解决这些问题应该做的事情。