在C语言中确定字符串输入(scanf)的内存分配大小

4

我希望能够从用户处获取C中的字符串输入。

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

问题在于当用户输入一个字符串时,我不一定知道需要为它保留多少空间。

例如,如果他们写了James,我就需要malloc((sizeof(char) * 5),但他们可能会写Bond,那么我只需malloc(sizeof(char) * 4)

难道我应该事先确保分配足够的空间(例如malloc(sizeof(char) * 100))吗?

scanf是否会在幕后进行任何realloc的修剪,还是这对我来说是一个内存泄漏需要自己解决?


1
分配最大大小的缓冲区是一种常见的方法。虽然建议使用像 fgets 这样的函数,它可以更轻松地防止超出缓冲区大小。scanf 不会以任何方式改变缓冲区大小。但这不是一个内存泄漏,因为它仍然可以被释放。它实际上是一些浪费的内存。根据上下文,这可能是可接受的权衡,而不是保持简单。 - kaylum
只需使用固定大小的缓冲区,并根据需要添加到已分配的缓冲区中。例如,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; }(或类似的内容)。 - David C. Rankin
1
对于字符串的大小,对于“James”,您需要使用malloc((sizeof(char)*6)而不是5 - 不要忘记为空字符留出空间 - 否则您没有一个字符串,而是一个持有字符的分配块。注意:sizeof(char)被定义为1,应省略。不要使用scanf()读取字符串,而是使用fgets()并使用strcspn()修剪'\n' - David C. Rankin
David,你的观点很好,我喜欢那种澄清。 - gcr
3个回答

3
你有两个误解需要解决。首先,scanf() 不修改存储方式(讨论用途省略非标准的 "%a",后来改名为 "%m" 格式说明符)。其次,你忘记提供 length + 1 字符的存储空间,以确保有足够的空间容纳 null 结尾字符
在你的语句中 "例如,如果他们写了“James”,我需要 malloc((sizeof(char)*5)" - 不,不是这样,你需要用 malloc(6) 来提供空间给James\0。注意到 sizeof(char) 被定义为 1,应省略。
至于如何读取字符串,通常应避免使用 scanf()。即使使用scanf(),除非读取的是以空格分隔单词,否则不要使用 "%s" 转换说明符,因为它会在遇到空格时停止读取,从而无法读取 "James Bond"。此外,你需关注 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() 将输入读入缓冲区tmp中。strcspn() 作为索引被调用到tmp中,以查找不包括'\n'字符的数量(输入长度不包含'\n')并将字符串在该长度处添加 null 结束符,并用null结束符 '\0'(即普通的ASCII码0)覆盖'\n'字符。长度保存在 n 中。如果删除'\n'后行为空,则无需做任何操作,并返回函数当前 s 中的内容。
如果存在字符,则使用临时指针 realloc() 存储新字符(+1)。验证realloc()是否成功后,将新字符复制到存储的末尾,并将缓冲区中字符的总长度保存在used中,它被用作字符串开头的偏移量。重复这个过程直到没有字符可读,然后返回包含字符串的分配块(如果没有输入字符,则返回NULL)。
(注意:您可能还想传递一个指向 的指针作为参数,在返回之前可以更新为最终长度,以避免再次计算返回字符串的长度 - 这留给您决定)
在查看示例之前,让我们向函数添加调试输出,以便告诉我们总共分配了多少个字符。只需在返回之前添加下面的 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);
    }
}

示例用法/输出

当`MAXC`设置为`1024`时,除非猫踩在键盘上,否则不需要循环,因此所有输入均读入到`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 设置为 2 或者 10 都可以。唯一改变的是循环重新分配存储空间和将临时缓冲区的内容复制到最终存储空间的次数。例如,将 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()(我们将省略为什么在过去这一点比现在用 mmapmalloc() 要少)
请仔细查看并让我知道您是否有进一步的问题。

0

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

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

在使用字符串函数strlen获取字符串长度并加上终止符1后,您可以重新分配内存。


1
如果目标数组长度为100个字节,你应该传递99作为存储在空终止符之前的最大字符数:scanf("%99s", string); - chqrlie

0

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

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

    #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;
    }
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接