【问题标题】:Specifying the maximum string length to scanf dynamically in C (like "%*s" in printf)在 C 中动态指定要 scanf 的最大字符串长度(如 printf 中的“%*s”)
【发布时间】:2019-04-03 21:28:56
【问题描述】:

我可以使用这种技术指定scanf 读取到buffer 的最大字符数:

char buffer[64];

/* Read one line of text to buffer. */
scanf("%63[^\n]", buffer);

但是如果我们在编写代码时不知道缓冲区长度怎么办?如果是函数的参数呢?

void function(FILE *file, size_t n, char buffer[n])
{
    /* ... */
    fscanf(file, "%[^\n]", buffer); /* WHAT NOW? */
}

此代码容易出现缓冲区溢出,因为fscanf 不知道缓冲区有多大。

我记得以前看过这个,并开始认为这是解决问题的方法:

fscanf(file, "%*[^\n]", n, buffer);

我的第一个想法是"%*[*^\n]" 中的* 意味着传递了一个参数的最大字符串大小(在本例中为n)。这就是printf*的含义。

当我查看scanf 的文档时,我发现这意味着scanf 应该丢弃[^\n] 的结果。

这让我有些失望,因为我认为能够为scanf 动态传递缓冲区大小将是一个非常有用的功能。

有什么方法可以将缓冲区大小动态传递给scanf

【问题讨论】:

标签: c io size buffer scanf


【解决方案1】:

基本答案

scanf() 中没有 printf() 格式说明符 * 的类似物。

The Practice of Programming,Kernighan 和 Pike 建议使用snprintf() 来创建格式字符串:

size_t sz = 64;
char format[32];
snprintf(format, sizeof(format), "%%%zus", sz);
if (scanf(format, buffer) != 1) { …oops… }

额外信息

将示例升级为完整功能:

int read_name(FILE *fp, char *buffer, size_t bufsiz)
{
    char format[16];
    snprintf(format, sizeof(format), "%%%zus", bufsiz - 1);
    return fscanf(fp, format, buffer);
}

这强调了格式规范中的大小比缓冲区的大小小一(它是可以存储的非空字符数,不计算终止的空值)。请注意,这与fgets() 形成对比,fgets() 的大小(顺便说一下;不是size_t)是缓冲区的大小,而不是小一。有多种改进功能的方法,但它表明了这一点。 (如果您愿意,可以将格式中的s 替换为[^\n]。)

另外,正如Tim Čascomments 中指出的那样,如果您想要(其余)一行输入,通常最好使用fgets() 来读取该行,但请记住它包括输出中的换行符(而%63[^\n] 将换行符留给下一个 I/O 操作读取)。对于更一般的扫描(例如,2 或 3 个字符串),这种技术可能会更好——尤其是在使用 fgets()getline() 然后 sscanf() 来解析输入时。

此外,由 Microsoft(或多或少)实施并在 ISO/IEC 9899-2011(C11 标准)附录 K 中标准化的 TR 24731-1“安全”功能明确要求长度:

if (scanf_s("%[^\n]", buffer, sizeof(buffer)) != 1)
    ...oops...

这可以避免缓冲区溢出,但如果输入太长,可能会产生错误。大小可以/应该像以前一样在格式字符串中指定:

if (scanf_s("%63[^\n]", buffer, sizeof(buffer)) != 1)
    ...oops...

if (scanf_s(format, buffer, sizeof(buffer)) != 1)
    ...oops...

请注意,对于使用生成的格式字符串的代码,必须忽略或抑制有关“非常量格式字符串”的警告(来自某些编译器的某些标志集)。

【讨论】:

  • 对于"%64[^\n]" 的特定情况,更好的解决方案是fgets(buf, 64, stdin) --- 它确实如此,除了您可以将长度提供为“正确" 论据。
  • @Tim fgets(buf, 64, stdin)完全相同。 fgets 还为您提供换行符。
  • @elias:哦,对了,忘了。不过很容易修复:buf[strcspn(buf, "\n")] = 0;
  • @chux:有效点。通常的明智修复是在格式字符串的开头包含一个空格,因此会跳过前导空格。请注意,如果字符串位于格式字符串的末尾并且输入是交互式的(格式字符串中的尾随换行符或空白是交互式灾难;输入不会终止,直到输入了非空白字符)。
  • @a3f:由你决定。我没有 Windows 机器,所以我无法验证我要说的内容,但是……如果您使用的是 scanf_s() 或其亲属之一以及 %c%s%[ 转换规范被限制为例如 20 个字符(例如 %20s)并且用户键入 21 个字符,第 21 个字符由下一个转换规范处理(可能不起作用)。另一方面,如果您只有%s,则第 21 个字符会触发运行时约束违规,并且所有输入实际上都会丢失并且扫描完成。您可以选择自己喜欢的行为。
【解决方案2】:

scanf 系列函数中确实没有可变宽度说明符。替代方法包括动态创建格式字符串(尽管如果宽度是编译时常量,这似乎有点傻)或简单地接受幻数。一种可能性是使用预处理器宏来指定缓冲区和格式字符串宽度:

#define STR_VALUE(x) STR(x)
#define STR(x) #x

#define MAX_LEN 63

char buffer[MAX_LEN + 1];
fscanf(file, "%" STR_VALUE(MAX_LEN) "[^\n]", buffer);

【讨论】:

  • 当缓冲区长度是函数的参数时,这显然不起作用,但在缓冲区长度已知预处理时间的情况下使用它是一个很好的技巧。
  • @elias 是的,因为它是用预处理器完成的,所以值必须是一个预处理器宏,实际上只有一个简单的整数文字(即使STR_VALUE(BUFFER_SIZE - 1) 也不起作用)。
【解决方案3】:

另一个选择是#define字符串的长度:

#define STRING_MAX_LENGTH "%10s"

#define DOUBLE_LENGTH "%5f"

【讨论】:

  • 请注意,%5d 是一个整数,而不是顾名思义的 double
  • 我不认识问题或帖子。我编辑匹配double类型
猜你喜欢
  • 1970-01-01
  • 2020-09-11
  • 2021-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-21
  • 1970-01-01
相关资源
最近更新 更多