从 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个回答

0

对@Jerry Coffin和@Tim Čas的回答进行补充:

strchr版本的设计比strcspn(以及strlen版本可能是最快的)要快得多。

strcspn的内部必须迭代通过"\n"字符串,如果合理实现,它只需要执行一次并将字符串长度存储在某个地方。然后在搜索时,它还必须使用一个嵌套的for循环来遍历"\n"字符串。

忽略像库质量实现这些函数所考虑的字长之类的东西,朴素的实现可能看起来像这样:

char* my_strchr (const char *s, int c)
{
  while(*s != '\0')
  {
    if(*s == c)
      return (char*)s;
    s++;
  }
  return NULL;
}

size_t my_strcspn (const char *s1, const char *s2)
{
  size_t s2_length = strlen(s2);
  size_t i;
  for(i=0; s1[i] != '\0'; i++)
  {
    for(size_t j=0; j<s2_length; j++)
    {
      if(s1[i] == s2[j])
      {
        return i;
      }
    }
  }
  return i;
}
  • 对于strchr函数,每个字符有两个分支。一个搜索空终止符,另一个比较当前字符与所搜索的字符。

  • 对于strcspn函数,它必须像我的例子一样预先计算s2的大小,或者在查找null以及搜索键时迭代它。后者本质上就是strchr函数所做的事情,因此内部循环可以用strchr替换。无论我们如何实现,都会有很多额外的分支。

    细心的语言专家还可能注意到strcspn标准库定义中缺少restrict关键字。这意味着编译器不允许假设s1s2是不同的字符串。这也阻止了一些优化。

strlen版本将比两者都要快,因为strlen只需要检查null终止,并且不需要其他操作。虽然正如@chux-Reinstate Monica的答案中提到的,有一些情况它不适用,因此它比其他版本稍微脆弱一些。

问题的根源在于fgets函数的糟糕API - 如果它在早期实现得更好,它将返回与实际读取的字符数相对应的大小,这将是很好的。或者像strchr一样返回指向最后一个读取字符的指针。而标准库浪费了返回值,只返回传递字符串中第一个字符的指针,这有点有用。

想象一下,如果我们有一个像 char* result = fgetsane(s,n,stdin); if(result != NULL) { size_t size = result - s; if(*result == '\n') *result = '\0'; } 这样的函数,那么尾随的 \n 和实际读取的大小都可以轻松解决。如果只是早些时候的 Unix 开发人员多花 5 分钟考虑函数的 API 就好了... - Lundin

-1

对于通过调用fgets获得的字符串,Tim Čas的一行代码非常出色,因为您知道它们在末尾包含一个单独的换行符。

如果您处于不同的上下文环境中,并且想要处理可能包含多个换行符的字符串,则可能正在寻找strrspn。它不是POSIX,这意味着您在所有Unix系统上都找不到它。我为自己的需要编写了一个。

/* Returns the length of the segment leading to the last 
   characters of s in accept. */
size_t strrspn (const char *s, const char *accept)
{
  const char *ch;
  size_t len = strlen(s);

more: 
  if (len > 0) {
    for (ch = accept ; *ch != 0 ; ch++) {
      if (s[len - 1] == *ch) {
        len--;
        goto more;
      }
    }
  }
  return len;
}

对于那些在C语言中寻找Perl chomp等效函数的人,我认为这就是它(chomp只会删除尾部的换行符)。

line[strrspn(string, "\r\n")] = 0;

strrcspn函数:

/* Returns the length of the segment leading to the last 
   character of reject in s. */
size_t strrcspn (const char *s, const char *reject)
{
  const char *ch;
  size_t len = strlen(s);
  size_t origlen = len;

  while (len > 0) {
    for (ch = reject ; *ch != 0 ; ch++) {
      if (s[len - 1] == *ch) {
        return len;
      }
    }
    len--;
  }
  return origlen;
}

2
因为你知道它们在结尾处只包含一个换行符。即使没有 '\n'(或者字符串是 ""),它也能正常工作。 - chux - Reinstate Monica
针对您的第一条评论chux,我的回答保留了那个。当没有\n时,我必须在strrcspn中使用resetlen。 - Philippe A.
为什么要使用 goto end; 而不是 return len; - chqrlie
@chqrlie,我需要走出这个笨拙的两层循环。伤害已经造成了。为什么不使用goto? - Philippe A.
你的代码中有两种goto:一种无用的goto,可以用return语句替换;另一种是向后跳转的goto,被认为是有害的。使用strchr可以更简单地实现strrspnstrrcspn函数:size_t strrspn(const char *s, const char *accept) { size_t len = strlen(s); while (len > 0 && strchr(accept, s[len - 1])) { len--; } return len; }以及 size_t strrcspn(const char *s, const char *reject) { size_t len = strlen(s); while (len > 0 && !strchr(reject, s[len - 1])) { len--; } return len; } - chqrlie
如果strchr不运行到字符串s的最后一个字符,我会同意它。我同意返回值是最好的选择。这将用一条指令替换两条指令。但这并不意味着另一个函数中的goto应该被删除。如果你不喜欢goto,就不要使用它们。请不要再说所有的goto都是邪恶的。 - Philippe A.

-2
下面的函数是我在Github上维护的字符串处理库的一部分。它可以从字符串中删除不需要的字符,正好符合您的要求。
int zstring_search_chr(const char *token,char s){
    if (!token || s=='\0')
        return 0;

    for (;*token; token++)
        if (*token == s)
            return 1;

    return 0;
}

char *zstring_remove_chr(char *str,const char *bad) {
    char *src = str , *dst = str;
    while(*src)
        if(zstring_search_chr(bad,*src))
            src++;
        else
            *dst++ = *src++;  /* assign first, then incement */

    *dst='\0';
        return str;
}

一个例子用法可以是:

Example Usage
      char s[]="this is a trial string to test the function.";
      char const *d=" .";
      printf("%s\n",zstring_remove_chr(s,d));

  Example Output
      thisisatrialstringtotestthefunction

你可能想要检查其他可用的函数,甚至为项目做出贡献 :) https://github.com/fnoyanisi/zString


你应该在 *src++; 中删除 *,并将 badtokend 声明为 const char *。另外,为什么不使用 strchr 代替 zChrSearch?在你的 zStrrmv 函数中,*src 不能是 '\0' - chqrlie
感谢@chqrlie!我已经更新了代码以反映您的建议。zstring最初是一个有趣的项目,旨在创建一个字符串操作库,而不使用任何标准库函数,因此我没有使用strchr - fnisi
3
编写一个“字符串操作库”,而不使用任何标准库函数是一个很好的练习,但为什么要告诉别人去使用它呢?如果说有什么区别,那就是它会比任何标准库都更慢、测试得更少。 - melpomene
这个做的事情与问题所问的不同。它可能可以用来去掉唯一的换行符,但感觉有点过火。 - Jonathan Leffler

-2
 for(int i = 0; i < strlen(Name); i++ )
{
    if(Name[i] == '\n') Name[i] = '\0';
}

你应该试一试。这段代码基本上是循环遍历字符串,直到找到 '\n' 为止。当找到 '\n' 时,它将被空字符终止符 '\0' 替换。

请注意,在此行中,您正在比较字符而不是字符串,因此无需使用 strcmp():

if(Name[i] == '\n') Name[i] = '\0';

由于您将使用单引号而不是双引号。如果您想了解更多关于单引号和双引号的区别,这里有一个链接。


3
低效:for(int i = 0; i < strlen(Name); i++ )会多次调用strlen(Name)(循环改变了Name[]),因此对于长度为N的字符串,这是一个O(N*N)的解决方案。只需要调用一次strlen(Name),如果需要提供一个O(N)的解决方案。不清楚为什么使用int i而不是size_t i。考虑使用for(size_t i = 0; i < Name[i]; i++ ) - chux - Reinstate Monica
@chux 更像是 for (size_t i = 0; Name[i]; i++) { if (Name[i] == '\n') { Name[i] = '\0'; break; } } - melpomene
@melpomene 是的,那样会很直接并且好。但是如果没有 breaki++ 会发生,并且随后的 Name[i] 将为 0,停止循环。您的好主意的优点是,在循环后 i 就是字符串长度。 - chux - Reinstate Monica
@melpomene 我现在明白了。是的,for(size_t i = 0; i < Name[i]; i++ ) 应该改为 for(size_t i = 0; Name[i]; i++ ) - chux - Reinstate Monica

-2

这是我的解决方案。非常简单。

// Delete new line
// char preDelete[256]  include "\n" as newline after fgets

char deletedWords[256];
int iLeng = strlen(preDelete);
int iFinal = 0;
for (int i = 0; i < iLeng; i++) {
    if (preDelete[i] == '\n') {

    }
    else {
        deletedWords[iFinal]  = preDelete[i];
        iFinal++;
    }
    if (i == iLeng -1 ) {
        deletedWords[iFinal] = '\0';
    }
}

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