C/C++ 中非法的 void main() 与 main() 函数返回值的作用

这篇 post 是主要是写给 Association of Robots and Artificial Intelligence 的小伙伴的~主要说说为什么 void main() 是非法的,以及 main() 函数的返回值到底有什么用~

先从 void main() 讲起吧~这里就先引用 C++ 之父,Bjarne Stroustrup,在他的博客中写过的一篇问答:Can I write "void main()"?

Can I write "void main()"?

The definition
                                      void main() { /* ... */ }
is not and never has been C++, nor has it even been C. See the ISO C++ standard 3.6.1[2] or the ISO C standard 5.1.2.2.1. A conforming implementation accepts
                                      int main() { /* ... */ }
and
                                      int main(int argc, char* argv[]) { /* ... */ }
A conforming implementation may provide more versions of main(), but they must all have return type int. The int returned by main() is a way for a program to return a value to "the system" that invokes it. On systems that doesn't provide such a facility the return value is ignored, but that doesn't make "void main()" legal C++ or legal C. Even if your compiler accepts "void main()" avoid it, or risk being considered ignorant by C and C++ programmers.

翻译:这种写法 void main() { /* ... */ },从来都没有在 C/C++ 中存在过,根据 ISO C++ 标准 3.6.1 或者 ISO C 标准 5.1.2.2.1,只有 int main() {/* ... */} 或者是 int main(int argc, char* argv[]) {/* ... */} 才是可接受的。

A conforming implementation may provide more versions of main(), but they must all have return type int. The int returned by main() is a way for a program to return a value to “the system” that invokes it. On systems that doesn’t provide such a facility the return value is ignored, but that doesn’t make “void main()” legal C++ or legal C. Even if your compiler accepts “void main()” avoid it, or risk being considered ignorant by C and C++ programmers. —— Bjarne Stroustrup

翻译:符合规范的写法可能会有一些别的版本,但是他们都必须返回 int。这个main()返回的 int是用来会返回给调用这个程序的“系统”的。在没有提供这样的功能的系统中,这个返回值会被忽略,但是那也绝不使得void main在 C/C++ 中是合法的。即便你的编译器让你编译过了,或者就是仔细考虑过这么写的后果/风险

比如,我们来编译一下如下的 C 代码

void main() {
    
}

那么编译时就会报一个警告

编译器告诉我们 main() 函数应该有的返回类型是 int 而不是 void

此外,void main() 也不在 C89 或者 ANSI C 规范中受支持,要么会报错,要么会产生警告。事实上,没有任何一个 C/C++ 标准支持这种形式的 main() 函数。以下是依次以 C89 标准和 ANSI C 标准编译时会有的输出。

可以看到 C89 和 ANSI C 规范更是严格,直接编译报错,提示 main() 函数必须有 int 类型的返回值。

那让我们更进一步,知其然知其所以然~

假设有如下代码~

int main() {
    return 1;
}

现在我们来看看它被编译器编译成汇编语言时是什么样子

gcc -S main.c

编译输出是 main.s,其内容为

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 14	sdk_version 10, 14
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movl	$0, -4(%rbp)
	movl	$1, %eax
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

.subsections_via_symbols

上面代码高亮的那一行,movl $1, %eax,就是对应了 main() 函数返回值的那行代码。

如果我们定义成 void main() 的话,那么在 eax 寄存器中就可能是一个随机的值~

可是不管是随机的值也好,还是我们可以自己可以控制的值也好,它是返回到哪里去了呢?

现在就先从 Shell 开始说起~可能有的小伙伴刚才已经注意到了,在前面以 C89 或者 ANSI C 规范编译报错之后,左侧的箭头就变成红色的了!

也就是说 Shell 通过某种方式知道了我们编译失败了!

那么这种神奇的方式则是靠编译器在它自己的 main() 函数中,返回了相应的非 0int 值来做到的~

可是这个值到底是多少呢?在 macOS / Linux 下,执行完某个程序之后,如果希望获取到它的 main() 的返回值的话,则可以以如下方式查看~(中途不能在那一个 Shell 中执行别的程序,因为只会保存上一个程序的 main() 返回值)

echo $?

这个命令其实也特别形象,就像在问,出错了吗??? 一样2333333

可以看到,两次编译在报错之后,gcc 的返回值都是 1~那么学过 C/C++ 的都知道,在 C/C++ 里面我们规定的是,只要不是 0,那么就是 true。现在 gcc 的返回值为 1,因此对于问题「出错了吗???」的回答就是 true

Shell 正是因为拿到了这个返回值,所以才知道上一个程序没有正确执行完或者报错,并且让小箭头变成红色来提示我们(⁎⁍̴̛ᴗ⁍̴̛⁎)

那这个值可以不是 1 吗?当然可以的!

我们随便改一下我们刚才只有 3 行的小程序~

int main() {
    return 233;
}

然后编译运行它看看~

你看~这下是不是变成 233 了,可是这个值除了拿来让小箭头变红之外,还有什么用呢?当然还有不少用了~

  1. 让程序的作者及用户知道错误代码
  2. Shell 中的应用
  3. 以编程的方式调用别的应用时

1. 让程序的作者及用户知道错误代码

第一点就是让程序的作者及用户知道错误代码~要不然程序的开发者在 debug 的时候怎么知道是哪里错了呢,毕竟可以造成问题的原因还是有很多的QAQ

这样的话,作为编写程序的人,可以通过 main() 函数的返回值,来大致定位是什么原因导致的~

2. Shell 中的应用

第二点则是因为很多时候,我们不会把所有功能都做到一个程序里,这么做也不现实,通常都是分成好多个程序;此外,我们有时也需要调用别人写的程序。那么在我们把它们在 Shell Script 里或者就是在 Terminal (终端)串起来的时候,我们就需要知道上一步执行的程序到底有没有成功

假设说,我们写好了一个下载视频的程序,download,还有一个将视频转换成 mp4 格式的程序,convert_mp4,最后有一个把转换好的 mp4 同步到移动硬盘里的程序,sync_video

显然,这三个程序都可能因为不同的原因出错:download 可能因为没有网络而无法下载视频,convert_mp4 可能拿到的是一个无效的视频文件,sync_video 可能因为你中途把移动硬盘物理拔出了而出错。

在 Shell 脚本中的话,我们大概会这么写

download && convert_mp4 && sync_video

上面这条命令的含义就是,先执行 download,如果「出错了QwQ」(返回值不为 0)的话,那么就在这里中止;否则就接着执行 convert_mp4

如果convert_mp4 也是「出错了QAQ」(返回值不为 0)的话,那同样的~就在这里中止了~否则就会执行 sync_video

关于 sync_video 也是一样的~要是「出错了Σ(・□・;)」(返回值不为 0)的话,那同样在这里中止;

不过因为 sync_video 后面没有别的程序了,所以要是执行到了 sync_video 的话,它的返回值就是这一整行命令的返回值~

要是没有 执行到 sync_video 的话,则必然是前面某一个程序出错了,那么这一行命令的返回值,就会是出错的那个程序的返回值~

一个简单的例子是这样的~假如我们把程序改成这样,也就是根据用户输入来决定 main() 的返回值

#include <stdio.h>

int main() {
    int return_value = 0;
    scanf("%d", &return_value);
    return return_value;
}

编译好之后,我们串起来 3 个,也就是 ./main && ./main && ./main,组成一条 Shell 语句~根据我们写的代码,以及前面的理解,我们最多会输入 3 个数,那我们先拟依次输入 027

gcc main.c -o main
./main && ./main && ./main
echo $?

可以看到在输入完 2 之后,这条语句就停下来了,并且整条 Shell 语句的返回值也是 2

那我们再试试看拟依次输入 00233~那么预想就是,前两个因为输入的是 0,那么 main() 返回的也是 0——意味着没有出错,因此就看第三个——第三个输入是一个不为 0 的数,233,那么第三个的返回值是 233,Shell 会知道这里出错了,整条语句的返回值也就是 233 了~

./main && ./main && ./main
echo $?

可以看到过程、输出跟我们的预想一样~

3. 以编程的方式调用别的应用

除了上面 2 种使用到的地方之外,还有就是我们可以通过编程的方式去调用一个程序,自然,我们有时也需要那个程序的 main() 的返回值,才知道它是否正确执行完了,如果报错的话( ;´Д`),就可以根据它 main() 的返回值知道是什么错误等等(。・ω・。)

现在把我们的程序简单修改一下~

输入的数在 [1, 10] 这个区间内,则调用自身,等新的进程结束之后,返回输入的数作为 main() 的返回值;不在这个区间内的话,就直接将那个值作为 main() 函数的返回值 ♪(´ε` )

这里先贴上运行的截图可能比较有趣2333333

可以看到我们首先是通过 ./main 启动了这个程序,接下来输入了 1,让它调用自己

随后第二个进程启动,我们输入了 2,又让它调用自己;最后第三个进程启动,这时我们的输入是 233,因此第三个进程的 main() 返回值是 233;这个返回值被第二个进程拿到,输出了

[OK] The return value of another me from `posix_spawn` is 233

之后,第二个进程结束。由于第二个进程在问我们的时候,我们输入的是 2,现在第三个进程已经结束,第二个进程也来到了 return 语句上,因此它的 main() 返回值是 2,而这个值又被第一个进程捕获,输出

[OK] The return value of another me from `posix_spawn` is 2

同样的,等第二个进程结束,第一个进程输出完之后,它也来到了 return 语句前~在它在问我们的时候,我们输入的是 1,因此 1 就是第一个进程的 main() 返回值了!

于是这个返回值就被 Shell 给拿到了~也就是 echo $? 的结果

下面就是代码啦www

#include <spawn.h> // posix_spawn()
#include <stdio.h> // scanf(), printf()
#include <string.h> // strerror()
#include <sys/wait.h> // waitpid()

int main(int argc, char * argv[]) {
    int invoke_self = 0;
    printf("[INFO] 输入值在 [1, 10] 区间内, 则通过 `posix_spawn` 调用自身, 否则将输入的数作为 `main()` 返回值:\n");
    scanf("%d", &invoke_self);

    // 判断是否需要调用自己
    if (invoke_self >= 1 && invoke_self <= 10) {
        // 如需要
        // 则调用 `posix_spawn`
        pid_t pid;
        int status = posix_spawn(&pid, argv[0], NULL, NULL, argv, NULL);
        // 判断 `posix_spawn` 的返回值
        // 0: 成功启动新的进程
        if (status == 0) {
            // 输出新的进程的信息
            printf("[OK] Another me is running now, pid is %d\n", pid);
            // 等待那个新的进程结束
            if (waitpid(pid, &status, 0) != -1) {
                // 结束之后获取它的 `main()` 的返回值
                if (WIFEXITED(status)) {
                    int return_value = WEXITSTATUS(status);
                    // 输出那个新的进程的 `main()` 的返回值
                    printf("[OK] The return value of another me from `posix_spawn` is %d\n", return_value);
                }
            } else {
                // 等待新的进程结束时有错误产生
                perror("[ERROR] waitpid");
            }
        } else {
            // 创建新的进程时有错误产生
            printf("[ERROR] posix_spawn: %s\n", strerror(status));
        }
    }

    // 若不需要则直接返回用户输入的值
    return invoke_self;
}

声明: 本文为0xBBC原创, 转载注明出处喵~

Leave a Reply

Your email address will not be published. Required fields are marked *