K&R示例中的函数返回错误的字符串长度?

3

这段代码来自 K&R 的第 65 页。描述说这个函数返回字符串的长度。代码如下:

int trim (char s[])
{
    int n;

    for (n = strlen(s)-1; n >= 0; n--)
        if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
            break;
    s[n+1] = '\0';
    return n;
}

看起来它应该返回n+1。空字符不被视为字符串的一部分吗?

示例:

char s[4];
s[0] = c, s[1] = a, s[2] = t, s[3] = '\0';

这是否意味着字符串大小为3,有3个可用元素?该函数将返回2,这是不正确的。另外,那么字符串长度定义是什么?

2
我认为对于函数 trim() 的目的和返回值存在一些误解。该函数旨在修剪通过引用传递的变量,并在未修剪任何内容时返回负值,在字符串为空时返回0,在修剪字符串时返回正值。如果函数 trim() 旨在返回字符串的长度,那么它不就应该是 return strlen(s) 吗? - alvits
@alvits 这是一个新的视角,但只有在字符串为空或所有内容都被修剪并且新字符串修剪后的strlen-1或未修剪时才会返回负值。我认为这更加令人困惑... - Darkhan
@Daveel - 实际上我搞错了。你是对的,当字符串为空时它会返回负数。如果字符串没有被修剪,它将返回0,因为它永远找不到空格。当字符串被修剪时,它将返回大于0的值,因为找到了一个空格。还是有点困惑吗? - alvits
@alvits 我认为这仍然很困惑,因为:如果只有一个字符或一个字符+空格,则函数将返回0;无论字符串是否有空格,函数都将返回大于0的值;因此,函数结果不会提供任何可靠的信息。 - Darkhan
5个回答

3
问题在于这是一段糟糕的代码,它展示了如何不应该编写代码。通常来说,任何糟糕的代码都包含一个漏洞,而这个例子正是证明了这个规则。:)
这是一段糟糕的代码,因为您甚至不能自信地说这个函数应该返回什么。:) 如果它没有返回strlen(s),那么为什么它要特别返回strlen(s)-1,尤其是对于空字符串。
我会按照以下方式编写此函数。
size_t trim( char s[] )
{
   size_t n = strlen( s );

   while ( n != 0 && ( s[n-1] == ' ' || s[n-1] == '\t' || s[n-1] == '\n' ) ) --n;

   s[n] = '\0';

   return n;
}

将我的代码与你展示的代码进行比较。在我的代码中,如果循环不被迭代,函数将返回strlen(s)非常明显。你甚至不需要了解循环的作用。例如,如果你删除循环:

size_t trim( char s[] )
{
   size_t n = strlen( s );

   s[n] = '\0';

   return n;
}

代码将非常清晰易读,这是一个不变量。

至于循环,则使用了C++中的双向迭代器习语。因此,这样的代码易于阅读,并且没有任何break语句。:)

请注意,重要的是在去除尾随空格后,函数应该返回sizeof(s)。例如,当您想要连接两个字符串时,可以使用它。


好的,当你返回字符串长度时(以及大多数涉及字符串的现代C函数),字符串长度也不包括NULL字符?因此,对于字符串“CAT”,其长度被认为是3而不是4(即不包括空字符)? - Spellbinder2050
@Spellbinder2050 这个函数返回的结果与标准函数strlen相同,因此它们可以互换使用。 - Vlad from Moscow

3

你说的没错,给出的trim实现并没有返回结果字符串的长度。

然而,并不一定是错误的。

我手头的K&R(第二版)上写着:

The following function, trim, removes trailing blanks, tabs, and newlines from the end of the string, using a break to exit from a loop when the rightmost non-blank, non-tab, non-newline is found.

/* trim:  remove trailing blanks, tabs, newlines */
[... code ...]

strlen returns the length of the string....

没有地方说 trim 期望的返回值应该是什么。虽然我同意它的实际返回值不直观,但这并不一定是错误的,因为我们没有被告知它应该如何运作。

另外,您可能希望查看 K&R 的 The C Programming Language 的勘误表(此示例未列出)。


尽管我同意您的观点,即trim不应返回字符串长度,但我不同意返回值是不直观的。返回值是有意义的,负数表示字符串未被修剪,零表示字符串被修剪为空或一开始就为空,最后,正数表示字符串已被修剪。 - alvits
@alvits 这不正确。只有在初始字符串为空时才返回负值。考虑到即使仔细检查也很难理解返回值,我坚持认为它是不直观的。此外,如果你所说的是它应该的行为方式,那么它本可以更清晰、更明确地表达。 - jamesdlin
你说得对,它并不像我想象的那么直观。事实上,我第一次看时就弄错了。但是返回值仍然是有意义的。因此,当字符串为空时,它将返回负数;当字符串未被修剪时,它将返回0,因为它永远找不到空格;最后,当字符串被修剪且找到空格时,它将返回大于0的值。 - alvits

2

您说得完全正确:在您的示例中,返回的“n”等于“strlen(s)-1”,而不是“strlen(s)”。

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

int 
trim (char s[])
{
  int initial_strlen = strlen(s);
  int n;

  for (n = initial_strlen-1; n >= 0; n--) {
    if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
      break;
  }
  s[n+1] = '\0';
  printf ("s=%s, initial strlen=%d, current strlen=%d, n=%d\n",
    s, initial_strlen, strlen(s), n);
  return n+1;
}

int 
main (int argc, char *argv[]) 
{
  char buf[80];

  strcpy(buf, "cat   ");
  printf ("trim #1= %d\n", trim (buf));

  strcpy(buf, "cat\t\t\t   ");
  printf ("trim #2= %d\n", trim (buf));
  return 0;
}

样例输出:

s=cat, initial strlen=6, current strlen=3, n=2
trim #1= 3
s=cat, initial strlen=9, current strlen=3, n=2
trim #2= 3

1
顺便提一下,不要更改字符串字面值。 - BLUEPIXY
@BLUEPIXY - 你说得完全正确。我只是想,在从文字常量复制到缓冲区时添加额外的“strcpy()”可能会很令人困惑。但你是对的:永远不要尝试修改(潜在的只读)字符串常量。 - FoggyDay
@FoggyDay:更糟糕的是,所有常量字面值(包括字符串)都有可能共享空间。想象一个巨大的拼图。 - Deduplicator
1
@FoggyDay,你不需要使用strcpy。一个简单的解决方法是使用char s[] = "cat "; - jamesdlin

1
我猜这本书中有一个错误,函数应该是这样的:
int trim(char s[])
{
    int n;
    for (n = strlen(s); n > 0; n--) {
        if (s[n-1] != ' ' && s[n-1] != '\t' && s[n-1] != '\n') {
            break;
        }
    }
    s[n] = '\0';
    return n;
}

1
一方面,字符串的长度不包括终止零字符。例如,这就是 strlen 的工作原理。例如字符串 "ABCD" 的长度为 4,这是一件自然的事情。
另一方面,上述的 trim 函数确实会返回比实际字符串长度少一个字符的长度。实际上,它应该返回 n + 1
你的示例中字符串的长度的确为3。那是正确的长度。终止零字符不计入长度。如果计算终止零,则长度将为4

楼主说得对。关键是,如果“trim()”声称返回“字符串长度”,那么它需要返回“n + 1”,而不是“n”。正如所示的例子返回字符串长度。 - FoggyDay
我没有点踩,但是我强烈不同意"In fact, it is not guaranteed to work with empty strings even in its original form, since it attempts to access s[1]."。如果空字符串作为参数提供,则在for循环中将n设置为-1(因为在这种情况下strlen(s)为零),然后评估n >= 00(因此不进入循环体),并且赋值s[n+1] = '\0';设置s[-1+1]的值,即s[0],因此不会尝试数组溢出。 - Grzegorz Szpetkowski
@Grzegorz Szpetkowski:是的,你说得对。我不知怎么就假设一个空字符串在循环后会使n变为零。我从答案中删除了那段话。 - AnT stands with Russia
1
在 ISO C 中,strlen 返回 size_t,因此如果 strlen(s) == 0,则将 (size_t)-1 分配给 int,导致实现定义的行为(可能会引发信号)。这应该被视为一个错误。然而,在 K&R1 编写时,我不确定是否存在 size_tstrlen 可能已被声明为返回 int,因此代码将正常工作。 - M.M

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