如何在C语言中读取用户输入的字符串?

52

我想使用C程序读取用户输入的名称。

为此,我编写了以下代码:

char name[20];

printf("Enter name: ");
gets(name);

但是使用 gets 不好,那么有什么更好的方法吗?

8个回答

95

在编写代码时,我们绝对不应该使用gets(或具有未限定字符串大小的scanf),因为这会导致缓冲区溢出。相反,我们应该使用带有stdin句柄的fgets函数,它允许我们限制将放入缓冲区中的数据。

以下是我用于从用户获取输入行的小代码片段:

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

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}

这让我能够设置最大大小,如果输入的数据过多,它会检测到,并将剩余的行刷新,以便不影响下一个输入操作。
您可以使用以下内容进行测试:
// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

1
请问scanf实现的系统库是否可以防止命令溢出(我知道如果开发人员没有检查输入,程序内部可能会发生溢出,但是系统库是安全的,对吗?)。 - Marm0t
8
如果你将一个40字节行输入到一个20字节的缓冲区中,而使用 scanf("%s") 函数,那么你就会出现问题。scanf 函数的整个目的在于扫描格式化的内容,而用户输入很少有比它更 非格式化 的了 :-) - paxdiablo
7
@Marm0t - 从以下问题的角度考虑一下:如果所有实现得到的只是一个指向内存切片(类型转换为char *)的指针,而没有任何参数告诉实现目标缓冲区的大小,那么应该如何防止溢出? - luis.espinal
+1 给 paxdiablo,因为他阐明了 scanf 和 gets 的问题。 - luis.espinal
@paxdiablo - 很棒的建议,我之前给出的scanf建议让我回忆起了我不好的C语言时代。我推荐使用strn函数来避免缓冲区溢出问题,应该注意到自己的scanf溢出问题。你的解决方案加一分,你关于20字节/40字节缓冲区示例的评论也加一分! - pstrjds
2
@pstrjds,从控制台使用scanf并不总是不好的,它可以用于一些有限的事情,比如数字输入(和家庭作业)等。但即使在这种情况下,它也不像生产质量应该具备的那样健壮。即使我需要使用类似scanf的操作来解析输入时,我也会将其读入缓冲区中,然后再从那里使用sscanf进行解析。 - paxdiablo

21

我认为读取用户输入的字符串的最佳和最安全的方式是使用getline()

下面是一个如何实现的例子:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    char *buffer = NULL;
    int read;
    unsigned int len;
    read = getline(&buffer, &len, stdin);
    if (-1 != read)
        puts(buffer);
    else
        printf("No line read...\n");

    printf("Size read: %d\n Len: %d\n", read, len);
    free(buffer);
    return 0;
}

2
read = getline(&buffer, &len, stdin);会给GCC警告,例如:gcc -Wall -c "getlineEx2.c" getlineEx2.c: 在函数'main'中: getlineEx2.c:32:5: 警告: 从不兼容的指针类型传递参数2给'getline' [默认启用] read = getline(&buffer, &len, stdin); ^ 在文件/usr/include/stdio.h:29:0中包含, 来自getlineEx2.c:24: /usr/include/sys/stdio.h:37:9: 注意: 预期是'size_t *'但实参是'unsigned int *' ssize_t _EXFUN(getline, (char **, size_t *, FILE *)); ^ 编译成功完成。 - rpd
2
只是更新一下,现在使用getline需要将len设置为size_t或unsigned long。 - Wes
2
缺点:仅支持POSIX而不是ANSI C。 - Ciro Santilli OurBigBook.com

7
在POSIX系统中,如果可用的话,您应该使用getline
您还可以使用Chuck Falconer的公共领域ggets函数,它提供了更接近于gets但没有问题的语法。(Chuck Falconer的网站不再可用,尽管archive.org有一份拷贝,我也制作了自己的ggets页面。)

有一个警告,即它不能正常处理以 CR 结尾的文件。这种情况并不像你想象的那么少见(在遇到一个充满这种文件的文件夹后,他说道)。很遗憾,他们没有允许 getdelim/getline 接受一个分隔符列表而不是一个单独的整数,这是一个被错过的机会。 - Maury Markowitz
@MauryMarkowitz 它可以在使用CR作为本地行尾格式的系统上运行。文本模式流将把任何本地行尾类型转换为\n - jamesdlin

4
我找到了一个简单而好的解决方案:
char*string_acquire(char*s,int size,FILE*stream){
    int i;
    fgets(s,size,stream);
    i=strlen(s)-1;
    if(s[i]!='\n') while(getchar()!='\n');
    if(s[i]=='\n') s[i]='\0';
    return s;
}

基于fgets,但去除了'\n'和stdin的额外字符(替换fflush(stdin)不适用于所有操作系统,如果需要在此之后获取字符串,则非常有用)。


1
应该使用 fgetc 而不是 getchar,这样它就可以使用提供的 stream 而不是 stdin - jamesdlin

2
使用 scanf 函数移除输入字符串前的任何空格并限制要读取的字符数:
#define SIZE 100

....

char str[SIZE];

scanf(" %99[^\n]", str);

/* Or even you can do it like this */

scanf(" %99[a-zA-Z0-9 ]", str);

如果您不使用 scanf 限制要读取的字符数量,它可能会像 gets 一样危险。

我可以用一个变量来代替在 scanf(" %99[^\n]", str); 中手动输入 99 吗? - undefined

1
在BSD系统和Android上,您也可以使用fgetln
#include <stdio.h>

char *
fgetln(FILE *stream, size_t *len);

就像这样:
size_t line_len;
const char *line = fgetln(stdin, &line_len);

line没有以空字符结尾,并且在末尾包含\n(或者根据您的平台使用的内容)。在流上进行下一个I/O操作后,它将变为无效。您可以修改返回的line缓冲区。


0

ANSI C 未知最大长度的解决方案

直接从 Johannes Schaub 的https://dev59.com/pHRC5IYBdhLWcg3wYP-h#314422中复制即可。

使用完毕后不要忘记free返回的指针。

char * getline(void) {
    char * line = malloc(100), * linep = line;
    size_t lenmax = 100, len = lenmax;
    int c;
    
    if(line == NULL)
        return NULL;

    for(;;) {
        c = fgetc(stdin);
        if(c == EOF)
            break;

        if(--len == 0) {
            len = lenmax;
            char * linen = realloc(linep, lenmax *= 2);

            if(linen == NULL) {
                free(linep);
                return NULL;
            }
            line = linen + (line - linep);
            linep = linen;
        }

        if((*line++ = c) == '\n')
            break;
    }
    *line = '\0';
    return linep;
}

这段代码使用malloc来分配100个字符的内存空间。然后从用户逐个获取字符。如果用户达到了101个字符,它会使用realloc将缓冲区扩大到200个字符。当达到201时,它将再次加倍到400,以此类推,直到内存不足。

之所以选择加倍而不是每次只增加100大小,是因为使用realloc增加缓冲区大小可能导致旧缓冲区的复制,这是一项潜在的昂贵操作。

数组必须在内存中是连续的,因为我们希望能够通过内存地址高效地随机访问它们。因此,如果我们在RAM中有:

content     buffer[0] | buffer[1] | ... | buffer[99] | empty | empty | int i
RAM address 1000      | 1001      |     | 1100       | 1101  | 1102  | 1103

我们不能只增加buffer的大小,因为这会覆盖我们的int i。所以realloc需要在内存中找到另一个有200个空闲字节的位置,然后将旧的100个字节复制到那里,并释放旧的100个字节。

通过倍增而不是加法,我们很快就能达到当前字符串大小的数量级,因为指数增长非常快,所以只需要进行合理数量的复制。


-2
你可以使用scanf函数来读取字符串。
scanf("%[^\n]",name);

我不知道其他更好的选项来接收字符串,


不要使用 scanf,因为它非常难以正确使用。这种用法特别危险,因为它不限制输入并且很容易溢出 name 缓冲区。 - jamesdlin

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