我們知道,函數是用來完成某個功能的相對獨立的一段代碼。函數在完成這個功能的時候,往往需要外部數據的支持,這時就需要在調用這個函數時向它傳遞所需要的數據它才能完成這個功能獲得結果。例如,當調用一個加法函數時,需要向它傳遞兩個數作為加數和被加數,然後在它內部才能對這兩個數進行計算獲得加和結果。在定義一個函數的時候,如果這個函數需要跟外部進行數據交換,就需要在函數定義中加入形式參數表,以確定函數的調用者向函數傳遞的數據個數和具體類型。例如,可以這樣定義需要兩個int類型加數的加法函數:
// 聲明並定義Add()函數 // 形式參數表確定了這個函數需要兩個int類型的參數 int Add( int a, int b ) { return a + b; }
從Add()函數的聲明中可以知道這個函數有兩個int類型數作為參數,所以在調用的時候,就需要用兩個int類型數作為實際的參數對其進行調用:
// 以1和2作為實際參數調用Add()函數 // 也就是向這個函數傳遞1和2這兩個數據 int nRes = Add(1, 2);
我們將在定義函數時形式參數表中給出的參數稱為形式參數,例如這裡的a和b;而將在函數調用時函數名之後括號中給出的參數稱為實際參數,例如這裡的1和2。在執行函數調用的時候,系統會使用調用函數時給出的實際參數一一對應地對函數聲明中的各個形式參數進行賦值。例如,在執行“Add(1,2)”函數調用時,這裡1和2兩個實際參數會分別被賦值給Add()函數的兩個形式參數a和b。也就是說,在程序進入Add()函數內部執行時,a和b兩個變量的值一開始就是1和2,這也就意味著,通過形式參數,我們將數據1和2 從函數的調用者傳遞進入了Add()函數內部。如圖5-6所示。
圖5-6 函數調用過程中的參數傳遞
在執行函數調用時,系統需要將實際參數復制給形式參數以實現數據的傳遞。可是如果要向函數內部傳遞一些比較大的數據,比如擁有多個數據元素的數組,這個復制過程會非常耗費時間而顯著地降低程序的效率。為了提高效率,更多時候,我們用傳遞指向這個大體積數據的指針來代替傳遞這個數據本身。在函數內部,可以通過指針來訪問它所指向的外部數據,同樣可以達到向函數內部傳遞數據的目的。例如,在前面的工資程序中,當所有工資數據都輸入到arrSalary數組後,需要將這些工資數據傳遞給GetAverage()函數來計算平均工資。這時,我們就可以向函數傳遞指向這個數組的指針,也就是數組名,來實現向函數傳遞整個數組的目的:
// 定義計算數組平均值的函數 float GetAverage(int* pArr,int nCount) { // 判斷數據是否合法 if(nCount <= 0 || nullptr == pArr) { return 0; // 如果數據不合法,返回默認值 } // 計算平均值 int nTotal = 0; // 用for循環遍歷數組,統計工資總數 for(int i = 0; i < nCount; ++i) { // 通過傳遞進來的數組首地址指針訪問數組元素 nTotal += pArr[i]; } // 返回工資總數與數據個數的商,就是平均工資 return (float)nTotal/nCount; }
int main() { // 定義保存工資數據的數組 const int NUM = 100000; int arrSalary[NUM] = {0}; // 輸入工資數據到數組… // 以數組名(數組首地址)和數據元素個數為實際參數調用函數 float fAver = GetAverage(arrSalary,NUM); cout<<"平均工資是:"<<fAver<<endl; return 0; }
在定義GetAverage()函數時,我們定義了兩個形式參數,第一個“int*”類型的pArr表示指向數組首地址的指針。只要有了這個指針,在函數內部,我們就可以把它當作一個數組名直接通過它訪問數組中的各個數據元素。例如在for循環中用“pArr[i]”的形式就可以訪問到數組中的各個元素。因為第一個參數只是數組的首地址,並沒有包含數組元素個數的信息,所以要想訪問整個數組,還需要用第二個int類型的形式參數nCount來表示這個數組中數據元素的個數。這樣,我們在函數內部就可以利用傳遞進來的數組首地址和元素個數,用for循環對整個數組進行遍歷訪問統計工資總數,進而計算工資平均值。在調用GetAverage()函數時,按照函數的聲明要求,我們將數組名arrSalary,也就是指向數組首地址的指針,以及數組元素個數NUM作為實際參數對其進行調用。
// 以數組名和數據元素個數為實際參數調用函數 float fAver = GetAverage(arrSalary,NUM);
在執行這個函數調用的時候,實際參數arrSalary和NUM會分別被復制給形式參數pArr和nCount。這樣,在GetAverage()函數內部就可以通過pArr和nCount訪問arrSalary數組,也就是通過一個小小的指針向函數傳遞了一個大大的數組。整個過程不需要復制arrSalary數據的100000個int類型數據,取而代之的是4個字節的數組首地址指針,從而避免了大量數據的復制,提高了函數調用的效率。
知道更多:訪問主函數的參數,接收命令行傳遞的數據
要向普通函數傳遞數據,我們可以在調用函數時通過函數參數的形式來完成。但是主函數不會被我們調用,如果我們想要向主函數傳遞數據,則要借助在執行程序時的命令行參數來完成。例如,我們想要傳遞兩個加數給一個加法計算程序add.exe並讓它計算結果,就可以以如下的命令形式來執行這個程序,它就會接收命令行中的兩個加數並計算得到結果:
F:\code>add.exe 3 4(回車) 3 + 4 = 7 (輸出結果)
要想做到這一點,我們需要給main()主函數添加兩個參數:int類型的argc和字符串指針數組類型的argv。當我們在命令行執行程序時,操作系統會根據我們的命令行指令對這兩個參數分別賦值。其中,第一個參數argc就是命令行中指令的個數,包括程序名本身在內。在這裡,對於“add.exe 3 4”這個命令行指令而言,argc的值就應該是3。而第二個參數argv實際上是一個字符串指針數組,其中的各個字符串指針,依次指向命令行中各個指令字符串。當然,也包括程序名在內。這樣,argv[0]指向的就是“add.exe”這個字符串,而argv[1]指向的就是“3”這個字符串,依次類推。明白這些規則後,我們就可以在主函數中通過訪問這兩個參數,從而接收從命令行傳遞進來的數據:
#include <iostream> using namespace std; int main(int argc, char* argv[]) { // 根據指令的個數(argc),判斷指令是否正確 // 如果不正確,則提示正確的使用方法 if(3 != argc) // 通過argc得到指令個數 { // 通過argv[0]得到程序的名字 cout<<"用法: "<<argv[0]<<" num1 num2"<<endl; return -1; // 命令行指令不合法,返回一個錯誤值 } // 如果指令正確,則通過argv訪問命令行傳遞的數 // 通過atoi()函數, // 分別將argv[1]和argv[2]指向的字符串“3”和“4”轉換為數字3和4 int a = atoi(argv[1]); int b = atoi(argv[2]); // 利用轉換後的數據計算結果 int res = a + b; // 輸出結果 // 這裡,將命令行指令當作字符串來訪問,直接輸出 cout<<argv[1]<<" + "<<argv[2]<<" = "<<res<<endl; return 0; }
在主函數中,我們首先利用argc對命令行指令的個數進行了判斷,以此來判斷程序的執行方式是否正確。然後,就是從argv字符串指針數組中獲取程序執行時的各個指令了。因為argv提供給我們的是命令行指令的字符串,如果是數字指令,我們還需要利用atoi()等轉換函數,將字符串轉換成對應的數值數據。在完成命令行指令從字符串到數字的轉換之後,我們就可以將其用於計算並輸出結果。在輸出的時候,我們又將argv數組中的字符串指針用作字符串直接輸出命令行的指令。
通過主函數的argc和argv參數,讓我們可以接收來自命令行指令的數據,從而在執行程序的時候,對程序的行為進行控制(提供選項或者數據等),極大地增加了程序執行的靈活性。
到這裡我們已經知道了,函數就像一個具有某種功能的箱子,把原材料數據通過函數參數放進去,函數箱子經過一定的處理後,就得到了我們想要的結果數據。比如,把兩個整數放到Add()函數箱子中,經過加和處理後得到的就是這兩個整數的和。通過函數參數,我們可以把原材料數據放到函數箱子中去,那麼我們又如何從函數箱子中取出結果數據呢?
還記得在聲明函數的時候,需要指明這個函數的返回值類型嗎?只要一個函數的返回值類型不是void,那它就具有返回值,而我們就是通過函數的返回值來從函數中取得結果數據的。下面還是以Add()函數為例:
int Add( int a, int b ) { // 計算結果數據 int res = a + b; // 利用return關鍵字返回結果數據 return res; } // 調用函數,獲得計算結果 int nRes = Add(2,3);
在函數內部,我們首先對通過參數傳遞進來的原材料數據2和3進行加和計算,得到結果數據5,然後使用return關鍵字結束函數的執行並將結果數據(5)返回,而從調用這個函數的外部來看,這個結果數據也就是整個函數調用表達式“Add(2,3)”的值,進而,我們可以把這個值賦值給nRes變量,nRes變量的值變為5。這也就是說,通過返回值我們從Add()函數中取回了結果數據5。換句話說,函數調用表達式的值就是從函數箱子中取出的結果數據,這個數據的類型就是函數的返回值類型。
既然整個函數調用表達式可以看成是從函數得到的結果數據,擁有特定的數據類型,那麼除了可以用它對變量進行賦值之外,還可以將其應用在任何可以使用此類型數值的地方直接參與計算。例如,函數調用表達式可以用在條件語句中表示某個復雜條件是否成立:
// IsPassed()函數的返回值是bool類型 // 它的調用表達式可以看成是一個bool類型的數據,可以直接與true進行邏輯運算 if( true == IsFinished()) { // … }
另外,對於返回值為bool類型的函數調用表達式,可以被看作是一個bool類型的數據,從而上面的代碼還可以改寫成下面這種更簡潔的形式:
// 直接判斷IsFinished()函數的返回值是否是true, // 如果我們要判斷IsFinished()函數的返回值是否是false, // 則可以用if( !IsFinished() )的形式 if( IsFinished() ) { // … }
除此之外,函數調用表達式還可以應用在另一個函數調用表達式中,直接作為參數參與另一個函數調用。例如:
// 函數調用表達式Power(2)和Power(3)是整型數值, // 直接用做Add()函數的整型參數參與其調用 int nRes = Add( Power(2), Power(3) );
在執行計算的時候,會先分別計算Power(2)和Power(3)這兩個函數調用表達式的值得到4和9,然後再以這兩個數據作為參數調用Add()函數,得到最終結果13。這裡值的提醒的是,這種把一個函數調用表達式當作某個數據直接參與計算的方式,雖然可以讓代碼更加簡潔,但是卻在一定程度上降低了代碼的可讀性,所以應該有選擇地使用,避免形成過於復雜的表達式,達到代碼簡潔與可讀性之間的平衡。
從以上代碼可以注意到,每個函數只有唯一的返回值,使用函數返回值只能從函數中取出一個數據,如果想要從函數中取出多個數據又該怎麼辦呢?
回想一下,我們是如何將一個大體積的數據傳入函數的?是的,我們使用了指針。利用指針的指代特性,可以在函數內部通過指針訪問它所指向外部內存,讀取其中的數據,從而間接地實現將函數外的數據傳入函數。同樣地,我們在通過指針訪問它所指向的外部內存時,也可以將函數內的數據寫入這個內存位置,從而間接地實現將函數內的數據傳出函數。不好理解嗎?沒關系,來看一個實際的例子。在我們前面的工資程序中,我們需要用一個InputSalary()函數來負責工資數據的輸入,這時就需要利用指針將函數內輸入的工資數據傳出函數:
// 輸入員工的工資數據 int InputSalary(int* pArr, const int MAX_NUM ) { // 參數有效性檢查… = 0; // 臨時變量,暫存用戶輸入的數據 int nIndex = 0; // 輸入的序號 do { cout<<"請輸入第"<<nIndex<<"號員工的工資:"<<endl; cin>>nTemp; // 如果輸入的是負數或零,表示輸入工作結束,跳出輸入循環 if ( nTemp <= 0 ) { break; } // 將合法的數據保存到數組中,開始下一次輸入 // 通過指針將數據寫入它指向的外部數組,實現數據的傳出 pArr[nIndex] = nTemp; ++nIndex; } while ( nIndex < MAX_NUM ); // 返回輸入的數據總個數 return nIndex; }
InputSalary()函數的第一個參數pArr指向的是函數外部用於保存工資數據的數組。這樣在函數內部,我們就可以通過這個指針將用戶輸入的工資數據保存到它所指向的外部數組,從而間接實現了函數內部多個數據的傳出。另外在這個函數中,還利用函數返回值從函數中得到了輸入的數據總個數。這也表明,函數返回值和函數指針參數這兩種方式都可以從函數中傳出數據。它們可以單獨使用,也可以混合使用。一般而言,函數返回值多用於從函數內返回單個小體積數據,比如某個基本數據類型的結果數據,而函數指針參數多用於從函數內返回多個或大體積數據,比如包含多個數據的數組或大體積的結構體。
現在,我們就可以利用InputSalary()函數將工資數據輸入到arrSalary數組,然後再利用前面的GetAverage()函數來統計平均工資,實現工資程序對平均工資的統計功能:
int main() { // 定義保存工資數據的數組 t int NUM = 100000; int arrSalary[NUM] = {0}; // 輸入工資數據到數組,用指針實現傳出數據 int nCount = InputSalary(arrSalary,NUM); // 統計平均工資,用指針實現傳入數據 float fAver = GetAverage(arrSalary,nCount); cout<<"平均工資是:"<<fAver<<endl; return 0; }
在這裡,我們用數組名arrSalary作為InputSalary()函數的參數, 用於從函數內傳出數據,而同樣的arrSalary用作GetAverage()函數的參數,則是用於向函數內傳入數據。這是因為,通過指針,既可以對它所指向的內存做寫操作,從而將函數內的數據傳出函數,同時也可以做讀操作,從而將函數外的數據傳入函數。指針參數既可以傳出數據也可以傳入數據,而至於到底是傳出還是傳入,取決於函數內部對它所指向內存的訪問是寫還是讀,如圖5-7所示。
圖 5-7 通過指針實現函數中數據的傳出與傳入
從這個例子也可以看到,經過“自頂向下,逐步求精”的功能分解,我們將主函數中相對獨立的輸入功能和統計功能分別封裝到了InputSalary()函數和GetAverage()函數中,實現了將復雜程序分解裝箱的工作。經過這樣的分解封裝,之前比較復雜臃腫的主函數,現在只需要簡單地調用這兩個子函數就完成了所有功能。將程序裝箱成函數,整個程序結構變得更加清晰,實現和維護都更加容易。