你有两个误解需要解决。首先,
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
char *getstr (const char *prompt)
{
char tmp[MAXC], *s = NULL;
size_t n = 0, used = 0;
if (prompt)
fputs (prompt, stdout);
while (1) {
if (!fgets (tmp, sizeof tmp, stdin))
return s;
tmp[(n = strcspn (tmp, "\n"))] = 0;
if (!n)
break;
void *tmpptr = realloc (s, used + n + 1);
if (!tmpptr) {
perror ("realloc-getstr()");
return s;
}
s = tmpptr;
memcpy (s + used, tmp, n + 1);
used += n;
if (n + 1 < sizeof tmp)
break;
}
return s;
}
以上,使用大小为
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>
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
分配一些初始大小,比如
2
或
8
,并跟踪使用的字符数量
used
,然后在
used == allocated
时将分配的大小加倍并继续执行。您需要分配存储块,因为您不希望对每个添加的字符
realloc()
(我们将省略为什么在过去这一点比现在用
mmap
的
malloc()
要少)
请仔细查看并让我知道您是否有进一步的问题。
fgets
这样的函数,它可以更轻松地防止超出缓冲区大小。scanf
不会以任何方式改变缓冲区大小。但这不是一个内存泄漏,因为它仍然可以被释放。它实际上是一些浪费的内存。根据上下文,这可能是可接受的权衡,而不是保持简单。 - kaylumchar 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. Rankinmalloc((sizeof(char)*6)
而不是5
- 不要忘记为空字符留出空间 - 否则您没有一个字符串,而是一个持有字符的分配块。注意:sizeof(char)
被定义为1
,应省略。不要使用scanf()
读取字符串,而是使用fgets()
并使用strcspn()
修剪'\n'
。 - David C. Rankin