如何在C语言中从控制台读取一行?

122

在C语言控制台程序中,读取完整行的最简单方法是什么?输入文本的长度可能不固定,我们无法对其内容作出任何假设。

14个回答

91
你需要动态内存管理,并使用fgets函数读取你的一行。然而,似乎没有办法知道它读取了多少个字符。所以你使用fgetc()函数。
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;
}

警告:千万不要使用gets()!它不会进行边界检查,可能会导致缓冲区溢出。

4
你可以通过使用带有缓冲区的fgets函数,并检查结尾是否有换行符,来提高效率。如果没有换行符,则重新分配累加缓冲区,将内容复制到其中,然后再次调用fgets。 - Paul Tomblin
3
这个函数需要进行修改:在重新分配内存后,应该将"len = lenmax;"这行代码放到realloc函数之前或者改为"len = lenmax >> 1;"——或者其他等效的代码,以考虑到已经使用了一半长度的情况。 - Matt Gallagher
1
@Johannes,针对你的问题,@Paul的方法在大多数(即可重入的)libc实现中可能会更快,因为你的方法隐式地为每个字符锁定stdin,而他的方法每个缓冲区只锁定一次。如果线程安全不是问题,但性能是,则可以使用不太便携的fgetc_unlocked - vladr
@NGix 抱歉回复晚了,你可能已经发现了:是的。fgetc(stdin) 可能会返回 EOF,特别是当 stdin 从文件中进行管道传输或用户按下关闭 stdin 的键组合时(例如在 Linux 上按下 CTRL+d 或在 Windows 上按下 CTRL+z)。 - autistic
7
请注意,这个getline()函数与POSIX标准中的getline()函数不同。 - Jonathan Leffler
显示剩余13条评论

34
如果您使用 GNU C 库或其他符合 POSIX 标准的库,则可以使用 getline() 函数,并将 stdin 作为文件流参数传递给它。

26
一个非常简单但不安全的静态分配读取行的实现方式:
char line[1024];

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

一个更加安全的实现方式,避免了缓冲区溢出的可能性,但是可能会存在没有读取整行的情况:
char line[1024];

scanf("%1023[^\n]", line);

不是在声明变量时指定的长度和在格式字符串中指定的长度之间的“差一”问题。这是一个历史遗留问题。

23

所以,如果您正在寻找命令参数,请查看Tim的答案。 如果您只想从控制台读取一行:

#include <stdio.h>

int main()
{
  char string [256];
  printf ("Insert your full address: ");
  gets (string);
  printf ("Your address is: %s\n",string);
  return 0;
}

是的,它不安全,可能会发生缓冲区溢出,它不检查文件结尾,也不支持编码和许多其他功能。 事实上,我甚至没有考虑它是否具备这些功能。 我同意,我有点犯了错误 :) 但是......当我看到像“如何在C中从控制台读取一行?”这样的问题时,我认为一个人需要简单的东西,比如gets()而不是像上面那样100行的代码。 实际上,我认为,如果你试着在现实中编写那100行的代码,你会犯更多的错误,而如果你选择了gets,你会少犯一些错误;)


3
-1,不应使用gets()函数,因为它不执行边界检查。 - unwind
9
另一方面,如果您正在为自己编写程序,只需读取输入,那么这是完全可以的。一个程序需要多少安全性取决于规格说明——您不必每次都将其作为优先事项。 - Martin Beckett
4
@Tim - 我想保留所有的历史记录 :) - Paul Kapustin
7
被踩了。gets 已经不存在了,所以在 C11 中这个方法行不通。 - Antti Haapala -- Слава Україні
2
如果这个函数甚至接近生产环境,最终用户将会感到非常不满或者被黑客攻击。这不是一个好的函数,永远不应该使用。 - wizzwizz4
显示剩余3条评论

16

getline 可运行示例

getline 被提及 在这个答案中,但这里提供一个示例。

它是POSIX 7标准函数,为我们分配内存,并在循环中漂亮地重用已分配的缓冲区。

对于指针初学者,阅读此文:为什么getline的第一个参数是指向指针char**而不是char *?

main.c

#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char *line = NULL;
    size_t len = 0;
    ssize_t read = 0;
    while (1) {
        puts("enter a line");
        read = getline(&line, &len, stdin);
        if (read == -1)
            break;
        printf("line = %s", line);
        printf("line length = %zu\n", read);
        puts("");
    }
    free(line);
    return 0;
}

编译并运行:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out

结果:这会显示在终端上:

enter a line

然后如果您输入:

asdf

输入并按下回车键,将会出现以下内容:

line = asdf
line length = 5

接着是另一个:

enter a line

或者从管道输入到标准输入:

printf 'asdf\nqwer\n' | ./main.out

提供:

enter a line
line = asdf
line length = 5

enter a line
line = qwer
line length = 5

enter a line

在Ubuntu 20.04上进行测试。

glibc实现

没有POSIX?也许你想看看glibc 2.23实现

它解析为getdelim,这是一个简单的POSIX超集,具有任意行终止符。

每当需要增加时,它会将分配的内存翻倍,并且看起来是线程安全的。

它需要一些宏扩展,但你不太可能做得更好。


在这里,len 的目的是什么,当读取时也提供了长度? - Honinbo Shusaku
1
@Abdul,请查看man getlinelen是现有缓冲区的长度,0是魔法数字,表示要分配内存。read是读取的字符数。缓冲区大小可能大于read - Ciro Santilli OurBigBook.com

11

您可能需要使用字符循环(getc())来确保没有缓冲区溢出并且不截断输入。


6

建议使用getchar()从控制台读取数据,直到返回结束行或EOF的值,构建自己的缓冲区。如果无法设置合理的最大行大小,则可以动态增长缓冲区。

您还可以使用fgets作为安全的方式来获取C空终止字符串格式的行:

#include <stdio.h>

char line[1024];  /* Generously large value for most situations */

char *eof;

line[0] = '\0'; /* Ensure empty line if no input delivered */
line[sizeof(line)-1] = ~'\0';  /* Ensure no false-null at end of buffer */

eof = fgets(line, sizeof(line), stdin);

如果控制台输入已经用尽或者由于某些原因操作失败,则会返回eof == NULL,此时行缓冲区可能不会改变(这就是设置第一个字符为'\0'很方便的原因)。
fgets不会溢出line[]并确保在成功返回后,在最后接受的字符后面有一个空值。
如果到达行末尾,则终止'\0'之前的字符将是'\n'。
如果在结束'\0'之前没有终止符'\n',则可能还有更多数据或下一个请求将报告文件结束。您需要再次使用fgets来确定哪个是哪个。(在这方面,使用getchar()循环更容易。)
在上面的示例代码(已更新)中,如果在成功的fgets之后line[sizeof(line)-1] == '\0',则您知道缓冲区已完全填充。如果该位置由'\n'引导,则表示您很幸运。否则,在stdin中还有更多的数据或文件已结束。(当缓冲区没有完全填满时,仍然可能处于文件结束状态,并且当前行的末尾可能没有'\n'。由于必须扫描字符串以查找/消除任何在字符串结尾(缓冲区中的第一个'\0')之前的'\n',我倾向于首先使用getchar()。)
做您需要做的工作以应对读取的第一块数据量仍多于所需的行数的情况。动态增长缓冲区的示例可以使用getchar或fgets来工作。需要注意一些棘手的边缘情况(例如记得在扩展缓冲区之前,让下一个输入从结束上一个输入的'\0'位置开始存储)。

4

如何在C中从控制台读取一行?

  • 构建自己的函数是实现从控制台读取一行的方法之一。

  • 我使用动态内存分配来分配所需的内存量。

  • 当我们即将耗尽分配的内存时,我们尝试将内存大小加倍。

  • 在这里,我使用循环逐个扫描字符串中的每个字符,使用getchar()函数,直到用户输入'\n'EOF字符。

  • 最后,在返回该行之前,我们删除任何额外分配的内存。

//the function to read lines of variable length

char* scan_line(char *line)
{
    int ch;             // as getchar() returns `int`
    long capacity = 0;  // capacity of the buffer
    long length = 0;    // maintains the length of the string
    char *temp = NULL;  // use additional pointer to perform allocations in order to avoid memory leaks

    while ( ((ch = getchar()) != '\n') && (ch != EOF) )
    {
        if((length + 1) >= capacity)
        {
            // resetting capacity
            if (capacity == 0)
                capacity = 2; // some initial fixed length 
            else
                capacity *= 2; // double the size

            // try reallocating the memory
            if( (temp = realloc(line, capacity * sizeof(char))) == NULL ) //allocating memory
            {
                printf("ERROR: unsuccessful allocation");
                // return line; or you can exit
                exit(1);
            }

            line = temp;
        }

        line[length] = (char) ch; //type casting `int` to `char`
        length++;
    }
    line[length + 1] = '\0'; //inserting null character at the end

    // remove additionally allocated memory
    if( (temp = realloc(line, (length + 1) * sizeof(char))) == NULL )
    {
        printf("ERROR: unsuccessful allocation");
        // return line; or you can exit
        exit(1);
    }

    line = temp;
    return line;
}
  • Now you could read a full line this way :

     char *line = NULL;
     line = scan_line(line);
    

这是一个使用 scan_line() 函数的 示例程序
#include <stdio.h>
#include <stdlib.h> //for dynamic allocation functions

char* scan_line(char *line)
{
    ..........
}

int main(void)
{
    char *a = NULL;

    a = scan_line(a); //function call to scan the line

    printf("%s\n",a); //printing the scanned line

    free(a); //don't forget to free the malloc'd pointer
}

示例输入:

Twinkle Twinkle little star.... in the sky!

样例输出:

Twinkle Twinkle little star.... in the sky!

与其他答案不同,此代码可在GCC、Windows 10操作系统下编译为C11,64位二进制文件。 - KANJICODER

2

scanf内可以使用类似于正则表达式的语法来接收整行输入:

scanf("%[^\n]%*c", str); ^\n表示读取直到遇到换行符。然后,使用 %*c 读取换行符,并且 * 表示该换行符被丢弃。

示例代码:

#include <stdio.h>
int main()
{
   char S[101];
   scanf("%[^\n]%*c", S);
   printf("%s", S);
   return 0;
}

1
+1. 多么优雅的答案!小提示:如果需要使用 scanf_s() 而不是 scanf(),则需要提供第三个参数。最安全的命令应该是 scanf_s("%[^\n]%*c", S, (unsigned)_countof(S)); - Ronald Souza

1

我之前遇到过同样的问题,这是我的解决方案,希望能帮到你。

/*
 * Initial size of the read buffer
 */
#define DEFAULT_BUFFER 1024

/*
 * Standard boolean type definition
 */
typedef enum{ false = 0, true = 1 }bool;

/*
 * Flags errors in pointer returning functions
 */
bool has_err = false;

/*
 * Reads the next line of text from file and returns it.
 * The line must be free()d afterwards.
 *
 * This function will segfault on binary data.
 */
char *readLine(FILE *file){
    char *buffer   = NULL;
    char *tmp_buf  = NULL;
    bool line_read = false;
    int  iteration = 0;
    int  offset    = 0;

    if(file == NULL){
        fprintf(stderr, "readLine: NULL file pointer passed!\n");
        has_err = true;

        return NULL;
    }

    while(!line_read){
        if((tmp_buf = malloc(DEFAULT_BUFFER)) == NULL){
            fprintf(stderr, "readLine: Unable to allocate temporary buffer!\n");
            if(buffer != NULL)
                free(buffer);
            has_err = true;

            return NULL;
        }

        if(fgets(tmp_buf, DEFAULT_BUFFER, file) == NULL){
            free(tmp_buf);

            break;
        }

        if(tmp_buf[strlen(tmp_buf) - 1] == '\n') /* we have an end of line */
            line_read = true;

        offset = DEFAULT_BUFFER * (iteration + 1);

        if((buffer = realloc(buffer, offset)) == NULL){
            fprintf(stderr, "readLine: Unable to reallocate buffer!\n");
            free(tmp_buf);
            has_err = true;

            return NULL;
        }

        offset = DEFAULT_BUFFER * iteration - iteration;

        if(memcpy(buffer + offset, tmp_buf, DEFAULT_BUFFER) == NULL){
            fprintf(stderr, "readLine: Cannot copy to buffer\n");
            free(tmp_buf);
            if(buffer != NULL)
                free(buffer);
            has_err = true;

            return NULL;
        }

        free(tmp_buf);
        iteration++;
    }

    return buffer;
}

1
如果您使用goto来处理错误情况,您的代码将变得简单得多。尽管如此,您是否认为在循环中重复使用tmp_buf而不是每次都使用相同大小的malloc会更好呢? - Shahbaz
使用单个全局变量 has_err 报告错误使得此函数线程不安全且不够方便易用。请不要这样做。通过返回 NULL 已经可以指示错误了。此外,可以考虑在通用的库函数中不打印错误消息。 - Jonathan Leffler

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