




已閱讀5頁,還剩6頁未讀, 繼續免費閱讀
版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
笨鳥先飛學編程系列之三 函數提及“函數”這個詞兒,很多人都像我一樣,感覺很恐慌,因為它總讓我想起代數里講的方方面面。這些對于像我這樣的笨鳥來說,真的太深奧,總是不敢去考慮它,去琢磨它。雖然這里講的跟那個并非同一個東西,但不免總是心有余悸。今天要講的東西比較多,我盡量把它講的詳細明白,但由于本人笨鳥一個,能力實在有限,大家多多包涵。先列一下今天要講的目錄:1. 什么是函數。2. 函數的定義和使用方法。3. 從調試中看函數的調用機制。4. 撩開函數的面紗。5. 結尾語。好,以上是今天要講的目錄,下面進入正題:一、 什么是函數。函數,就是完成某個或者某種固定功能的最小的模塊(總感覺這樣寫不是很嚴謹)。當然,如果我就這樣解釋,相比大家很定會說我應付,說我不負責任,所以,這里我多牢騷幾句。在C語言中,默認指定的函數入口點是main函數,所以,我們在很多時候,只在這個函數中寫代碼,但是當我們的程序大到一定的程度,這個函數未免顯的臺過臃腫了;而且從方便實用的角度來說,把所有的功能都寫在main函數中,看起來很不直觀;而且很多的功能我們可能在別的程序里還能用到,如果我們都在一個函數里,那移植起來肯定也很麻煩;從維護方面來講,這樣很不容易維護,牽一處則動全身。比如下面的代碼:int main()/初始化變量;initcode001;initcode002;initcode003;/開始實現功能一的代碼gn001:code001;code002;code003;/顯示結果printf(功能一的執行結果.n請選擇下一個功能:);scanf(%d,&bl001);switch (bl001)case gn001: goto gn001; break;case gn002:goto gn002; break;case gn003:goto gn003; break;case gn004:goto gn004; break;/ 開始實現功能二的代碼gn002:/顯示結果printf(功能二的執行結果.n請選擇下一個功能:);scanf(%d,&bl001);switch (bl001)case gn001: goto gn001; break;case gn002:goto gn002; break;case gn003:goto gn003; break;case gn004:goto gn004; break;從上面的代碼可以看出,很多的重復代碼,而且,如果我要在別的程序里使用功能二的代碼,需要認真的將代碼提取出來,難免發生錯誤。而且如果這個程序有70多個功能那這個程序就麻煩了。因此,在寫程序的時候,需要根據功能來講整個程序劃分成一個個模塊,哪個模塊有問題,我們就只要對有問題的模塊修改,整理就可以了。在另外的程序中,需要用到哪個模塊就將相應的模塊移植到指定的程序里,就可以了,而函數,就是模塊中最小的單位。以后,根據我們系列的深入,我們會繼續講到DLL,LIB等。徹底的將我們的項目工程模塊化。如下面的代碼:#include stdio.h/ 這里聲明一下函數MaxNum,讓編譯器知道有一個名字叫MaxNum的函數,它有三個整型參數。int MaxNum(int num001, int num002, int num003);/void main()int num1 = 0, num2 = 0, num3 = 0;int result = 0;scanf(%d,%d,%d, &num1, &num2, &num3);/ 讓用戶輸入任意三個數result = MaxNum(num1, num2, num3);/調用MaxNum 函數printf(%dn, result);/顯示MaxNum函數的返回值/下面是函數定義部分/*/ 函數名: MaxNum/ 參 數:/ num001:隨便一個整型數據/ num002:隨便一個整型數據/ num003:隨便一個整型數據/ 功 能:/取出三個參數中最大的一個數并返回。/*/int MaxNum(int num001, int num002, int num003)if (num001 = num002)if (num001 = num003)return num001; elsereturn num003;elseif (num002 = num003)return num002; elsereturn num003;這樣下來,我們的程序就比較規范了,也方便任務的分工,寫這個函數的人只管這個函數功能的實現,調用這個函數的人只要知道這個函數的功能和怎么使用就可以了,不用管這個函數功能是怎么實現的,OK既然知道函數是什么及為什么要用函數了,那下面我們進入下一節二、 函數的定義和使用方法。通過上一小節的節的代碼,我相信很多的朋友已經知道函數是怎么聲明并使用的了,這里我再具體的說一下:定義一個函數的格式是:返回值類型 函數調用方式 函數名(參數1, 參數2)函數指令;return 返回值;具體的使用例子,大家就看上一小節中的函數例子就可以,我偷個懶,嘿嘿相信,很多的朋友會問我一些問題:1. 上面的代碼中,那個MaxNum函數好像是定義了兩次哎,先是聲明,再是定義,聲明跟定義有什么區別呀。2. 在上面代碼中函數的定義好像沒有本節函數定義格式中的調用方式好,第一個問題呢,我們可以先把聲明的那一條語句刪除掉,然后編譯一下程序,看看,提示什么呢?Compiling.Func.cppE:項目工程測試例子Func.cpp(11) : error C2065: MaxNum : undeclared identifierE:項目工程測試例子Func.cpp(26) : error C2373: MaxNum : redefinition; different type modifiersError executing cl.exe.Func.exe - 2 error(s), 0 warning(s)好,那我們再把這個MaxNum函數的定義部分移到main函數的前面,再編譯,哈哈沒有問題了。這說明了什么呢?我們程序再執行的時候,先進入main函數,如果我們自定義的函數再main函數前面,那編譯器就會知道,MaxNum是我們自己定義的函數,如果我們定義的函數MaxNum在main函數的后面,編譯器再編譯我們再main函數調用的代碼時由于它不知道我們定義了MaxNum,所以調用MaxNum的代碼就不能被識別了。因此,我們應該在調用我們定義的函數前,先聲明一下,讓編譯器知道我們定義了這么個函數,就可以了,當然,如果程序很想,我們完全可以把我們定義的函數放在程序文件的前面,main函數放在最后,免去聲明的麻煩,但是定義函數前,先聲明函數是個好習慣,因為以后我們寫的程序可能會幾個程序文件一起編譯關于第二個問題,我們看下一節吧三、 從調試中看函數的調用機制。我們直接使用上面的程序做例子,Release編譯時,設置生成MAP文件,編譯好程序以后,OD打開它,載入MAP文件,當然,如果不會搗鼓的,可以參考MAP文件中的信息:AddressPublics by ValueRva+BaseLib:Object0001:00000000_main00401000 fFunc.obj0001:00000050?MaxNumYAHHHHZ00401050 fFunc.obj0001:00000070_printf00401070 fLIBC:printf.obj0001:000000a1_scanf004010a1 fLIBC:scanf.obj來到我們的main函數中:00401000 /$ 83EC 0C sub esp, 0C; 申請一塊堆棧,給局部變量預留空間00401003 |. 33C0 xor eax, eax00401005 |. 8D4C24 04 lea ecx, dword ptr esp+400401009 |. 894424 08 mov dword ptr esp+8, eax0040100D |. 894424 04 mov dword ptr esp+4, eax00401011 |. 894424 00 mov dword ptr esp, eax00401015 |. 8D4424 00 lea eax, dword ptr esp00401019 |. 50 push eax ; Arg4 = 80040101A |. 8D5424 0C lea edx, dword ptr esp+C0040101E |. 51 push ecx ; Arg3 = 50040101F |. 52 push edx ; Arg2 = 300401020 |. 68 34804000 push 00408034 ; Arg1 = ASCII %d,%d,%d00401025 |. E8 77000000 call _scanf0040102A |. 8B4424 10 mov eax, dword ptr esp+100040102E |. 8B4C24 14 mov ecx, dword ptr esp+1400401032 |. 8B5424 18 mov edx, dword ptr esp+1800401036 |. 50 push eax ; Arg3 = 800401037 |. 51 push ecx ; Arg2 = 500401038 |. 52 push edx ; Arg1 = 300401039 |. E8 12000000 call ?MaxNumYAHHHHZ00401050 /$ 8B4C24 04 mov ecx, dword ptr esp+400401054 |. 8B4424 08 mov eax, dword ptr esp+800401058 |. 3BC8 cmp ecx, eax0040105A |. 7C 09 jl short Fu.004010650040105C |. 8B4424 0C mov eax, dword ptr esp+C00401060 |. 3BC8 cmp ecx, eax00401062 |. 7D 09 jge short Fu.0040106D00401064 |. C3 retn00401065 | 8B4C24 0C mov ecx, dword ptr esp+C00401069 |. 3BC1 cmp eax, ecx0040106B |. 7D 02 jge short Fu.0040106F0040106D | 8BC1 mov eax, ecx0040106F C3 retn0040103E |. 50 push eax0040103F |. 68 30804000 push 00408030 ; ASCII %d,LF00401044 |. E8 27000000 call _printf00401049 |. 83C4 30 add esp, 30 ; 回復堆棧平衡0040104C . C3 retn這里面應該沒有我們不認識的匯編指令吧。我們再這里就看一下這些代碼,當然如果可以的話,你可以單步跟蹤這個程序,尤其注意看下堆棧的變化。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 參數nPush 參數2Push 參數1call 函數首地址add esp, 函數參數的個數*4由于很多的C庫函數都是用這樣的方式調用它,所以,這種函數的調用方式叫做C類調用(在C語言中用在這個程序中,模式都是這類的調用方式,也可以用_cdecl修飾)在這個程序中,由于main使用的3個函數都是這一種調用方式,編譯器為了減少指令把堆棧一起平衡了,而沒有分別對每個函數進行堆棧平衡。2. _stdcall 調用方式好的,現在我們把我們自己定義的函數MaxNum的聲明和定義都改成這樣:int _stdcallMaxNum(int num001, int num002, int num003);這樣,Main函數中應該就是有兩種調用方式了,我們可以更清楚的看出C類調用的特點:00401000 /$ 83EC 0C sub esp, 0C00401003 |. 33C0 xor eax, eax00401005 |. 8D4C24 04 lea ecx, dword ptr esp+400401009 |. 894424 08 mov dword ptr esp+8, eax0040100D |. 894424 04 mov dword ptr esp+4, eax00401011 |. 894424 00 mov dword ptr esp, eax00401015 |. 8D4424 00 lea eax, dword ptr esp00401019 |. 50 push eax0040101A |. 8D5424 0C lea edx, dword ptr esp+C0040101E |. 51 push ecx0040101F |. 52 push edx00401020 |. 68 34804000 push Func.00408034 ; ASCII %d,%d,%d00401025 |. E8 87000000 call Func.scanf0040102A |. 8B4424 10 mov eax, dword ptr esp+100040102E |. 8B4C24 14 mov ecx, dword ptr esp+1400401032 |. 8B5424 18 mov edx, dword ptr esp+1800401036 |. 83C4 10 add esp, 10; 平衡Scanf的參數使用的堆棧00401039 |. 50 push eax0040103A |? 51 push ecx0040103B |? 52 push edx0040103C |? E8 0F000000 call Func.MaxNum00401050 /$ 8B4C24 04 mov ecx, dword ptr esp+400401054 |. 8B4424 08 mov eax, dword ptr esp+800401058 |. 3BC8 cmp ecx, eax0040105A |. 7C 0B jl short Func.004010670040105C |. 8B4424 0C mov eax, dword ptr esp+C00401060 |. 3BC8 cmp ecx, eax00401062 |. 7D 0B jge short Func.0040106F00401064 |. C2 0C00 retn 0C00401067 |? 8B4C24 0C mov ecx, dword ptr esp+C0040106B |. 3BC1 cmp eax, ecx0040106D | 7D 02 jge short Func.004010710040106F 8BC1 mov eax, ecx00401071 |. C2 0C00 retn 0C; _stdcall的調用方式,在子函數中平衡堆棧00401041 |? 50 push eax00401042 |? 68 30804000 push Func.00408030 ; ASCII %d,LF00401047 |? E8 34000000 call Func.printfGetStringTypeWsWyte0040104C . 83C4 14 add esp, 14; 這里只平衡printf的參數跟一開始申請的0xC的堆棧就可以了。0040104F C3 retn我們在程序中加入的_stdcall就修改程序默認的調用方式為Windows標準調用方式,現在我們留心看一下MaxNum函數的調用和實現部分,總結一下這種Win標準調用的特點:push 參數nPush 參數2Push 參數1call 函數首地址函數的代碼;retn 函數參數的個數*4; 這里就相當于add esp, 函數參數的個數*4 ,然后再RETN這類調用就是windows的標準調用,它的修飾符號是_stdcall,幾乎所有的windows的API都用這種方式調用,所以,在VS開發環境中,_stdcall又被定義成了WINAPI。3. _fastcall調用方式為了保證本次課題的完整性,我多嘮叨幾句,說一下_fastcall的調用方式(本來是想在下一次課題面向對象編程中再講述的),這種方式的調用在面向對象編程中比較常見,這里大概的做一下簡單的介紹,等在下一次課題:C+的基礎特性 中詳細講述。這種調用方式就是同時使用寄存器和堆棧一起傳遞參數,為了描述的更清楚,我們還是用上一小節的程序做例子,我們再把程序中的_stdcall改成_fastcall,然后Release編譯,OD打開:00401000 /$ 83EC 0C sub esp, 0C00401003 |. 33C0 xor eax, eax00401005 |. 8D4C24 04 lea ecx, dword ptr esp+400401009 |. 894424 08 mov dword ptr esp+8, eax0040100D |. 894424 04 mov dword ptr esp+4, eax00401011 |. 894424 00 mov dword ptr esp, eax00401015 |. 8D4424 00 lea eax, dword ptr esp00401019 |. 50 push eax0040101A |. 8D5424 0C lea edx, dword ptr esp+C0040101E |. 51 push ecx0040101F |. 52 push edx00401020 |. 68 34804000 push Func.00408034 ; ASCII %d,%d,%d00401025 |. E8 77000000 call Func.scanf0040102A |. 8B4424 10 mov eax, dword ptr esp+100040102E |. 8B5424 14 mov edx, dword ptr esp+1400401032 |. 8B4C24 18 mov ecx, dword ptr esp+1800401036 |. 83C4 10 add esp, 1000401039 |. 50 push eax0040103A |. E8 11000000 call Func.MaxNumr; 原本3個參數的函數,現在編程一個參數了00401050 /$ 8B4424 04 mov eax, dword ptr esp+4 ; 從這里明白:它用了ECX和EDX傳遞了兩個參數00401054 |. 3BCA cmp ecx, edx00401056 |. 7C 09 jl short Func.0040106100401058 |. 3BC8 cmp ecx, eax0040105A |. 7C 0B jl short Func.004010670040105C |. 8BC1 mov eax, ecx0040105E |. C2 0400 retn 400401061 | 3BD0 cmp edx, eax00401063 |. 7C 02 jl short Func.0040106700401065 |. 8BC2 mov eax, edx00401067 C2 0400 retn 4看到了么?也不麻煩哦,我們總結一下_fastcall的調用特點:push 參數nmov edx, 參數2mov ecx, 參數1call 函數首地址函數的代碼;retn 函數參數的個數*4; 這里就相當于add esp, 函數參數的個數*4 ,然后再RETN當然,也不完全都是使用ECX和EDX兩個寄存器,根據編譯器的不同,使用的寄存器也不同,如果調試程序調試的多了,我們可以發現:a) VS的編譯器如果用_fastcall方式調用函數,一般都是將最左邊的兩個小于DWORD類型的參數分別用ECX和EDX傳遞。b) Borland公司的編譯器如果用_fastcall方式調用函數,一般都是將最左邊的三個小于DWORD類型的參數分別用EAX,EDX和ECX傳遞。更多的特點還需要大家自己去總結。4. PASCAL 調用方式還有一種調用方式:PASCAL方式調用,限于篇幅,這里就不再舉例子了,只是簡單的總結一下它的特點,我們就進入下一節:Push 參數2Push 參數1push 參數ncall 函數首地址函數的代碼;retn 函數參數的個數*4; 這里就相當于add esp, 函數參數的個數*4 ,然后再RETN很明顯,這種調用方式與_stdcall方式十分相似,就是傳遞的參數順序不同而已。四、 撩開函數的面紗。如果認真調試了本節的這個程序的朋友,一定會發現如下一些知識點:a) 函數的返回值一般存放再EAX中。b) 用esp+偏移和用ebp+偏移標識函數的參數及局部變量的異同。c) CALL/JMP的區別。a) 區別就是一個CALL就相當于push eip+5 然后再JMP 到指定的代碼中。這樣,再retn的時候,就知道返回到哪個地址了。b) 利用這個有很多的用法和小技巧,比如代碼自定位 等等。其實歸根結底,程序的代碼本身就是數據,當若我們比較一個數組的二進制數據跟一個函數代碼的二進制形式,我們根本無法區別他們,換句話說,我們完全可以把代碼當作數據來處理,這里引用一個比較簡單的例子,大家可以一起試一下:/showthread.php?t=71790另外,我們知道,一個函數的參數一般都是變量,當我們把一個函數名字(函數的首地址)當作一個變量來處理,那我們完全就可以讓一個函數名作為另一個函數的參數,這個最典型的應用就是回調。具體的我們等到提高篇中具體講解 回調函數。本節為了證明我上面的描述,給出一個小程序,算是開闊視野,也算是最函數的本質做個解釋,希望大家能調試跟蹤一下。#include stdio.h#include windows.htypedef unsigned char BYTE;typedef VOID (CALLBACK *MYSPRINTF)(char *, const char *, .);typedef VOID (CALLBACK *MYLSTRCAT)(char *, char *);typedef VOID (CALLBACK *MYMSGBOX)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);MYSPRINTF mySprintf = (MYSPRINTF)GetProcAddress(LoadLibraryA(msvcr71.dll), sprintf); MYLSTRCAT myStrCat = (MYLSTRCAT)GetProcAddress(LoadLibraryA(KERNEL32.dll), lstrcatA); MYMSGBOX myMsgBox = (MYMSGBOX)GetProcAddress(LoadLibraryA(user32.dll), MessageBoxA); BYTEbuf= 0xB8,0x00,0x12,0x00,0x00,0xE8,0xAE,0x00,0x00,0x00,0x55,0x56,0x57,0xB9,0x7F,0x00,0x00,0x00,0x33,0xC0,0x8D,0x7C,0x24,0x0D,0xC6,0x44,0x24,0x0C,0x00,0xC6,0x84,0x24,0x0C,0x02,0x00,0x00,0x00,0xF3,0xAB,0x66,0xAB,0xAA,0xB9,0xFF,0x03,0x00,0x00,0x33,0xC0,0x8D,0xBC,0x24,0x0D,0x02,0x00,0x00,0xBE,0x01,0x00,0x00,0x00,0xF3,0xAB,0x66,0xAB,0xAA,0xBF,0x01,0x00,0x00,0x00,0x3B,0xF7,0x7C,0x33,0x8B,0xEE,0xA1,0x18,0x61,0x40,0x00,0x55,0x57,0x56,0x8D,0x4C,0x24,0x18,0
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025農藥種子購銷合同范本
- 河北承德市高新區2024-2025學年高二下冊期中考試數學試卷附解析
- 廣東省湛江市2023?2024學年高二下冊期末考試數學試卷附解析
- 2024年黔南州荔波縣“特崗計劃”教師招聘真題
- 2024年琿春市事業單位招聘真題
- 殯儀館可行性研究報告
- 社區移動應用開發基礎知識點歸納
- 2025年寧夏初級注冊安全工程師試題
- 石大學前衛生學試卷(五)及參考答案
- 在線教育與數字技能培訓模式的創新探索
- 人民調解業務知識培訓講座課件
- 《活著》讀書分享優秀課件
- 《中國近代史綱要》社會實踐作業
- 中興項目管理初級認證VUE題庫(含答案)
- 三年級上冊第一單元習作課件
- 中醫藥膳學:中醫藥膳制作的基本技能課件
- 往來款項明細表-A4
- 甘肅省人力資源服務機構
- 飾面板安裝工程檢驗批質量驗收記錄
- 北京市科技計劃項目(課題)驗收(結題)管理細則(試行)
- 路基交驗具體要求(共5頁)
評論
0/150
提交評論