【问题标题】:Determining memory allocation size for string input in C (scanf)确定 C 中字符串输入的内存分配大小(scanf)
【发布时间】:2021-03-14 01:10:25
【问题描述】:

我想从 C 中的用户那里获取字符串输入。

我知道如何使用scanf 来获取字符串输入。我知道如何使用mallocrealloc 为变量分配空间。

问题是当用户输入一个字符串时,我不一定知道我需要为其预留空间的大小。

例如,如果他们写James,我需要malloc((sizeof(char) * 5),但他们可能写Bond,在这种情况下我只需要malloc(sizeof(char) * 4)

是不是我应该确保事先分配足够的空间(例如malloc(sizeof(char) * 100))。

然后scanf 做了任何realloc 在引擎盖下的修剪还是我要修复的内存泄漏?

【问题讨论】:

  • 分配最大大小的缓冲区是一种常见的方法。虽然会建议使用像fgets 这样的函数,它可以更容易地防止超出缓冲区大小。 scanf 不会以任何方式更改缓冲区大小。但这本身并不是内存泄漏,因为它仍然可以被释放。这是一个有点浪费的记忆。取决于可能是可接受的权衡与保持简单的上下文。
  • 只需使用固定大小的缓冲区并根据需要添加到分配的缓冲区中。说char tmp[1024], *string = NULL; size_t used = 0;,然后循环将输入读入tmp,例如while (1) { if (!fgets(tmp, sizeof tmp, stdin)) break; size_t n; tmp[(n = strcspn (tmp, "\n"))] = 0; string = realloc (string, used + n + 1); memcpy (string + used, tmp, n + 1); used += n; if (n + 1 < sizeof tmp) break; }(或类似的)
  • "size for a string" - 对于"James",你需要malloc((sizeof(char)*6),而不是5 - 不要忘记nul-的空间终止字符——否则你没有字符串,你有一个分配的块来保存字符。注意:sizeof(char) 定义为1,应省略。不要使用scanf() 读取字符串,而是使用fgets() 并使用strcspn() 修剪'\n'
  • 好点大卫,我喜欢这个澄清。

标签: c string malloc scanf char-pointer


【解决方案1】:

我个人使用 malloc 方法,但您需要注意一件事,您还可以在 scanf 中限制 %s 接受的字符以匹配您的缓冲区。

char *string = (char*) malloc (sizeof (char) * 100);
scanf ("%100s", string);

你可以在得到字符串大小后重新分配内存,方法是使用字符串函数strlen,然后为终止符加1。

【讨论】:

  • 如果目标数组是 100 字节长,你应该传递 99 作为空终止符之前要存储的最大字符数:scanf("%99s", string);
【解决方案2】:

你有两个误解,你正在努力解决。首先scanf() 不以任何方式修改存储(为了讨论的目的省略了非标准的"%a",后来重命名为"%m" 说明符)。其次,您忘记提供 length + 1 存储字符以确保 null-terminating 字符的空间。

在您的声明中“例如,如果他们写“詹姆斯”,我需要malloc((sizeof(char)*5)” - 不,不,您需要malloc (6)James\0 提供空间。另请注意,sizeof (char) 定义为 1,应省略。

关于如何读取字符串,您通常希望避免使用scanf(),即使使用scanf(),除非您正在读取空格分隔的单词,否则您不想使用@ 987654332@ 转换说明符,一旦遇到空格就停止读取,从而无法读取"James Bond"。此外,在您致电scanf() 后,您还会遇到stdin 中未读内容的问题。

使用"%s" 读取时,'\n' 字符留在stdin 中未读。如果使用不忽略前导空格的输入函数(即任何面向字符或面向行的输入函数),这是一个陷阱,它将在您下次尝试阅读时咬住您。这些陷阱以及与scanf() 使用相关的许多其他缺陷是鼓励新C 程序员使用fgets() 来读取用户输入的原因。

使用足够大的缓冲区(如果没有,则使用简单的循环)fgets() 将在每次调用时消耗一整行输入,确保该行中没有任何未读的内容。唯一需要注意的是fgets() 读取并在其填充的缓冲区中包含尾随'\n'。您只需通过调用strcspn() 来修剪尾随换行符(它还可以同时为您提供字符串的长度)

如上所述,解决“我不知道我有多少个字符?”的一种方法问题是使用固定大小的缓冲区(字符数组),然后重复调用fgets(),直到在数组中找到'\n'。这样,您可以通过确定读入固定大小缓冲区的字符数来为该行分配最终存储空间。如果您的固定大小缓冲区是 10 并且您有要读取的 100 字符并不重要,您只需在循环中调用 fgets() 直到您读取的字符数小于完整的固定大小缓冲区值得。

现在理想情况下,您将调整临时固定大小缓冲区的大小,以便您的输入适合第一次,从而消除循环和重新分配的需要,但如果猫踩到键盘 - 您就被覆盖了。

我们来看一个例子,功能类似于 CS50 的get_string() 函数。它允许用户为用户提供提示,并为结果读取和分配存储空间,返回一个指向已分配块的指针,其中包含用户负责在完成后调用 free() 的字符串。

#define MAXC 1024       /* if you need a constant, #define one (or more) */

char *getstr (const char *prompt)
{
    char tmp[MAXC], *s = NULL;                      /* fixed size buf, ptr to allocate */
    size_t n = 0, used = 0;                         /* length and total length */
    
    if (prompt)                                     /* prompt if not NULL */
        fputs (prompt, stdout);
    
    while (1) { /* loop continually */
        if (!fgets (tmp, sizeof tmp, stdin))        /* read into tmp */
            return s;
        tmp[(n = strcspn (tmp, "\n"))] = 0;         /* trim \n, save length */
        if (!n)                                     /* if empty-str, break */
            break;
        void *tmpptr = realloc (s, used + n + 1);   /* always realloc to temp pointer */
        if (!tmpptr) {                              /* validate every allocation */
            perror ("realloc-getstr()");
            return s;
        }
        s = tmpptr;                                 /* assign new block to s */
        memcpy (s + used, tmp, n + 1);              /* copy tmp to s with \0 */
        used += n;                                  /* update total length */
        if (n + 1 < sizeof tmp)                     /* if tmp not full, break */
            break;
    }
    
    return s;       /* return allocated string, caller responsible for calling free */
}

上面,MAXC 个字符的固定大小缓冲区用于读取用户的输入。一个连续循环调用fgets() 将输入读入缓冲区tmpstrcspn() 被称为tmp 的索引以查找不包括'\n' 字符(不包括'\n' 的输入的长度)和nul-terminates 的字符数该长度的字符串用 nul-terminating 字符 '\0' 覆盖 '\n' 字符(这只是普通的旧 ASCII 0)。长度保存在n 中。如果删除 '\n' 后该行为空,则无需执行任何操作,该函数将返回当时 s 中的任何内容。

如果存在字符,则 临时 指针用于 realloc() 存储新字符 (+1)。验证realloc() 成功后,将新字符复制到存储的末尾,并将缓冲区中字符的总长度保存在used 中,用作字符串开头的偏移量。重复此过程,直到您用完要读取的字符并返回包含该字符串的分配块(如果未输入任何字符,则返回 NULL

(注意:您可能还希望将指向size_t 的指针作为参数传递,该参数可以在返回之前更新为最终长度,以避免再次计算返回字符串的长度——留给你)

在查看示例之前,让我们将调试输出添加到函数中,以便告诉我们总共分配了多少字符。只需在返回前在下方添加printf(),例如

    }
    printf (" allocated: %zu\n", used?used+1:used); /* (debug output of alloc size) */
    
    return s;       /* return allocated string, caller responsible for calling free */
}

一个简短的例子,循环读取输入,直到在一个空行上按下 Enter 导致程序在释放所有内存后退出:

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

/* insert getstr() function HERE */

int main (void) {
    
    for (;;) {
        char *s = getstr ("enter str: ");
        if (!s)
            break;
        puts (s);
        putchar ('\n');
        free (s);
    }
}

使用/输出示例

MAXC1024 没有机会循环,除非猫踩在键盘上,所以所有输入都被读入 tmp,然后分配存储空间以准确保存每个输入:

$ ./bin/fgetsstr
enter str: a
  allocated: 2
a

enter str: ab
  allocated: 3
ab

enter str: abc
  allocated: 4
abc

enter str: 123456789
  allocated: 10
123456789

enter str:
  allocated: 0

MAXC 设置为210 也可以。唯一改变的是循环重新分配存储并将临时缓冲区的内容复制到最终存储的次数。例如。使用MAXC 10,用户不会知道以下区别:

$ ./bin/fgetsstr
enter str: 12345678
 allocated: 9
12345678

enter str: 123456789
 allocated: 10
123456789

enter str: 1234567890
 allocated: 11
1234567890

enter str: 12345678901234567890
 allocated: 21
12345678901234567890

enter str:
 allocated: 0

在上面你已经强制while (1) 循环对每个10 字符或更多字符的字符串执行两次。因此,当您想将MAXC 设置为一些合理的大小以避免循环时,考虑到在大多数 x86 或 x86_64 计算机上至少有 1M 的函数堆栈,1K 缓冲区就可以了。如果您正在为存储空间有限的微控制器编程,您可能希望减小尺寸。

虽然您也可以为tmp 分配,但实际上没有必要,而且使用固定大小的缓冲区对于坚持标准-C 来说非常简单。如果您有可用的 POSIX,那么getline() 已经为您拥有的任何大小输入提供了自动分配功能。这是fgets() 的另一个很好的替代方案——但 POSIX 不是标准 C(尽管它被广泛使用)

另一个不错的选择是简单地循环使用getchar() 一次读取一个字符,直到达到'\n'EOF。在这里,您只需为s 分配一些初始大小,比如28,并跟踪used 的字符数,然后在used == allocated 时将分配的大小加倍并继续。您可能希望分配存储块,因为您不想为添加的每个字符分配realloc()(我们将省略讨论为什么今天使用mmaped malloc() 比过去不那么正确)

查看一下,如果您还有其他问题,请告诉我。

【讨论】:

    【解决方案3】:

    解决这个问题有多种方法:

    • 使用任意最大长度,将输入读入本地数组并根据实际输入分配内存:

      #include <stdio.h>
      #include <string.h>
      
      char *readstr(void) {
          char buf[100];
          if (scanf("%99s", buf) == 1)
              return strdup(buf);
          else
              return NULL;
      }
      
    • 如果支持且允许,请使用非标准库扩展。例如 GNU libc 有一个 m 修饰符就是为了这个目的:

      #include <stdio.h>
      
      char *readstr(void) {
          char *p;
          if (scanf("%ms", &p) == 1)
              return p;
          else
              return NULL;
      }
      
    • 一次读取一个字节并按需重新分配目标数组。这是一个简单的方法:

      #include <ctype.h>
      #include <stdio.h>
      #include <stdlib.h>
      
      char *readstr(void) {
          char *p = NULL;
          size_t i = 0;
          int c;
          while ((c = getchar()) != EOF) {
              if (isspace(c)) {
                  if (i > 0) {
                      ungetc(c, stdin);
                      break;
                  }
              } else {
                  char *newp = realloc(p, i + 2);
                  if (newp == NULL) {
                      free(p);
                      return NULL;
                  }
                  p = newp;
                  p[i++] = c;
                  p[i] = '\0';
              }
          }
          return p;
      }
      

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多