这篇 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()
函数中,返回了相应的非 0
的 int
值来做到的~
可是这个值到底是多少呢?在 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
了,可是这个值除了拿来让小箭头变红之外,还有什么用呢?当然还有不少用了~
- 让程序的作者及用户知道错误代码
- Shell 中的应用
- 以编程的方式调用别的应用时
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 个数,那我们先拟依次输入 0
,2
,7
gcc main.c -o main ./main && ./main && ./main echo $?
可以看到在输入完 2
之后,这条语句就停下来了,并且整条 Shell 语句的返回值也是 2
那我们再试试看拟依次输入 0
,0
,233
~那么预想就是,前两个因为输入的是 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; }