【问题标题】:Problem with sscanf reading multiple strings from a input filesscanf 从输入文件中读取多个字符串的问题
【发布时间】:2020-06-20 20:38:51
【问题描述】:

所以我正在慢慢地继续学习 C。现在,我有一个任务,从文件中读取数据并对其进行排序。

文件数据:

House naming 1 30 300
House naming 2 45 450
.......
House naming 10 5 120

所以,第一个值:House naming,可以是任何命名,如Empire state building

第二个值是:房屋地址(我只选择了integer值)

第三个值是:建筑物的年龄

第四个值是:千瓦时/年

程序必须从文件中获取数据 -> 打印出来 -> 排序(如何?见下文) -> 再次打印出来,排序。

排序:

  • kwh
  • kwh
  • kwh > 300 && age > 40 - 准备拆除。

代码如下:

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

int main(void) {
    int kwh;
    int age;
    char building[SIZE];
    int addr;
    char buff[SIZE];
    FILE *fi;

    // opening the files and checking if it succeeded
    fi = fopen(F_INPUT, "r");
    if (fi == NULL) {
        printf("Error opening input file \"%s\"", F_INPUT);
        exit(EXIT_INPUT_FAIL);
    }
    while (fgets(buff, sizeof(buff), fi) != NULL) {
        sscanf(buff, "%s %d %d %d", building, &addr,&age,&kwh);
        if (kwh < 200) {
            puts(buff);
            printf("Sustainable\n");
        } else
        if (kwh < 300 && age < 40) {
            puts(buff);
            printf("Needs renovation\n");
        } else
        if (kwh > 300 && age > 40) {
            puts(buff);
            printf("IN DEMOLITION LIST\n");
        }
    }
    /* close the files when they're not needed anymore */
    fclose(fi);
    return 0;
}

我结合了几个步骤让它变得更容易,读取数据 -> 输出已标记为 1) 可持续,2) 需要翻新,3) 准备拆除。

问题出在while 循环中,我认为它出在sscanf 函数中。 在我的逻辑中,如果我没记错的话,它必须从文件中读取一个字符串,使用逻辑(查看sscanf 和输入文件):char valueintegerintegerinteger。 程序读取文件,输出数据,但将所有建筑物标记为sustainable

您建议更仔细阅读或阅读多个字符串时选择哪种逻辑更好。

输出:

House naming 1 30 300
Sustainable
House naming 2 45 450
Sustainable
........
House naming 10 5 120
Sustainable

【问题讨论】:

  • 请注意,格式说明符 %s 在第一个空格处停止,因此它不能用于读取,例如,“帝国国家建设”。一种解决方案可能是将输入字符串分解为带有strtok 的令牌指针数组,从最后三个中提取整数,并从剩余的第一个中构建一个新字符串。
  • 您的sscanf 未按预期工作。始终检查函数的返回值,在本例中为 sscanf。并花时间学习如何使用调试器。也很有用:How to debug small programs
  • sscanf with %s 读取直到遇到第一个空格。如果名称字段可以包含多个单词(包括数字?),它可能会令人生畏。尝试拆分为多个以空格分隔的字段;那么最后 3 个字段是数字,前 n-3 个字段是名称。
  • “房屋命名”可以包含数字吗?喜欢“Stdio 54”吗?
  • 请注意,在您链接的页面上的示例中,输入中有 4 个格式说明符和 4 个由空格分隔的文本序列。如果有 五个 文本序列,例如"Saturday Sunday March 25 1989",它将无法正常工作。格式%s在第一个空格处停止。 IMO 你能做的最好的事情是编写一个简短的测试程序来探索scanf 的行为方式,并且没有其他目的。

标签: c file input scanf puts


【解决方案1】:

通过fgets() 将文件中的 line 读入 string 是 OP 所做的很好的第一步。

“房屋命名”可以包含数字吗?喜欢“Stdio 54”吗?
是的,它可以包含数字,但不能。如果我是对的,这个任务没有提到命名。

下一部分很棘手,因为 房子名称 和以下 3 个整数之间没有唯一的分隔符。

一种方法是找到 3 个尾随整数,然后将剩余的开头文本作为 房屋名称

  while (fgets(buff, sizeof(buff), fi) != NULL) {
    int address, age, power;
    char *p = buff + strlen(buff);  // start as buff end
    p = reverse_scan_int(buff, p, &power);
    p = reverse_scan_int(buff, p, &age);
    p = reverse_scan_int(buff, p, &address);
    if (p) {
      *p = '\0';
      trim(buff);  // remove leading trailing white-space
      printf("house_name:'%s', address:%d age:%d power:%d\n", buff, address,
          age, power);
    } else {
      printf("Failed to parse '%s'\n", buff);
    }
  }

现在我们只需要reverse_scan_int()。示例未经测试的代码想法:

#include <ctype.h>
#include <stdbool.h>
char *reverse_scan_int(const char *begin, char *one_past, int *i) {
  if (one_past == NULL) {
    return NULL;
  }
  // is...() functions work best with unsigned char
  unsigned char *p = (unsigned char *) one_past;
  // Skip trailing whitespace;
  while (p > begin && isspace(p[-1])) {
    p--;
  }
  // Skip digits;
  bool digit_found = false;
  while (p > begin && isdigit(p[-1])) {
    p--;
    digit_found = true;
  }
  if (!digit_found)
    return NULL;
  if (p > begin && (p[-1] == '-' || p[-1] == '+')) {
    p--;
  }
  *i = atoi(p); // More roubust code would use `strtol()`, leave for OP.
  return (char *) p;
}

trim a string 有很多方法,包括this one

【讨论】:

  • 除了使用int 之外,使用bool 有什么好处?我已经考虑过很多次了,bool 的大小没有保证可以比 int 节省几个字节(尽管许多编译器使用小于int 的类型实现它),然后你会得到额外的处理以处理寄存器中较短的类型(尽管如果类型较小,这可能允许处理器优化同一寄存器中的其他内容)。所以我倾向于坚持使用int 作为标志,我缺少任何有说服力的反驳论点吗?可读性?
  • @DavidC.Rankin 公平积分。好处:6.001 对 1/2 打其他。 IMO,对于演示/学习者代码和未来趋势bool 是实现清晰和更优化代码的方法。 int 可能会更优化吗?也许,一个人可以分析,但对于如此小的线性潜在增益来说并不那么重要。 IAC,为了速度,我会比较之前/之后的 p 指针并删除 digit_found,但我发现这更清楚。
  • 从一般的角度来看,我确实认为它增加了可读性,因为它清楚地将变量指定为true/false 指示,而不是简单地作为int。我想这也是一个脑残的事情,直到 C99 Is bool a native C type? 之前都没有 bool,所以它让你思考未来的趋势,对于未来的新程序员来说,这是一个好点。
【解决方案2】:

您的问题很难用sscanf() 解决,因为房屋名称和 3 个数字字段之间没有明确的分隔符。 %s 不合适:它解析一个单词。在您的程序中,sscanf() 实际上无法转换数字并为所有行返回 1,从而在您比较实际未初始化的数值时导致未定义的行为。

这是使用%[ 转换规范的修改版本:

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

#define F_INPUT  "input.txt"
#define EXIT_INPUT_FAIL  1

int main(void) {
    char buff[256];
    char building[100];
    int addr, age, kwh;
    FILE *fi;

    // opening the files and checking if it succeeded
    fi = fopen(F_INPUT, "r");
    if (fi == NULL) {
        printf("Error opening input file \"%s\"", F_INPUT);
        exit(EXIT_INPUT_FAIL);
    }
    while (fgets(buff, sizeof(buff), fi) != NULL) {
        /* parse the building name upto and excluding any digit,
           then accept 3 integral numbers for the address, age and power */
        if (sscanf(buff, "%99[^0-9]%d%d%d", building, &addr, &age, &kwh) != 4) {
            printf("parsing error: %s", buff);
            continue;
        }
        if (kwh < 200) {
            puts(buff);
            printf("Sustainable\n");
        } else
        if (kwh < 300 && age < 40) {
            puts(buff);
            printf("Needs renovation\n");
        } else
        if (kwh > 300 && age > 40) {
            puts(buff);
            printf("IN DEMOLITION LIST\n");
        }
        // allocate structure with building details and append it to the list or array of buildings
    }
    /* close the files when they're not needed anymore */
    fclose(fi);
    // sort the list or array and print it
    // free the list or array
    return 0;
}

【讨论】:

  • 我在教程点上找到了这个例子,sscanf 用空格读取。它和我的差不多,不是吗? tutorialspoint.com/c_standard_library/c_function_sscanf.htm
  • @AlexeyKozlov:此帮助页面中的示例读取 2 个单词和 2 个 %s 转换规范。可以在 %[...] 转换规范中包含空格,但您需要一个分隔符......我刚刚更新了我的答案以使用这种方法,停在数字上。