C - 从文件中复制文本会导致未知字符也被复制

C - Copying text from a file results in unknown characters being copied over as well

当 运行 连接以下 C 文件时,将字符复制到 fgetc 到我的 tmp 指针会导致由于某种原因复制未知字符。从 fgetc() 收到的字符是预期的字符。但是,出于某种原因,当将此字符分配给我的 tmp 指针时,未知字符会被复制过来。

我尝试在网上寻找原因,但没有找到任何运气。据我所知,这可能与 UTF-8 和 ASCII 问题有关。但是,我不确定该修复程序。我是一个相对较新的 C 程序员,对内存管理还是个新手。

输出:

TMP: Hello, DATA!�
TEXT: Hello, DATA!�

game.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>

const int WIN_WIDTH = 1366;
const int WIN_HEIGHT = 768;

char *readFile(const char *fileName) {
    FILE *file;
    file = fopen(fileName, "r");

    if (file == NULL) {
        printf("File could not be opened for reading.\n");
    }

    size_t tmpSize = 1;
    char *tmp = (char *)malloc(tmpSize);

    if (tmp == NULL) {
        printf("malloc() could not be called on tmp.\n");
    }

    for (int c = fgetc(file); c != EOF; c = fgetc(file)) {
        if (c != NULL) {
            if (tmpSize > 1)
                tmp = (char *)realloc(tmp, tmpSize);

            tmp[tmpSize - 1] = (char *)c;
            tmpSize++;
        }
    }
    tmp[tmpSize] = 0;

    fclose(file);
    printf("TMP: %s\n", tmp);
    return tmp;
}

int main(int argc, char **argv) {
    al_init();
    al_install_keyboard();

    ALLEGRO_TIMER* timer = al_create_timer (1.0 / 30.0);
    ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();

    ALLEGRO_DISPLAY* display = al_create_display(WIN_WIDTH, WIN_HEIGHT);
    ALLEGRO_FONT* font = al_create_builtin_font();

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(display));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    int redraw = 1;
    ALLEGRO_EVENT event;

    al_start_timer(timer);

    char *text = readFile("game.DATA");
    printf("TEXT: %s\n", text);

    while (1) {
        al_wait_for_event(queue, &event);
        if (event.type == ALLEGRO_EVENT_TIMER)
            redraw = 1;
        else if ((event.type == ALLEGRO_EVENT_KEY_DOWN) || (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE))
            break;
        
        if (redraw && al_is_event_queue_empty(queue)) {
            al_clear_to_color(al_map_rgb(0, 0, 0));
            al_draw_text(font, al_map_rgb(255, 255, 255), 0, 0, 0, text);
            al_flip_display();

            redraw = false;
        }
    }

    free(text);
    al_destroy_font(font);
    al_destroy_display(display);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

game.DATA 文件:

Hello, DATA!

我用运行的程序:

gcc game.c -o game $(pkg-config allegro-5 allegro_font-5 --libs --cflags)

--编辑--

我尝试获取文件读取代码并将其 运行 放入一个新的 c 文件中,出于某种原因它在那里工作,但在带有快板代码的 game.c 文件中时不起作用。

test.c:

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

char *readFile(const char *fileName) {
    FILE *file;
    file = fopen(fileName, "r");

    if (file == NULL) {
        printf("File could not be opened for reading.\n");
    }

    size_t tmpSize = 1;
    char *tmp = (char *)malloc(tmpSize);

    if (tmp == NULL) {
        printf("malloc() could not be called on tmp.\n");
    }

    for (int c = fgetc(file); c != EOF; c = fgetc(file)) {
        if (c != NULL) {
            if (tmpSize > 1)
                tmp = (char *)realloc(tmp, tmpSize);

            tmp[tmpSize - 1] = (char *)c;
            tmpSize++;
        }
    }
    tmp[tmpSize] = 0;

    fclose(file);
    printf("TMP: %s\n", tmp);
    return tmp;
}

void main() {
    char *text = readFile("game.DATA");
    printf("TEXT: %s\n", text);

    free(text);
    return 0;
}

始终产生正确的输出:

TMP: Hello, DATA!
TEXT: Hello, DATA!

当您编写一个每次都更新各种内容的循环时,就像您在此处的循环中使用 tmpSize 一样,重要的是要掌握理论计算机科学类型所称的“loop invariants”。也就是说,每次循环中什么是真的?重要的是不仅要正确地维护你的循环不变量,选择你的循环不变量也很重要,这样它们很容易维护,也很容易让后来的人reader理解和理解。验证。

因为 tmpSize 开始时是 1,我猜你的循环不变量是这样的,“tmpSize 总是比我到目前为止读过的字符串的大小多一个”。选择这个有点奇怪的循环不变式的一个原因当然是,您需要额外的字节来终止 [=24=]。另一个线索是你正在设置 tmp[tmpSize-1] = c;.

但这是第一个问题。当我们退出循环时,如果 tmpSize 仍然比您目前读取的字符串的大小多 1,让我们看看会发生什么。假设我们读了三个字符。所以 tmpSize 应该是 4。所以我们将设置 tmp[4] = 0;。可是等等!请记住,C 中的数组是从 0 开始的。所以我们读取的三个字符在 tmp[0]tmp[1]tmp[2] 中,我们希望终止字符 [=24=] 进入 tmp[3],而不是 tmp[4]。出了点问题。

但实际上,情况比那更糟。我完全不确定我是否理解循环不变式,所以我作弊并插入了一些调试打印输出。在 realloc 调用之前,我添加了

printf("realloc %zu\n", tmpSize);

最后,就在 tmp[tmpSize] = 0; 行之前,我添加了

printf("final %zu\n", tmpSize);

它打印的最后几行(在读取包含“Hello, DATA!”的 game.DATA 文件时,就像你的一样)是:

...
realloc 10
realloc 11
realloc 12
final 13

但是这个被两个关闭了!如果最后一次重新分配数组的大小为 12,则有效索引为 0 到 11。但不知何故我们最终将 [=24=] 写入单元格 13.

我花了一段时间才弄明白,但第二个问题是你在循环的顶部进行了重新分配,在你递增 tmpLen.

之前

对我来说,“比目前读取的字符串大小多一”的循环不变量实在是太难想了。我非常喜欢使用循环不变式,其中“大小”变量跟踪我 读取的字符数,而不是 +1 或 -1。让我们看看这个循环看起来如何。 (我还清理了其他一些东西。)

size_t tmpSize = 0;
char *tmp = malloc(tmpSize+1);
if (tmp == NULL) {
    printf("malloc() failed.\n");
    exit(1);
}

for (int c = getc(file); c != EOF; c = getc(file)) {
    printf("realloc %zu\n", tmpSize+1+1);
    tmp = realloc(tmp, tmpSize+1+1);        /* +1 for c, +1 for [=13=] */
    if (tmp == NULL) {
        printf("realloc() failed.\n");
        exit(1);
    }
    tmp[tmpSize] = c;
    tmpSize++;
}

printf("final %zu\n", tmpSize);
tmp[tmpSize] = '[=13=]';

这里仍然有些可疑 -- 我说过我不喜欢像 +1 这样的“软糖因素”,这里我有 两个 -- 但至少现在调试打印输出

...
realloc 11
realloc 12
realloc 13
final 12

看来我还没有超过运行分配的内存。

为了让这变得更好,我想采取一种稍微不同的方法。一开始你不应该担心效率,但我可以告诉你调用 realloc 使缓冲区变大 1 的循环,每次读取一个字符时,最终可能是 真的效率低下。那么让我们再做一些改变:

size_t nchAllocated = 0;
size_t nchRead = 0;
char *tmp = NULL;

for (int c = getc(file); c != EOF; c = getc(file)) {
    if(nchAllocated <= nchRead) {
        nchAllocated += 10;
        printf("realloc %zu\n", nchAllocated);
        tmp = realloc(tmp, nchAllocated);
        if (tmp == NULL) {
            printf("realloc() failed.\n");
            exit(1);
        }
    }
    tmp[nchRead++] = c;
}

printf("final %zu\n", nchRead);
tmp[nchRead] = '[=15=]';

现在有两个独立的变量:nchAllocated 记录我分配的字符数,nchRead 记录我读取的字符数。虽然我将“计数器”变量的数量增加了一倍,但这样做我简化了很多其他事情,所以我认为这是一个净改进。

首先,请注意根本不再有 +1 软糖因素。

其次,此循环不会每次都调用 realloc——而是一次分配 10 个字符。并且因为分配的字符数和读取的字符数有不同的变量,所以它可以跟踪这样一个事实,即它分配的字符数可能比目前读取的字符数多。对于此代码,调试打印输出为:

realloc 10
realloc 20
final 12

另一个小改进是我们不必“预分配”数组——没有初始 malloc 调用。我们的循环不变量之一是 nchAllocated 是分配的字符数,我们从 0 开始,如果没有分配字符,那么 tmp 开始为 NULL 也没关系。这依赖于这样一个事实,即当您第一次调用 realloc 时,tmp 等于 NULLrealloc 就可以了,本质上就像 [=43] =].

但是您可能会问一个问题:如果我摆脱了所有软糖因素,我们应该在哪里分配一个额外的字节来保存终止 [=24=] 字符?它就在那里,但它很微妙:它潜伏在测试中

if(nchAllocated <= nchRead)

循环的第一次,nchAllocated 将为 0,nchRead 将为 0,但此测试为真,因此我们将分配第一个 10 个字符块,我们开始 运行。 (如果我们不关心 [=24=] 字符,测试 nchAllocated < nchRead 就足够了。)

...但是,其实,我错了!这里有一个微妙的错误!

读取的文件为空怎么办? tmp 将从 NULL 开始,我们永远不会在循环中进行任何旅行,因此 tmp 将保持 NULL,因此当我们分配 tmp[nchRead] = 0 时会爆炸的。

实际上,情况比这更糟。如果你非常仔细地跟踪逻辑,你会发现任何时候文件大小都是 10 的倍数,毕竟没有足够的 space 分配给 [=24=]

这表明“一次分配 10 个字符”方案的一个重大缺陷。代码现在更难测试,因为对于大小为 10 的倍数的文件,控制流是不同的。如果您从来没有碰巧测试过这种情况,您将不会意识到该程序中存在错误。

我通常解决这个问题的方法是注意我必须添加以终止字符串的 [=24=] 字节与我读到的指示字符串结束的 EOF 字符有点平衡文件。也许,当我读到EOF时,我可以用它来提醒我为[=24=]分配space。这实际上很容易做到,看起来像这样:

int c;
while(1) {
    c = getc(file);
    if(nchAllocated <= nchRead) {
        nchAllocated += 10;
        printf("realloc %zu\n", nchAllocated);
        tmp = realloc(tmp, nchAllocated);
        if (tmp == NULL) {
            printf("realloc() failed.\n");
            exit(1);
        }
    }

    if(c == EOF)
        break;

    tmp[nchRead++] = c;
}

printf("final %zu\n", nchRead);
tmp[nchRead] = '[=18=]';

这里的诀窍是我们不测试 EOF 直到 我们检查缓冲区中有足够的 space 之后,并且如有必要,称为 realloc。就好像我们在缓冲区中为 EOF 分配了 space —— 除了我们将 space 用于 [=24=] 之外。这就是我所说的“用它来提醒我为[=24=]分配space”的意思。

现在,我不得不承认这里还有一个缺点,即循环现在有些不合常规。顶部有 while(1) 的循环看起来像一个无限循环。这个有

if(c == EOF) break;

在它的中间,所以它实际上是一个“中间中断”循环。 (这与传统的 forwhile 循环形成对比,它们是“在顶部中断”,或者 do/while 循环,这是“在顶部中断bottom”。)就我个人而言,我发现这是一个有用的习语,而且我一直都在使用它。但是有些程序员,也许还有你的导师,会对它皱眉,因为它“怪异”、“不同”、“反常规”。在某种程度上他们是对的:非常规编程是有些危险的编程,如果后来的维护程序员因为不认识或不理解其中的习语而无法理解它,那就太糟糕了。 (这在某种程度上相当于英语单词“ain't”或拆分不定式。)

最后,如果你还和我在一起,我还有一点要说。 (如果你仍然和我在一起,谢谢。我知道这个答案已经 很长 ,但我希望你能学到一些东西。)

之前我说过“调用 realloc 使缓冲区变大 1 的循环,每次读取一个字符,最终都会非常低效。”事实证明,使缓冲区增大 10 倍的循环并没有好多少,而且效率仍然很低。您可以通过将其增加 50 或 100 来做得更好,但是如果您处理的输入可能非常大(数千个字符或更多),通常最好突飞猛进地增加缓冲区大小,或许通过 乘以 它乘以某个因素,而不是相加。所以这是该循环部分的最终版本:

    if(nchAllocated <= nchRead) {
        if(nchAllocated == 0) nchAllocated = 10;
        else nchAllocated *= 2;
    printf("realloc %zu\n", nchAllocated);
    tmp = realloc(tmp, nchAllocated);

甚至这种改进——乘以 2,而不是增加一些东西——也有代价:我们需要额外的测试,以特殊情况下循环的第一次旅行,因为 nchAllocated 开始输出为 0,且 0 × 2 = 0。

您的重新分配方案不正确:数组总是太短一个字节,空终止符写在字符串末尾后一个位置,而不是字符串末尾。这会导致打印一个额外的字节,无论值恰好在 realloc() 返回的块的内存中,它是未初始化的。

使用tmpLen作为到目前为止读取的字符串的长度并为新读取的字符和空终止符分配2个额外的字节不会造成混淆。

此外,测试 c != NULL 没有意义:c 是字节,NULL 是指针。同样,tmp[tmpSize - 1] = (char *)c; 是不正确的:您应该只写

tmp[tmpSize - 1] = c;

这是更正后的版本:

char *readFile(const char *fileName) {
    FILE *file = fopen(fileName, "r");

    if (file == NULL) {
        printf("File could not be opened for reading.\n");
        return NULL;
    }

    size_t tmpLen = 0;
    char *tmp = (char *)malloc(tmpLen + 1);

    if (tmp == NULL) {
        printf("malloc() could not be called on tmp.\n");
        fclose(file);
        return NULL;
    }

    int c;
    while ((c = fgetc(file)) != EOF) {
        char *new_tmp = (char *)realloc(tmp, tmpLen + 2);
        if (new_tmp == NULL) {
            printf("realloc() failure for %zu bytes.\n", tmpLen + 2);
            free(tmp);
            fclose(file);
            return NULL;
        }
        tmp = new_tmp;
        tmp[tmpLen++] = c;
    }
    tmp[tmpLen] = '[=11=]';

    fclose(file);
    printf("TMP: %s\n", tmp);
    return tmp;
}

通常最好以块或几何大小增量重新分配。这是一个简单的实现:

char *readFile(const char *fileName) {
    FILE *file = fopen(fileName, "r");

    if (file == NULL) {
        printf("File could not be opened for reading.\n");
        return NULL;
    }

    size_t tmpLen = 0;
    size_t tmpSize = 16;
    char *tmp = (char *)malloc(tmpSize);
    char *newTmp;

    if (tmp == NULL) {
        printf("malloc() could not be called on tmp.\n");
        fclose(file);
        return NULL;
    }

    int c;
    while ((c = fgetc(file)) != EOF) {
        if (tmpSize - tmpLen < 2) {
            size_t newSize = tmpSize + tmpSize / 2;
            newTmp = (char *)realloc(tmp, newSize);
            if (newTmp == NULL) {
                printf("realloc() failure for %zu bytes.\n", newSize);
                free(tmp);
                fclose(file);
                return NULL;
            }
            tmpSize = newSize;
            tmp = newTmp;
        }
        tmp[tmpLen++] = c;
    }
    tmp[tmpLen] = '[=12=]';

    fclose(file);
    printf("TMP: %s\n", tmp);

    // try to shrink allocated block to the minimum size
    // if realloc() fails, return the current block
    // it seems impossible for this reallocation to fail
    // but the C Standard allows it.
    newTmp = (char *)realloc(tmp, tmpLen + 1);
    return newTmp ? newTmp : tmp;
}