这段C代码是如何工作的?

11

我看了一下在C语言中使用递归打印字符串反转的以下代码:

void ReversePrint(char *str) { //line 1
  if(*str) {                   //line 2
      ReversePrint(str+1);     //line 3
      putchar(*str);           //line 4
  }
}

我相对较新于 C 语言,对第二行代码感到困惑。根据我的理解,*str 是解引用指针,并应返回当前位置字符串的值。但是它如何作为条件语句的参数使用(应该期望布尔值吧?)?在第三行中,指针将始终递增到下一个块(4 个字节,因为它是 int 类型)...那么,如果字符串结尾后面的内存块中有数据,这段代码会失败吗?

更新:所以在 C 中没有布尔类型,对吗?条件语句的求值结果为 "false" 如果值为 0,否则为 "true" ?


8
小伙子,我希望那不是生产代码! :) - Drew Hall
2
想知道一个字符串需要多长才能爆炸。 - Skurmedel
可能会很长...但仍然。 - Skurmedel
3
最初的方法名为StrReverse4,因此可能是为长度约为4个字符的字符串而设计的。 - Edd
10个回答

38

第二行代码检查当前字符是否为字符串的空结束符号 - 因为C语言的字符串是以空结束符号结尾的,而空字符被认为是false值,所以当它碰到字符串结尾时,它将开始展开递归(而不是尝试调用空结束符后面的字符的StrReverse4函数,这将超出有效数据范围)。

同时注意指针指向一个char类型,因此递增指针只会增加1个字节(因为char是单字节类型)。

例如:

 0  1  2  3
+--+--+--+--+
|f |o |o |\0|
+--+--+--+--+
  1. str = 0时,*str'f',因此对于str+1=1进行递归调用。
  2. str = 1时,*str'o',因此对于str+1=2进行递归调用。
  3. str = 2时,*str'o',因此对于str+1=3进行递归调用。
  4. str = 3时,*str'\0',而\0是一个false值,因此if(*str)的求值结果为false,因此不进行递归调用,因此回到了递归的上一层...
  5. 最近的递归跟随着`putchar('o')`,然后是...
  6. 下一个最近的递归跟随着`putchar('o')`,然后是...
  7. 最久远的递归跟随着`putchar('f')`,我们结束了。

5
在C语言中,没有布尔类型,正确的做法是将逻辑语句中任何计算结果为0的值视作“false”,而将任何计算结果为非零的值视作“true”。由于“空字符null character”的值实际上就是0,因此它被视作“false”。 - Amber
6
小小的批评 - ASCII字符0通常被称为“nul”(只有一个l),而无效指针通常被称为“NULL”(有两个l)。我认为在这里要做出区分很重要,因为指针很容易让人迷失。 - Chris Lutz
4
“nul”是一个简写形式,但是当提到“null terminator”或“null character”时,通常使用正确的拼写(带有两个l)。例如:http://en.wikipedia.org/wiki/Null_character - Amber
3
我在维基百科上没有看到任何表明这只是你个人观点的内容,Heath。 - Amber
1
@Amber:C99就像C语言一样,就像C++98(或者甚至是C++03)就像C++一样。 - dreamlax
显示剩余6条评论

3
C字符串的类型实际上就是指向字符的指针。惯例是指针所指向的是一个由字符数组组成的字符串,以零字节结尾
因此,*str 就是 str 所指向字符串的第一个字符。
在条件语句中使用 *str,如果 str 指向(空)字符串中的终止空字节,则求值为 false

2

字符串的结尾通常是一个0字节,行 if (*str) 检查该字节的存在并在到达该字节时停止。


1
在字符串的末尾有一个0 - 所以你会得到"test" => [0]'t' [1]'e' [2]'s' [3]'t' [4]0if(0) -> false 这样就可以工作了。

1
在第三行,指针将始终增加到下一个块(4个字节,因为它是int)...
那是错误的,这是char *,它只会增加1。因为char只有1个字节长。
但是这如何作为条件语句的参数使用(应该接受布尔值对吧?)
您可以在if($$)中使用任何值,并且它只会检查它是否为非零,基本上bool也实现为简单的1=true和0=false。
在其他更高级别的强类型语言中,您不能在if中使用这些东西,但在C中,一切都归结为数字。您可以使用任何东西。
if(1) // evaluates to true 
if("string")  // evaluates to true
if(0) // evaulates to false

在C语言中,您可以在if和while条件语句中放置任何内容。


0

条件语句(ifforwhile等)需要布尔表达式。如果提供整数值,则评估将简化为0 == falsenon-0 == true。如前所述,c字符串的终止字符是空字节(整数值0)。因此,if在字符串末尾(或字符串中的第一个空字节)时将失败。

顺便说一下,如果您对空指针执行*str,则会引发未定义的行为;在取消引用之前,应始终验证指针是否有效。


0

1.

str 是指向 char 的指针。将 str 递增会使指针指向字符串的第二个字符(因为它是 char 数组)。 注意:递增指针将按照指针所指向的数据类型进行递增。

例如:

int *p_int;
p_int++;     /* Increments by 4 */

double *p_dbl;
p_dbl++;      /* Increments by 8 */

2.

if(expression)
{
   statements;
}

表达式被计算,如果结果值为零(NULL\00),则不执行语句。由于每个字符串都以\0结尾,递归将必须在某个时候结束。


并非每个字符串都保证以空字节结尾。实际上,大多数情况下是这样的;但没有什么要求一定如此。 - ezpz
同意。在这种情况下,我只是假设了这一点。 - Shrinidhi
1
@ezpz:按照定义,C字符串是以空字符结尾的字符序列;如果没有终止的空字符,它就不是C字符串 ;) - Christoph

0

C语言没有布尔值的概念:在C语言中,每个标量类型(即算术和指针类型)都可以在布尔上下文中使用,其中0表示false,非零表示true

由于字符串是以空字符结尾的,因此终止符将被解释为false,而每个其他字符(具有非零值!)将被解释为true。这意味着有一种简单的方法来迭代字符串的字符:

for(;*str; ++str) { /* so something with *str */ }

StrReverse4()通过递归而不是迭代执行相同的操作。


0

尝试使用这段代码,它和你正在使用的那个一样简单:

int rev(int lower,int upper,char*string)
{
  if(lower>upper)
          return 0;
   else
          return rev(lower-1,upper-1,string);
}

0

这有点离题,但当我看到这个问题时,我立刻想知道这是否比仅执行strlen并从后面迭代更快。

所以,我做了一个小测试。

#include <string.h>

void reverse1(const char* str)
{
    int total = 0;
    if (*str) {
            reverse1(str+1);
            total += *str;
    }
}

void reverse2(const char* str)
{
    int total = 0;
    size_t t = strlen(str);
    while (t > 0) {
            total += str[--t];
    }
}

int main()
{
    const char* str = "here I put a very long string ...";

    int i=99999;

    while (--i > 0) reverseX(str);
}

我首先使用函数reverse1将其编译为X=1,然后再编译为X=2。两次都使用了-O0。

递归版本始终返回大约6秒,而strlen版本返回1.8秒。

我认为这是因为strlen是用汇编实现的,而递归增加了相当大的开销。

如果我错了,请纠正我,但我相信这个基准测试是有代表性的。

无论如何,我觉得应该与你分享这个信息。


可能是因为在你的系统中,递归函数调用的开销与迭代解决方案的时间相比较大,这使得strlen只需要进行一次函数调用。我的主要观点是从一个测试中得出概括性结论可能是危险的,但也很有趣。最好先编写清晰、易于维护的代码,然后在实际性能测试揭示瓶颈时再进行优化。 - Berry
我同意。虽然strlen线性扫描整个字符串(我猜),但比较应该在一个函数调用(递归)和strlen实现的一次迭代加上1/n的strlen函数调用之间进行。但是,我再次同意,最好先编写清晰易懂的代码,如果需要再进行优化。还有一个问题,您的系统中递归解决方案比迭代解决方案更快吗? - Gaston

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