第1章 一个好的开始()
1.1 void main()并不是准确的
没有谁的人生从一开始就是错误的,
然而有的程序却是从不正确开始的。
还记得几年前,当我在黑板上写下“int main(void)”时,讲台下面突然传来了小声的议论。当然这是正常的,任何学生都应该有权利在课堂发表对课程内容的意见。最前面的一个学生甚至小声的冲我说“老师,是不是写错了,书上不是这样写的?”。
是的,书上的确不是这样写的,但是我知道至少我没有错。
好吧,就让一切都从这个不是错误的“错误”开始:
问题描述:
中国的大部分理工科学生最早接触的C语言应该都是大学课堂上,然后一遍遍的这样开始自己的程序“void main(void)”,或者在上机,或者在考试。然而大部分C程序员采用的却是
“int main( void )” 或者
“int main( int argc, char *argv[] )” /*这里的argc和argv可以改成其他名字*/
这样的两种写法来开始一个程序。
如果一个人采用“void main(void)”这种写法,往往会被认为是没有开发经验的新手,关于“void main()”正确与否的争论已经开始了很久,其实这种用法就如同周星驰电影中“你走先”一样,虽然并不符合普通话标准,但是由于广为流传,已经被大多数人所接受。“void main(void)”其实就是这样,C 标准中都从来没有定义过这种用法,但是这种用法却为一些编译器接受(编译器的开发商并不是原则性很强),因此“void main(void)”看起来没有什么异常。到底这几种看起来很简单的写法区别在哪呢?或者干脆没有区别呢?
实例分析
例1-1
void main() { ……
} | int main(void) { …… return 0; } |
直接从例1-1的两个程序的对比来看,int main(void)这种写法的特点是指定了主函数的返回类型,而且在最后以0作为函数正常执行的返回值。普通函数的返回值可能在程序中会被调用,但是主函数的返回值的用途是什么呢?程序是在操作系统上运行的,当程序执行结束时,应该给予操作系统一个信息。这个信息用于说明程序的退出状态。如果值为 0,则代表程序正常退出,其他值代表程序异常退出。
在C语言的层面上来看,两者的区别在于void main()没有返回值,而int main(void)有一个为0的返回值。
深入剖析:
在我们身边总是不缺乏这样的人,他们总是在轻松或沉重的写下一行行的代码。作为程序员毫无疑问是幸运的一类人,因为我们都生活在了一个不乏高级编程语言的时代。每天我们需要的做的就是设计程序的结构,然后用极为接近自然语言的 C++或者 java 去实现它,计算机是怎么实现的这些,有人关心吗?交给万能的编译器去做吧。在一些人的心里,编辑器永远不会犯错误,甚至要比人更可靠。
事实上,当你写下一行行代码时,你真的确信编译器是按照你的思维在工作吗?还记得电影《我,机器人》里的一个情节吗,为了确保人工智能不会对人类造成危害,人类科学家制定了人工智能必须遵守的“三大安全法则”。然而人工智能在没有违背这些法则的同时却给出了与人类预想完全不同的解释方式,从而违背了设计者的初衷。好的,无论从哪一点考虑,你都应该更了解编辑器到底在干什么,而这了解这一切最好的方式,就是将你的程序进行反汇编,这样计算机所做的一切都会清清楚楚的展现在你的面前。
当然,很多看到这本书的人可能并没有学过汇编,也不打算去学习汇编,这并不会影响你对本书的阅读,引入汇编只是为了了解编译器的工作方式。事实上,如果你只想解除一些C语言方面的困惑的话,完全可以直只看代码后面的注释。虽然这种感觉很像在喝一杯没有了气体的可乐。
好的,看一下例1-1的两个程序对应的汇编代码
void main(){ 00411350 push ebp 00411351 mov ebp,esp 00411353 sub esp,0C0h 00411359 push ebx 0041135A push esi 0041135B push edi 0041135C lea edi,[ebp-0C0h] 00411362 mov ecx,30h 00411367 mov eax,0CCCCCCCCh 0041136C rep stos dword ptr es:[edi] }
| int main(void){ 00411350 push ebp 00411351 mov ebp,esp 00411353 sub esp,0C0h 00411359 push ebx 0041135A push esi 0041135B push edi 0041135C lea edi,[ebp-0C0h] 00411362 mov ecx,30h 00411367 mov eax,0CCCCCCCCh 0041136C rep stos dword ptr es:[edi] return 0; 0041136E xor eax,eax } |
这两段代码仔细比较一下,可以发现,两段代码唯一的区别仅仅在于int main(void)这种写法仅仅在最后多了一句:
0041136E xor eax,eax
其中eax是一个寄存器,在程序结束的时候,这个寄存器通常保存的是函数的返回值。执行xor eax,eax语句的作用,就是将这个寄存器的值设置为0。这样的话,只要程序正常执行了的话,eax中的值永远是固定的0。但是如果没有这句的话,eax的值就不是一个C程序员可知的,因为在程序的执行中,eax会经常使用到的,比如上例中就将eax的值变成了0CCCCCCCCh,而在其他语句中eax的值更会经常改变,这样一来eax中的值就可能是任何值。
好的,在汇编语言的层面来看,void main()并不是没有返回值,而是返回了一个我们在编写C程序时根本无从得知的一个值。
总结
如果你是饭店的老板,后厨有两个很能干的厨师,其中一个厨师每完成一天任务后,都会将厨房收拾的干干净净,将所有厨具放回原来的位置。而另一个厨师每次完成任务后,东西都会放的到处都是,根本不去整理。
这样的两个厨师,如果一定要淘汰一个,你会淘汰哪一个呢?
之前提到过很多编译器其实可以接受void main()这种写法,甚至有些编译器其实在void main()后面自动加上了xor eax,eax,这样的话,即使使用了void main(),eax的值也是固定的0了。但是切记,不是所有的编译器都会这样做!
很多书籍都采用了“int main( int argc, char *argv[] )”这种写法就很容易理解了。本身操作系统是和程序有一个互动的过程。程序可以给操作系统一个返回值,操作系统也可以给程序几个参数。
扩展:
“所有这一切,无非是想说初学者如果一直写void main()可能曲解了C语言的本来目的,但是现在我们都了解了这些,我可不可以写void main()呢,这样的话可以少写一句 return 0啊,程序不也可以变得简洁吗”
是的,这样写程序可能会显得更简洁,但是正如同你跟每个人说“你走先”,他都能听得懂吗?至少我现在还没有弄明白“你走先”到底是“你先走”还是“你走前面”的意思,虽然这里面的区别不大。
既然C标准里从来没有定义过这种写法,甚至在最新的C99中规定了只能有“int main( void )”和“int main( int argc, char *argv[] )”这两种写法,谁敢肯定所有的编译器都会支持void main()呢?
这里就又涉及到了代码的移植性问题,设想一下,如果你是一个刚刚进入工作岗位的程序员,你的经理在要求你写一段小程序,当你辛辛苦苦的写完代码,战战兢兢地检查无误,然后自信的将代码交给那个魔鬼转身离开时,却听到经理办公室里传来的经理发出的不屑声。你清楚的知道这是一种对编程菜鸟的轻视,但是你却不知这声音为何而来?
发生什么了?
也许他只是用了一个不支持void main()这种写法的编译器而已,或者他已经忘了他在上学时教材上也是这样写的。
好的,以上的这一切想要说的就是,“int main( void )”和“int main( int argc, char *argv[] )”这样写是一个好的习惯,尤其是对初学者来说这样更容易去理解程序的运行方式。而且这是标准的写法,这样写你可以通过所有的标准C语言编译器,而且很多编程老鸟也会认同你。当然提高了程序的可移植性。
最后,引用C++之父Bjarne Stroustrup的一句话来结束这节,“The definition void main( ) { /* ... */ } is not and never has been C++, nor has it even been C.”