从 fgets() 输入中移除尾部换行符

350

我正在尝试从用户那里获取一些数据并在gcc中将其发送到另一个函数。代码大致如下。

printf("Enter your Name: ");
if (!(fgets(Name, sizeof Name, stdin) != NULL)) {
    fprintf(stderr, "Error reading Name.\n");
    exit(1);
}

然而,我发现它在结尾处有一个换行符 \n。 所以如果我输入 John,它最终会发送 John\n。 如何删除那个 \n 并发送一个正确的字符串。


31
如果(至少)没有从标准输入中读取到Name,则执行以下操作:if (!fgets(Name, sizeof Name, stdin))。(请注意不要使用两个否定词,即“!”和“!=”) - Roger Pate
7
“@Roger Pate ‘不要使用两个否定词’ --> 嗯,如果我们深入挖掘,“don't”和“negation”都是否定词。也许可以使用‘if (fgets(Name, sizeof Name, stdin)) {’。” - chux - Reinstate Monica
6
@chux,我相信你的意思是 if (fgets(Name, sizeof Name, stdin) == NULL ) { - R Sahu
@RSahu True: 讨厌的 ! - chux - Reinstate Monica
15个回答

626

也许最简单的解决方案之一是使用我最喜欢且鲜为人知的函数之一:strcspn():

buffer[strcspn(buffer, "\n")] = 0;

如果您想让它处理'\r'(比如,如果流是二进制的):

buffer[strcspn(buffer, "\r\n")] = 0; // works for LF, CR, CRLF, LFCR, ...
该函数计算字符数量,直到遇到 '\r''\n'(换句话说,它找到第一个 '\r''\n')。如果没有碰到任何字符,它会在 '\0' 处停止(返回字符串的长度)。
请注意,即使没有换行符,这个函数也能正常工作,因为 strcspn 会在 '\0' 处停止。在那种情况下,整行代码就是将 '\0' 替换为 '\0'

48
这个甚至还能处理罕见的以 '\0' 开头的缓冲区,而这种情况会让 buffer[strlen(buffer) - 1] = '\0'; 方法产生问题。 - chux - Reinstate Monica
7
@chux: 是的,我希望更多人知道strcspn()。在库中,它是我认为最有用的函数之一。今天我决定编写并发布一些常见的C语言技巧,比如使用strcspnstrspn实现的strtok_r函数就是其中之一。这是第一个例子:http://codepad.org/2lBkZk0w(*警告:*我不能保证它没有错误;它是匆忙编写的,可能有些错误)。我还不确定将在哪里发布它们,但打算以著名的“位操作技巧”为灵感来发布。 - Tim Čas
4
研究了一些方法来强化 fgets() 的截取功能。strcspn() 似乎是唯一正确的一行代码。虽然 strlen 更快,但不如 strcspn() 简单。 - chux - Reinstate Monica
7
问题的标题和内容都在询问fgets()输入中的尾随换行符,这个换行符也总是第一个换行符。 - Tim Čas
10
我理解你的想法,但我不能对特定词汇在谷歌搜索结果中的表现负责。请找谷歌,而不是找我。 - Tim Čas
显示剩余5条评论

231

优雅的方式:

Name[strcspn(Name, "\n")] = 0;

稍微有些不太美观的方法:

char *pos;
if ((pos=strchr(Name, '\n')) != NULL)
    *pos = '\0';
else
    /* input too long for buffer, flag error */

稍微有些奇怪的方法:

strtok(Name, "\n");

注意,如果用户输入一个空字符串(即只按回车键),则 strtok 函数不会像预期的那样工作。它会保留 \n 字符。

当然还有其他情况。


7
大多数目标多线程平台的 C 运行时库都支持线程,因此 strtok() 函数是线程安全的(它将使用线程本地存储来保存“调用间”状态)。尽管如此,通常最好使用非标准但足够常见的 strtok_r() 变体。 - Michael Burr
2
请查看我的答案,其中包含完全线程安全和可重入的变体,类似于您的 strtok 方法(并且它可以处理空输入)。实际上,实现 strtok 的好方法是使用 strcspnstrspn - Tim Čas
3
如果你处于可能出现过长行的环境中,处理else情况非常重要。静默截断输入可能会导致非常严重的错误。 - Malcolm McLean
4
如果你喜欢使用一行代码,并且正在使用glibc库,可以尝试*strchrnul(Name, '\n') = '\0';这行代码。该代码的作用是将字符串中第一个换行符\n替换成空字符\0来终止字符串。 - twobit
1
strchr(Name, '\n') == NULL 时,除了 "input too long for buffer, flag error" 的错误提示之外,还有其他可能性存在:stdin 中的最后一行文本没有以 '\n' 结尾,或者读取到了一个罕见的嵌入式空字符。 - chux - Reinstate Monica

87
size_t ln = strlen(name) - 1;
if (*name && name[ln] == '\n') 
    name[ln] = '\0';

9
如果字符串为空,它很可能会抛出异常,对吗?就像索引超出范围一样。 - Edward Olamisan
3
@EdwardOlamisan,这个字符串永远不会为空。 - James Morris
6
在不寻常的情况下,fgets(buf, size, ....)可能会返回strlen(buf) == 0。原因有三:1)当读取到的第一个字符是'\0'时,2)当size == 1时,以及3)当fgets()返回NULL并且buf内容可以是任何值(尽管OP的代码确实测试了是否为NULL)。建议使用以下代码来修正这个问题:size_t ln = strlen(name); if (ln > 0 && name[ln-1] == '\n') name[--ln] = '\0'; - chux - Reinstate Monica
2
如果字符串为空怎么办?ln将会是-1,但由于size_t是无符号的,因此会写入随机内存。我认为你想使用ssize_t并检查ln是否大于0。 - abligh
3
@legends2k:搜索编译时值(尤其是像strlen中的零值)可以比纯字符搜索更有效地实现。因此,我认为这种解决方案比基于strchrstrcspn的解决方案更好。 - AnT stands with Russia
显示剩余5条评论

27
下面是一种快速的方法,用于从由fgets()保存的字符串中删除潜在的'\n'。它使用strlen()进行两次测试。
char buffer[100];
if (fgets(buffer, sizeof buffer, stdin) != NULL) {

  size_t len = strlen(buffer);
  if (len > 0 && buffer[len-1] == '\n') {
    buffer[--len] = '\0';
  }
  // `len` now represents the length of the string, shortened or not.

现在根据需要使用bufferlen
这种方法的附带好处是后续代码中有一个len值。它可以比strchr(Name, '\n')更容易更快地实现。Ref 个人情况可能有所不同,但两种方法都有效。

buffer,从原始的fgets()在某些情况下不会包含"\n"
A)行对于buffer来说太长,所以只有'\n'之前的char保存在buffer中。未读的字符保留在流中。
B)文件中的最后一行没有以'\n'结尾。

如果输入中嵌入了空字符'\0'strlen()报告的长度将不包括'\n'的位置。


一些其他答案的问题:
1.

strtok(buffer, "\n");buffer"\n"时无法移除'\n'。根据这个答案 - 在这个答案之后进行了修改以警告这个限制。

2.

fgets()读取的第一个char'\0'时,以下代码在极少数情况下会失败。这种情况发生在输入以嵌入的'\0'开头时。然后buffer[len - 1]变成了buffer[SIZE_MAX],访问了肯定超出buffer合法范围的内存。这是答案在撰写本答案时的状态。后来一个非OP编辑它,包括了本答案中检查""的代码。

 size_t len = strlen(buffer);
 if (buffer[len - 1] == '\n') {  // 当len == 0时失败
   buffer[len -1] = '\0';
 }

3.

sprintf(buffer,"%s",buffer); 是未定义行为:参考。此外,它不会保存任何前导、分隔或尾随的空格。现在已经删除

4.

[由于后来的答案的好处进行编辑] 除了性能与strlen()方法相比之外,1行代码buffer[strcspn(buffer, "\n")] = 0;没有任何问题。在修剪方面,性能通常不是问题,因为代码正在进行I/O - 一个消耗CPU时间的黑洞。如果以下代码需要字符串的长度或对性能非常敏感,请使用strlen()方法。否则,strcspn()是一个很好的替代方法。


感谢您提供的有用答案。当使用malloc动态分配缓冲区大小时,我们可以使用strlen(buffer)吗? - rrz0
@Rrz0 buffer = malloc(allocation_size); length = strlen(buffer); 是不好的 - 指向 buffer 的内存中的数据是未知的。buffer = malloc(allocation_size_4_or_more); strcpy(buffer, "abc"); length = strlen(buffer); 是可以的。 - chux - Reinstate Monica

12

如果每行都有'\n',则直接从fgets输出中删除'\n'

line[strlen(line) - 1] = '\0';
否则:
void remove_newline_ch(char *line)
{
    int new_line = strlen(line) -1;
    if (line[new_line] == '\n')
        line[new_line] = '\0';
}

1
注意:使用strnlen而不是strlen更为安全。 - Mike Mertsock
3
问题中链接的第一个回答有一条评论指出:“请注意,strlen()、strcmp()和strdup()是安全的。'n'选项为您提供了额外的功能。” - Étienne
4
不,它不会。在这种情况下插入一个“n”并不能神奇地增加安全性,事实上它会使代码变得更加危险。类似的,strncpy也是一个极其不安全的函数。你所链接的帖子给出了错误的建议。 - M.M
5
对于空字符串(""),这种方法彻底失败了。而且strlen()返回的是size_t类型而不是int类型。 - alk
6
这对于空字符串是不安全的,它将在索引-1处进行写入。请勿使用此方法。 - Jean-François Fabre
显示剩余2条评论

2

对于单个'\n'的修剪,

void remove_new_line(char* string)
{
    size_t length = strlen(string);
    if((length > 0) && (string[length-1] == '\n'))
    {
        string[length-1] ='\0';
    }
}

如果需要去除多个 '\n',

void remove_multi_new_line(char* string)
{
  size_t length = strlen(string);
  while((length>0) && (string[length-1] == '\n'))
  {
      --length;
      string[length] ='\0';
  }
}

1
为什么要嵌套使用 if,当你可以使用 && 来写一个条件呢?那个 while 循环的结构很奇怪,它可以简单地写成 while (length > 0 && string[length-1] == '\n') { --length; string[length] = '\0'; } - melpomene
@melpomene 谢谢你的建议。我更新了代码。 - BEPP
1
我建议第一个函数更自然的定义方式是:size_t length = strlen(string); if (length > 0 && string[length-1] == '\n') { string[length-1] = '\0'; }。这也更好地反映了第二个定义(只是使用 if 而不是 while)。 - melpomene
@elpomene 谢谢。很有道理。我已经更新了代码。 - BEPP

1
如果使用getline是一个选项 - 不要忽视其安全问题,如果你想使用指针 - 你可以避免使用字符串函数,因为getline返回字符数。像下面这样:
#include <stdio.h>
#include <stdlib.h>
int main()
{
    char *fname, *lname;
    size_t size = 32, nchar; // Max size of strings and number of characters read
    fname = malloc(size * sizeof *fname);
    lname = malloc(size * sizeof *lname);
    if (NULL == fname || NULL == lname)
    {
        printf("Error in memory allocation.");
        exit(1);
    }
    printf("Enter first name ");
    nchar = getline(&fname, &size, stdin);
    if (nchar == -1) // getline return -1 on failure to read a line.
    {
        printf("Line couldn't be read..");
        // This if block could be repeated for next getline too
        exit(1);
    }
    printf("Number of characters read :%zu\n", nchar);
    fname[nchar - 1] = '\0';
    printf("Enter last name ");
    nchar = getline(&lname, &size, stdin);
    printf("Number of characters read :%zu\n", nchar);
    lname[nchar - 1] = '\0';
    printf("Name entered %s %s\n", fname, lname);
    return 0;
}

注意[getline 的安全问题] 也不应该被忽视。


1

我的新手方法;-)。如果这是正确的,请告诉我。它似乎适用于我所有的情况:

#define IPT_SIZE 5

int findNULL(char* arr)
{
    for (int i = 0; i < strlen(arr); i++)
    {
        if (*(arr+i) == '\n')
        {
            return i;
        }
    }
    return 0;
}

int main()
{
    char *input = malloc(IPT_SIZE + 1 * sizeof(char)), buff;
    int counter = 0;

    //prompt user for the input:
    printf("input string no longer than %i characters: ", IPT_SIZE);
    do
    {
        fgets(input, 1000, stdin);
        *(input + findNULL(input)) = '\0';
        if (strlen(input) > IPT_SIZE)
        {
            printf("error! the given string is too large. try again...\n");
            counter++;
        }
        //if the counter exceeds 3, exit the program (custom function):
        errorMsgExit(counter, 3); 
    }
    while (strlen(input) > IPT_SIZE);

//rest of the program follows

free(input)
return 0;
}

1
一般而言,不要删除你不需要的数据,最好在一开始就避免写入它。如果你不想在缓冲区中有换行符,不要使用fgets。相反,使用getc、fgetc或scanf等。也许可以这样做:
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
        char Name[256];
        char fmt[32];
        if( snprintf(fmt, sizeof fmt, "%%%zd[^\n]", sizeof Name - 1) >= (int)sizeof fmt ){
                fprintf(stderr, "Unable to write format\n");
                return EXIT_FAILURE;
        }
        if( scanf(fmt, Name) == 1 ) {
                printf("Name = %s\n", Name);
        }
        return 0;
}

注意,这种方法会让换行符未被读取,因此您可能需要使用类似于"%255[^\n]%*c"的格式字符串来丢弃它(例如,sprintf(fmt, "%%%zd[^\n]%%*c", sizeof Name - 1);),或者在scanf后面加上getchar()

你是否意识到上面的代码片段容易受到缓冲区溢出攻击?sprintf函数并不会检查缓冲区的大小! - Sapphire_Brick
1
@Sapphire_Brick,这真的不是问题。格式字符串的长度将为7加上名称长度的十进制表示中的数字数量。如果此长度大于24,则会出现其他问题。如果您想要更安全并使用“snprintf”,那肯定可以,但这将适用于比1PB大得多的缓冲区。 - William Pursell
为了溢出缓冲区,您需要创建一个大约8 yotta字节的自动数组,因为只有当“Name”超过2^83个字节时才会溢出缓冲区。在实际应用中,这不是问题。但是,是的,“snprintf”应始终优先于“sprintf”。代码已编辑。 - William Pursell
在这种情况下,使用scanf有什么优势,而不是简单地使用getchar()循环? - supercat
@supercat 绝对不行。在我看来,scanf永远不应该被用于任何事情,但它似乎是一个受欢迎的选择,并经常被滥用。 - William Pursell

1

去掉换行符的步骤可能是最显然的方法:

  1. 使用 strlen(),头文件为 string.h,确定 NAME 中字符串的长度。请注意,strlen() 不计算终止符 \0
size_t sl = strlen(NAME);

  1. 查看字符串是否以一个或多个 \0 字符(空字符串)开头。在这种情况下,由于 strlen() 不计算 \0 并停止在第一个出现的位置,因此 sl 将为 0
if(sl == 0)
{
   // Skip the newline replacement process.
}

检查正确字符串的最后一个字符是否为换行符'\n'。如果是这种情况,则将\n替换为\0。请注意,索引计数从0开始,因此我们需要执行NAME[sl - 1]:
if(NAME[sl - 1] == '\n')
{
   NAME[sl - 1] = '\0';
}

请注意,如果您在fgets()字符串请求中仅按下Enter键(字符串内容仅包含换行符),则此后NAME中的字符串将为空字符串。


  1. 我们可以使用逻辑运算符&&在一个if语句中将步骤2和3合并为一步:
if(sl > 0 && NAME[sl - 1] == '\n')
{
   NAME[sl - 1] = '\0';
}

完成的代码:
size_t sl = strlen(NAME);
if(sl > 0 && NAME[sl - 1] == '\n')
{
   NAME[sl - 1] = '\0';
}

如果您更喜欢一种函数来处理fgets的输出字符串,而不是每次都重新输入,请使用fgets_newline_kill

void fgets_newline_kill(char a[])
{
    size_t sl = strlen(a);

    if(sl > 0 && a[sl - 1] == '\n')
    {
       a[sl - 1] = '\0';
    }
}

在您提供的示例中,应该是这样的:
printf("Enter your Name: ");

if (fgets(Name, sizeof Name, stdin) == NULL) {
    fprintf(stderr, "Error reading Name.\n");
    exit(1);
}
else {
    fgets_newline_kill(NAME);
}

请注意,如果输入字符串中嵌入了\0,则此方法无法工作。如果是这种情况,strlen() 仅返回第一个\0之前的字符数。但这不是很常见的方法,因为大多数字符串读取函数通常会在第一个\0处停止并将字符串取到该空字符。
除了问题本身之外,请尽量避免使用双重否定,以使您的代码更加清晰: if (!(fgets(Name, sizeof Name, stdin) != NULL) {}。您可以简单地使用 if (fgets(Name, sizeof Name, stdin) == NULL) {}

不确定为什么你想这样做。去除换行符的目的不是为了给字符串添加空字符,而是为了去掉换行符。将字符串末尾的 "\n" 替换为 "\0" 是一种“去除”换行符的方法。但是在字符串内部替换 "\n" 字符会从根本上改变该字符串。有意多次使用换行符的字符串并不罕见,这实际上会截断这些字符串的末尾。要"删除"这些换行符,需要将数组内容向左移以覆盖 "\n"。 - ad absurdum
@exnihilo 如何使用 fgets() 输入包含多个换行符的字符串? - RobertS supports Monica Cellio
你可以将多次调用 fgets() 获取的字符串连接起来。但我不明白你的反对意见:你是提出处理多个换行符的代码的人。 - ad absurdum
@exnihilo 你说得对,我会重新考虑这个策略。我只是想提供一种非常严厉但可能实现所需结果的方式。 - RobertS supports Monica Cellio
@exnihilo 完全修改了我的答案,并按照主要方法使用了 strlen 等。不是重复的原因:1. 通过步骤解释代码。2. 提供基于函数和上下文的解决方案。3. 提示避免双重否定表达式。 - RobertS supports Monica Cellio

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