【问题标题】:C undefined behaviour with fscanf使用 fscanf 的 C 未定义行为
【发布时间】:2020-06-07 21:25:35
【问题描述】:

我在下面有一个代码 sn-p。 在 macOS 上,我在 Xcode 和 CLion 中运行它,得到了同样奇怪的结果。 另一方面,在使用 gcc 编译的 Linux 上,它可以完美运行。 我想知道代码在任何时候是否会产生未定义的行为。 它试图解析的输入文件是 vigenére 表,你知道,有 26 个字符的行,带有拉丁字母,并且字母在下面的 1 行上左移 1。 每行都由 CRLF 终止。 预期的输出是控制台上打印的表格。 意想不到的部分是 az 至少 1 行在 macOS 上显示不正确。 这是输入顺便说一句: https://pastebin.com/QnucTAFs (但我不知道是否保留了相应的行尾)

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

char ** parse(char *path) {
    FILE *f = fopen(path, "r");
    char **table = (char**)malloc(sizeof(char*) * 26);
    int i = -1;

    do table[++i] = (char*)malloc(sizeof(char) * 27);
    while (fscanf(f, "%s", table[i]) > 0);

    return table;
}

int main() {
    char **table = parse("Vtabla.dat");

    for (int i = 0; i < 26; i++) {
        for (int x = 0; x < 26; x++)
            printf("%c", table[i][x]);
        printf("\n");
    }

    return 0;
}

【问题讨论】:

  • fclose(f);parse 函数中很好,如果文件中的行数超出您的预期,这看起来会爆炸。
  • 这段代码从未编译过
  • @SteveFriedl 不能有更多行,因为字母表有 26 个字符长,即 Vigenère 表中的行数。
  • “不可能”是著名的遗言。在 C 中,您必须确保没有。
  • "无法使用 fgets 解析它" --> char buf[80]; if (fgets(buf, sizeof buf, f)) { buf[strcspn(buf, "\n\r")] = 0; assert(strlen(buf) == 26); strcpy(table[i], buffer); }.

标签: c scanf newline undefined-behavior


【解决方案1】:

这段代码有一个bug,在这个循环中:

do table[++i] = (char*)malloc(sizeof(char) * 27);
while (fscanf(f, "%s", table[i]) > 0);

table 包含 26 个指针,但在 fscanf() 失败的迭代中,table 变量的第 27 个指针在上一步中通过 malloc 初始化。这会破坏我系统上table 中的数据。你可以通过将这一行中的 26 改为 27 来说服自己,看看你的问题是否消失了:

char **table = (char**)malloc(sizeof(char*) * 26);

我的代码返工:

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

#define LETTERS 26

char **parse(char *path) {
    char **table = calloc(LETTERS, sizeof(char *));

    FILE *f = fopen(path, "r");

    for (int i = 0; i < LETTERS; i++) {
        table[i] = (char *) calloc(LETTERS+1, sizeof(char));

        if (fscanf(f, "%s", table[i]) <= 0) {
            break;
        }
    }

    fclose(f);

    return table;
}

int main() {
    char **table = parse("Vtabla.dat");

    for (int i = 0; i < LETTERS; i++) {
        for (int j = 0; j < LETTERS; j++)
            printf("%c", table[i][j]);

        printf("\n");
        free(table[i]);
    }

    free(table);
    return 0;
}

【讨论】:

  • “我认为 27 永远不会大于 26” :-)
  • 是的,很明显,可能就是这样。
【解决方案2】:

cmets 中的讨论很热烈,但 OP 似乎对许多有经验的开发人员所关注的更狭隘的关注点感兴趣,所以我将发布一个不是严格针对问题的答案,而是展示更广泛的关注点。

我相信我们中的许多人已经跳过了我们认为极不可能出现的情况的错误检查,但“找不到文件”或“文件格式错误”甚至不属于该类别。这试图解决这个问题,而且它会在读取后关闭文件,并用一个常量替换一个幻数(“26”)。

在读取每个输入行时,如果碰巧有太多字符,这会溢出缓冲区,但我会将这个限制检查留给读者作为练习。

格式错误的用户输入非常普遍,因此必须检查它。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <err.h>

#define ALPHABET_SIZE  26

char ** parse(const char *path) {

    FILE *f = fopen(path, "r");

    if (f == 0)
        errx(EXIT_FAILURE, "Cannot open input file %s (err=%s)", path, strerror(errno));

    char **table = malloc(sizeof(char*) * ALPHABET_SIZE);
    int i = -1;

    do
    {
      // BUG: overflows the table - see cdlane's answer
      table[++i] = malloc(ALPHABET_SIZE + 1);

                         // TODO: what if line is too long? Or too short?
    } while (i < ALPHABET_SIZE  &&  fscanf(f, "%s", table[i]) > 0);

    if (i != ALPHABET_SIZE)
        errx(EXIT_FAILURE, "Not enough input lines");

    fclose(f);

    return table;
}

int main() {
    char **table = parse("Vtabla.dat");

    for (int i = 0; i < ALPHABET_SIZE; i++) {
        for (int x = 0; x < ALPHABET_SIZE; x++)
            printf("%c", table[i][x]);
        printf("\n");
    }

    return 0;
}

【讨论】:

  • 也许是fscanf(f, "%s", table[i]) --> fscanf(f, "%26s", table[i]) 否则代码就像gets
  • @chux-ReinstateMonica 我对*scanf 有一种病态的厌恶,以至于我无法让自己去那里;您使用fgets() 的方法更好。
  • UV 表示对 *scanf 的病态厌恶。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-27
  • 1970-01-01
  • 1970-01-01
  • 2014-11-21
  • 1970-01-01
相关资源
最近更新 更多