我想从 C 语言中的用户那里获取字符串输入。
我知道如何使用scanf
来获取字符串输入。我知道如何使用malloc
和realloc
为变量分配空间。
问题是当用户输入字符串时,我不一定知道我需要保留空间的大小。
例如,如果他们写James
我需要malloc((sizeof(char) * 5)
但他们可能写了Bond
在这种情况下,我只需要malloc(sizeof(char) * 4)
.
只是我应该确保事先分配足够的空间(例如malloc(sizeof(char) * 100)
)。
然后scanf
是否在引擎盖下进行任何realloc
修剪,或者这是我需要修复的内存泄漏?
你有两个误解,你正在努力解决。首先scanf()
不会以任何方式修改存储(为了讨论的目的省略了非标准"%a"
,后来更名为"%m"
说明符)。其次,您忘记提供存储length + 1
字符以确保为空终止字符留出空间。
在你的陈述中">例如,如果他们写'詹姆斯',我需要malloc((sizeof(char)*5)
" - 不,不,你需要malloc (6)
为James
提供空间.另请注意,sizeof (char)
定义为1
,应省略。
至于如何读取字符串,您通常希望避免scanf()
,即使在使用scanf()
时,除非您正在读取空格分隔的单词,否则您也不想使用"%s"
转换说明符,一旦遇到空格就停止读取,从而无法读取"James Bond"
。此外,您还有在致电scanf()
后stdin
中未读的内容的问题。
使用"%s"
读取时,'n'
字符处于未读取状态stdin
。这是一个陷阱,如果使用不忽略前导空格的输入函数(即任何面向字符或面向行的输入函数),则会在下次尝试读取时咬住您。这些陷阱,以及与scanf()
使用相关的许多其他陷阱,是鼓励新的C程序员使用fgets()
来读取用户输入的原因。
使用足够大的缓冲区(如果没有,则使用简单的循环),fgets()
每次调用时都会消耗整行输入,确保该行中没有未读的内容。唯一需要注意的是,fgets()
读取并在其填充的缓冲区中包含尾随'n'
。您只需通过调用strcspn()
来修剪尾随换行符(这也可以同时为您提供字符串的长度)
如上所述,解决"我不知道我有多少个字符?"问题的一种方法是使用固定大小的缓冲区(字符数组),然后反复调用fgets()
,直到在数组中找到'n'
。这样,您可以通过确定读入固定大小缓冲区的字符数来为该行分配最终存储空间。固定大小的缓冲区是否10
并且要读取的字符数100
并不重要,只需在循环中调用fgets()
,直到读取的字符数小于完整固定大小缓冲区的值。
现在理想情况下,你会调整临时固定大小缓冲区的大小,以便你的输入适合第一次,从而消除了循环和重新分配的需要,但如果猫踩在键盘上 - 你就被覆盖了。
让我们看一个示例,其功能类似于 CS50get_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 */
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'
的输入长度),并以该长度以nul终止字符串,用nul 终止字符' '
覆盖'n'
字符(这只是普通的旧 ASCII0
)。长度保存在n
.如果在删除'n'
后该行为空,则无需执行任何其他操作,并且该函数返回当时s
中的任何内容。
如果存在字符,则使用临时指针realloc()
新字符 (+1
) 的存储。验证成功realloc()
后,新字符将复制到存储的末尾,缓冲区中字符的总长度将保存在used
中,该长度用作字符串开头的偏移量。重复此操作,直到您用完要读取的字符并返回包含字符串的分配块(如果未输入任何字符,则返回NULL
)
(注意:您可能还希望将指向size_t
的指针作为参数传递,该参数可以在返回之前更新到最终长度,以避免再次计算返回字符串的长度 - 留给您)
在查看示例之前,让我们向函数添加调试输出,以便它告诉我们总共分配了多少个字符。只需在退货前添加下面的printf()
,例如
}
printf (" allocated: %zun", 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);
}
}
示例使用/输出
在1024
MAXC
的情况下,除非猫踩在键盘上,否则就没有机会需要循环,因此所有输入都被读入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
也可以。唯一更改的是循环重新分配存储并将临时缓冲区的内容复制到最终存储的次数。例如,在10
时MAXC
,用户不会知道以下方面的区别:
$ ./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
例如2
或8
分配一些初始大小,并跟踪used
字符数,然后在used == allocated
时将分配的大小加倍并继续。您可能希望分配存储块,因为您不希望为每个添加的字符realloc()
(我们将省略为什么今天使用mmap
edmalloc()
不如过去的讨论)
仔细查看,如果您有其他问题,请告诉我。
我个人使用 malloc 方法,但您需要注意另一件事,然后您还可以限制 scanf 中接受的 %s 字符以匹配您的缓冲区。
char *string = (char*) malloc (sizeof (char) * 100);
scanf ("%100s", string);
然后,您可以在获取字符串大小后重新分配内存,方法是使用字符串函数 strlen,然后为终止符添加 1。
此问题有多种方法:
-
使用任意最大长度,将输入读入本地数组并根据实际输入分配内存:
#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] = ' '; } } return p; }