递归和静态变量

15

所以当我在印度准备参加入学考试时,我遇到了这段C代码

#include <stdio.h>
int main(void) {
    static int i = 4;
    if (--i) {
        main();
        printf("%d", i);
    }
}

我原本认为printf语句从不执行,输出结果为空白。 但是我看到答案是0000,这是由于使用了static关键字和int类型导致的。

有人可以解释一下为什么printf会执行吗?或者那个人错了吗?


16
在C++中,调用main会导致未定义的行为。如果想要的话,这段代码可能会让你的机器着火。 - cHao
7
我删除了C++标签,因为你的代码既可以是C++ 也可以是 C。由于你的代码使用了printf,并且你说它是C,所以C++标签是错误的。 - Marcus Müller
10
可以在C语言中实现。 - Eugene Sh.
3
在C语言中,如果(--i)在测试之前减少了i,如果i不为零,则进入块并调用main是合法的。第二次(以及每次后续)进入main时,静态变量i不会被重新初始化。所以第二次i的值确实为3。一旦i一直减到零,if测试返回false,块被跳过,最深度递归的main返回。那时每个printf语句都会执行。 - bruceg
3
与“重复调用main()函数的意外输出”非常相似。打印结果不同,并且还有一个额外的本地变量也被打印出来,但是…… - Jonathan Leffler
显示剩余7条评论
5个回答

31

如果问题涉及递归,它将打印出"000"(我不知道为什么答案显示4个零,因为"--i"减量发生在赋值之前)。如果展开调用,您会得到:

if (--i) { //i == 3
    if (--i) { //i == 2
        if (--i) { //i == 1
            if (--i) { //i == 0
                // rest does not get evaluated since the condition is false
            }
            printf("%d", i); // i is 0 now
        }
        printf("%d", i); //i is still 0
    }
    printf("%d", i); //i is still 0
}

然而,正如大多数人所提到的,这段代码很糟糕,我建议您永远不要考虑在编写任何软件时采用这种程度的马虎。


3
从技术上来说,这就是他问题的答案,但我猜它有些岔开了。 - pasha
2
也许“答案”显示四个0是因为他们不理解自己的“入学考试”... :v - code_dredd

22

在C语言中,递归调用main是合法的(尽管你为什么要这样做呢?)

此外,C11(5.1.2.2.1)规定:

它应该定义为类型为int的返回类型... [或者]如果返回类型与int不兼容,则向主机环境返回的终止状态未指定。

(C++规定返回类型必须是int

因此,该代码部分实际上符合标准,但实际返回值是由具体实现决定的(感谢@chux)

有人能解释一下为什么printf会执行吗?还是那个人说错了?

如果您正在使用C++编译器编译C,则该人是错误的。否则,代码将会被编译。

现在,printf确实会执行,因为static变量只初始化一次,根据6.2.4/3:

其标识符声明的对象[...]带有存储类说明符static ,具有静态存储期,其生命周期为整个程序执行期间,并且其存储的值仅在程序启动之前初始化一次

*在C++中绝对不合法。根据[basic.start.main]:“主函数不应在程序内使用”。这个答案里有很多C++的话题,因为最初问题的标签是C++,其中的信息是有用的。


14
有人能解释一下为什么printf会执行吗?还是那个人错了?
因为main返回。 这个答案写出了逻辑,但我看到你仍然在评论中提出这个问题。所以我会用另一种方式说同样的话,希望对你更清楚。
因为该变量是static,在第一次调用之前,变量被声明并设置为4.由于它是static,每次都会使用相同的变量。如果再次调用它(或者很可能在第一次调用时),它只是跳过了声明和初始化。
在第一次调用中,变量减少到3。3是真值,所以我们执行if块。这将递归调用main
在第二次调用中,变量减少到2。2在C语言中仍然是真值。我们再次递归调用main
在第三次调用中,变量减少到1。1仍然是真值。我们再次递归到main
在第四次调用中,变量减少到0。0在C语言中是假值。我们不进入if块。我们不递归。我们不打印。在这次调用main中没有剩余的任务可做。它在结尾处返回。

现在我们回到了第三次调用。 我们刚从对main的调用返回。 我们仍然在if块内部。 下一条语句是一个printf。 变量的值现在为0。 因此,我们打印出0。 没有其他事情可做,我们在结尾处返回。

现在我们回到了第二次调用。 仍然是0,所以我们再次打印0并返回。

现在我们回到了第一次调用。 仍然是0,所以我们再次打印0并返回。

我们打印了3次0,所以是000。

正如您所注意到的,在下降的过程中,我们不会打印。 我们在回程时打印。 那时,变量为0。 然后我们进入if的then块3次。 因此,我们递归,然后打印3次。

当您递归调用函数时,不会消除先前的调用。 它被放在堆栈上。 完成后,您回到离开的地方,并继续执行下一条语句。

您可能想考虑以下三个代码片段将如何产生不同的反应:

void recurse() {
    static int i = 4;
    if (--i) {
        recurse();
        printf("%d", i);
    }
}

第一次调用时打印000。第二次调用可能不会打印任何内容并崩溃。如果您的堆栈比int大小大(这在理论上似乎不太可能,但并非不可能),则会打印许多0。例如,对于8位int,它将打印255个0(每个可能的非零值都有一个)。

void recurse(int i) {
    if (--i) {
        recurse(i);
        printf("%d", i);
    }
}

如果以recurse(4)的形式调用,将会打印出123。作为参数,每次函数调用时我们都在处理不同的i

void recurse(int i) {
    if (--i) {
        printf("%d", i);
        recurse(i);
    }
}

如果以recurse(4)调用,则打印321。在下降时打印而不是在返回时打印。

void recurse(int i) {
    printf("%d", i);
    if (--i) {
        recurse(i);
    }
}

如果以recurse(4)的形式调用,将打印4321。 它在向下的过程中打印,而不是在向上的过程中。

void recurse() {
    static int i = 4;
    printf("%d", i);
    if (--i) {
        recurse();
    }
}

将会打印4321。我们是在向下执行时进行打印,而不是向上执行。请注意,参数和static变量以这种方式给出相同的结果。但是,如果再次调用此函数,它将不会有任何作用。参数将再次打印相同的内容。

void recurse() {
    int i = 4;
    if (--i) {
        recurse();
        printf("%d", i);
    }
}

循环永远执行。没有使用"static",每次都会创建一个新的"i"。这将一直运行,直到超载堆栈并崩溃。没有输出,因为它从未达到"printf"。
最简单的方法是去ideone.com之类的地方运行代码来检查。询问为什么会打印某些内容是合理的,但询问它打印了什么则应该自己回答。

谢谢你,这解决了一切。 - Sriram R
只是一个提示:在第一个代码块中,在第二次运行时,确实会发生一些事情。而那个东西可能会使堆栈溢出。(如果 i 是 0,--i 就是 -1。) - cHao

7
这段代码存在多个问题:
  • 没有包含<stdio.h>头文件,调用printf("%d", i);会产生未定义的行为。

  • 不带参数的main函数原型应该是int main(void)。将main定义为void main()在某些平台上是不可移植且不正确的。始终使用标准原型来定义main函数,并返回有效的退出状态以保持良好的风格。

  • 在C语言中,调用main函数递归是不好的编程风格,并且在C++中具有未定义的行为。

忽略未定义行为意味着任何事情都可能发生,包括印度公司使用可怕的编程测试。程序打印0000的原因可能是您错误地输入了if (i--)测试条件,或者从内存中错放了大括号,或者那个人完全错误。main函数递归调用3次,每次递减相同的i变量,直到它变成0,并且每个实例在返回到其调用者之前都打印i的值,即0,包括最初的main()调用。

static int i = 4;在函数内部定义一个初始值为4的全局变量i,只能在定义它的块中访问。所有递归实例都访问相同的变量。


我看不出省略头文件会导致调用 printf 变成未定义行为。在我看来,这将需要编译错误(如果没有找到其他的 printf 声明)。调用未声明的函数并不是未定义行为。 - François Andrieux
我只是没有在这里添加 printf,因为我认为它是暗示的。让我编辑它。 - Sriram R
2
调用未声明的函数会为其进行隐式定义。printf(“%d”,i);将使用隐式声明作为int printf(char *,int);,这与实际定义不兼容。因此导致未定义行为。 - chqrlie
@FrançoisAndrieux:如果声明和代码在调用约定、参数数量、返回类型等方面不一致,那么你就会遇到麻烦。 - cHao
将main定义为void main()是不正确的,根据5.1.2.2.1 1,“或以其他实现定义的方式”,ID行为也是允许的。然而,使用int main()同样可以,而且更短,毫无疑问是可行的。 - chux - Reinstate Monica
显示剩余2条评论

6

抽象化这个函数的质量和名称。

所有的static变量在程序启动期间只会被初始化一次,并且保持它们的值。

当启动例程第一次调用主函数时,变量i的值为4。然后您将它的值减少到3并再次调用main。这时i的值是3,然后您将其减少到2......


1
是的,但由于我们在 printf 之前调用了 main(),所以 printf 永远不会被执行。如果 printf 在 if 语句之外,这可能是情况。但是,printf 是在 if 语句内部并且在 main() 调用之后执行的。 - Sriram R

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