高级语言C之函数的基础及编程示例提及“函数”这个词儿,很多人都像我一样,感觉很恐慌,因为它总让我想起代数里讲的方方面面。这些对于像我这样的笨鸟来说,真的太深奥,总是不敢去考虑它,去琢磨它。虽然这里讲的跟那个并非同一个东西,但不免总是心有余悸。今天要讲的东西比较多,我尽量把它讲的详细明白,但由于本人笨鸟一个,能力实在有限,大家多多包涵。先列一下今天要讲的目录:1.什么是函数。2.函数的定义和使用方法。3.从调试中看函数的调用机制。4.撩开函数的面纱。5.结尾语。好,以上是今天要讲的目录,下面进入正题:一、什么是函数。函数,就是完成某个或者某种固定功能的最小的模块(总感觉这样写不是很严谨)。当然,如果我就这样解释,相比大家很定会说我应付,说我不负责任,所以,这里我多牢骚几句。在C语言中,默认指定的函数入口点是main函数,所以,我们在很多时候,只在这个函数中写代码,但是当我们的程序大到一定的程度,这个函数未免显的台过臃肿了;而且从方便实用的角度来说,把所有的功能都写在main函数中,看起来很不直观;而且很多的功能我们可能在别的程序里还能用到,如果我们都在一个函数里,那移植起来肯定也很麻烦;从维护方面来讲,这样很不容易维护,牵一处则动全身。比如下面的代码:intmain(){//初始化变量;initcode001;initcode002;initcode003;//开始实现功能一的代码gn001:{code001;code002;code003;//显示结果printf(功能一的执行结果...\n请选择下一个功能:);scanf(%d,&bl001);switch(bl001){casegn001:gotogn001;break;casegn002:gotogn002;break;casegn003:gotogn003;break;casegn004:gotogn004;break;}}//开始实现功能二的代码gn002:{……//显示结果printf(功能二的执行结果...\n请选择下一个功能:);scanf(%d,&bl001);switch(bl001){casegn001:gotogn001;break;casegn002:gotogn002;break;casegn003:gotogn003;break;casegn004:gotogn004;break;}}}从上面的代码可以看出,很多的重复代码,而且,如果我要在别的程序里使用功能二的代码,需要认真的将代码提取出来,难免发生错误。而且如果这个程序有70多个功能那这个程序就麻烦了。因此,在写程序的时候,需要根据功能来讲整个程序划分成一个个模块,哪个模块有问题,我们就只要对有问题的模块修改,整理就可以了。在另外的程序中,需要用到哪个模块就将相应的模块移植到指定的程序里,就可以了,而函数,就是模块中最小的单位。以后,根据我们系列的深入,我们会继续讲到DLL,LIB等。彻底的将我们的项目工程模块化。如下面的代码:#includestdio.h//这里声明一下函数MaxNum,让编译器知道有一个名字叫MaxNum的函数,它有三个整型参数。intMaxNum(intnum001,intnum002,intnum003);//////////////////////////////////////////////////////////////////////////voidmain(){intnum1=0,num2=0,num3=0;intresult=0;scanf(%d,%d,%d,&num1,&num2,&num3);//让用户输入任意三个数result=MaxNum(num1,num2,num3);//调用MaxNum函数printf(%d\n,result);//显示MaxNum函数的返回值}//下面是函数定义部分/************************************************************************///函数名:MaxNum//参数://num001:随便一个整型数据//num002:随便一个整型数据//num003:随便一个整型数据//功能://取出三个参数中最大的一个数并返回。/************************************************************************/intMaxNum(intnum001,intnum002,intnum003){if(num001=num002){if(num001=num003){returnnum001;}else{returnnum003;}}else{if(num002=num003){returnnum002;}else{returnnum003;}}}这样下来,我们的程序就比较规范了,也方便任务的分工,写这个函数的人只管这个函数功能的实现,调用这个函数的人只要知道这个函数的功能和怎么使用就可以了,不用管这个函数功能是怎么实现的,OK既然知道函数是什么及为什么要用函数了,那下面我们进入下一节二、函数的定义和使用方法。通过上一小节的节的代码,我相信很多的朋友已经知道函数是怎么声明并使用的了,这里我再具体的说一下:定义一个函数的格式是:返回值类型函数调用方式函数名(参数1,参数2……){函数指令;return返回值;}具体的使用例子,大家就看上一小节中的函数例子就可以,我偷个懒,嘿嘿……相信,很多的朋友会问我一些问题:1.上面的代码中,那个MaxNum函数好像是定义了两次哎~,先是声明,再是定义,声明跟定义有什么区别呀。2.在上面代码中函数的定义好像没有本节函数定义格式中的调用方式……好,第一个问题呢,我们可以先把声明的那一条语句删除掉,然后编译一下程序,看看,提示什么呢?Compiling...Func.cppE:\项目工程\测试例子\Func.cpp(11):errorC2065:'MaxNum':undeclaredidentifierE:\项目工程\测试例子\Func.cpp(26):errorC2373:'MaxNum':redefinition;differenttypemodifiersErrorexecutingcl.exe.Func.exe-2error(s),0warning(s)好,那我们再把这个MaxNum函数的定义部分移到main函数的前面,再编译,哈哈没有问题了。这说明了什么呢?我们程序再执行的时候,先进入main函数,如果我们自定义的函数再main函数前面,那编译器就会知道,MaxNum是我们自己定义的函数,如果我们定义的函数MaxNum在main函数的后面,编译器再编译我们再main函数调用的代码时由于它不知道我们定义了MaxNum,所以调用MaxNum的代码就不能被识别了。因此,我们应该在调用我们定义的函数前,先声明一下,让编译器知道我们定义了这么个函数,就可以了,当然,如果程序很想,我们完全可以把我们定义的函数放在程序文件的前面,main函数放在最后,免去声明的麻烦,但是定义函数前,先声明函数是个好习惯,因为以后我们写的程序可能会几个程序文件一起编译……关于第二个问题,我们看下一节吧……三、从调试中看函数的调用机制。我们直接使用上面的程序做例子,Release编译时,设置生成MAP文件,编译好程序以后,OD打开它,载入MAP文件,当然,如果不会捣鼓的,可以参考MAP文件中的信息:AddressPublicsbyValueRva+BaseLib:Object0001:00000000_main00401000fFunc.obj0001:00000050?MaxNum@@YAHHHH@Z00401050fFunc.obj0001:00000070_printf00401070fLIBC:printf.obj0001:000000a1_scanf004010a1fLIBC:scanf.obj来到我们的main函数中:00401000/$83EC0Csubesp,0C;申请一块堆栈,给局部变量预留空间00401003|.33C0xoreax,eax00401005|.8D4C2404leaecx,dwordptr[esp+4]00401009|.89442408movdwordptr[esp+8],eax0040100D|.89442404movdwordptr[esp+4],eax00401011|.89442400movdwordptr[esp],eax00401015|.8D442400leaeax,dwordptr[esp]00401019|.50pusheax;Arg4=80040101A|.8D54240Cleaedx,dwordptr[esp+C]0040101E|.51pushecx;Arg3=50040101F|.52pushedx;Arg2=300401020|.6834804000push00408034;Arg1=ASCII%d,%d,%d00401025|.E877000000call_scanf0040102A|.8B442410moveax,dwordptr[esp+10]0040102E|.8B4C2414movecx,dwordptr[esp+14]00401032|.8B542418movedx,dwordptr[esp+18]00401036|.50pusheax;Arg3=800401037|.51pushecx;Arg2=500401038|.52pushedx;Arg1=300401039|.E812000000call?MaxNum@@YAHHHH@Z{00401050/$8B4C2404movecx,dwordptr[esp+4]00401054|.8B442408moveax,dwordptr[esp+8]00401058|.3BC8cmpecx,eax0040105A|.7C09jlshortFu.004010650040105C|.8B44240Cmoveax,dwordptr[esp+C]00401060|.3BC8cmpecx,eax00401062|.7D09jgeshortFu.0040106D00401064|.C3retn00401065|8B4C240Cmovecx,dwordptr[esp+C]00401069|.3BC1cmpeax,ecx0040106B|.7D02jgeshortFu.0040106F0040106D|8BC1moveax,ecx0040106F\C3retn}0040103E|.50pusheax0040103F|.6830804000push00408030;ASCII%d,LF00401044|.E827000000call_printf00401049|.83C430addesp,30;回复堆栈平衡0040104C\.C3retn这里面应该没有我们不认识的汇编指令吧。我们再这里就看一下这些代码,当然如果可以的话,你可以单步跟踪这个程序,尤其注意看下堆栈的变化。1.__cdecl调用方式好的,我们现在来看一下这段代码,先看一下堆栈吧,在函数头,申请了一段大小为0xC的堆栈空间,在函数结尾平衡堆栈的时候,恢复了0x30的大小,也就是说,中间的这些PUSH的函数参数,占用了0x24的堆栈空间,(我们可以算一下,第一个函数scanf有4个参数PUSH了4次,第二个函数MaxNum有3个参数,PUSH了3次,第三个函数是printf,有两个参数,push了两次,一共PUSH了9次,DWORD(4)*9=0x24,再加上一开始在函数头申请的0xC大小的堆栈空间,一共是0x30)需要我们的代码再调用完函数后,进行恢复,否则堆栈就不平衡程序就出错误了,由此可以见,我们可以整理一下这类函数的调用方式:push参数n……Push参数2Push参数1call函数首地址addes