C++從零開始(七)——何謂函數
原始出處:網絡
本篇之前的內容都是基礎中的基礎,理論上只需前面所說的內容即可編寫出幾乎任何只操作內存的程序,也就是本篇以後說明的內容都可以使用之前的內容自己實現,只不過相對要麻煩和復雜許多罷了。
本篇開始要比較深入地討論C++提出的很有意義的功能,它們大多數和前面的switch語句一樣,是一種技術的實現,但更為重要的是提供了語義的概念。所以,本篇開始將主要從它們提供的語義這方面來說明各自的用途,而不像之前通過實現原理來說明(不過還是會說明一下實現原理的)。為了能清楚說明這些功能,要求讀者現在至少能使用VC來編譯並生成一段程序,因為後續的許多例子都最好是能實際編譯並觀察執行結果以加深理解(尤其是聲明和類型這兩個概念)。為此,如果你現在還不會使用VC或其他編譯器來進行編譯代碼,請先參看其他資料以了解如何使用VC進行編譯。為了後續例子的說明,下面先說明一些預備知識。
預備知識
寫出了C++代碼,要如何讓編譯器編譯?在文本文件中書寫C++代碼,然後將文本文件的文件名作為編譯器的輸入參數傳遞給編譯器,即叫編譯器編譯給定文件名所對應的文件。在VC中,這些由VC這個編程環境(也就是一個軟件,提供諸多方便軟件開發的功能)幫我們做了,其通過項目(Project)來統一管理書寫有C/C++代碼的源文件。為了讓VC能了解到哪些文件是源文件(因為還可能有資源文件等其他類型文件),在用文本編輯器書寫了C++代碼後,將其保存為擴展名為.c或.cpp(C Plus Plus)的文本文件,前者表示是C代碼,而後者表示C++代碼,則缺省情況下,VC就能根據不同的源文件而使用不同的編譯語法來編譯源文件。
前篇說過,C++中的每條語句都是從上朝下執行,每條語句都對應著一個地址,那麼在源文件中的第一條語句對應的地址就是0嗎?當然不是,和在棧上分配內存一樣,只能得到相對偏移值,實際的物理地址由於不同的操作系統將會有各自不同的處理,如在Windows下,代碼甚至可以沒有物理地址,且代碼對應的物理地址還能隨時變化。
當要編寫一個稍微正常點的程序時,就會發現一個源文件一般是不夠的,需要使用多個源文件來寫代碼。而各源文件之間要如何連接起來?對此C++規定,凡是生成代碼的語句都要放在函數中,而不能直接寫在文本文件中。關於函數後面馬上說明,現在只需知道函數相當於一個外殼,它通過一對“{}”將代碼括起來,進而就將代碼分成了一段一段,且每一段代碼都由函數名這個項目內唯一的標識符來標識,因此要連接各段代碼,只用通過函數名即可,後面說明。前面說的“生成代碼”指的是表達式語句和指令語句,雖然定義語句也可能生成代碼,但由於其代碼生成的特殊性,是可以直接寫在源文件內(在《C++從零開始(十)》中說明),即不用被一對“{}”括起來。
程序一開始要從哪裡執行?C++強行規定,應該在源文件中定義一個名為main的函數,而代碼就從這個函數處開始運行。應該注意由於C++是由編譯器實現的,而它的這個規定非常的牽強,因此縱多的編譯器都又自行提供了另外的程序入口點定義語法(程序入口點即最開始執行的函數),如VC,為了編寫DLL文件,就不應有main函數;為了編寫基於Win32的程序,就應該使用WinMain而不是main;而VC實際提供了更加靈活的手段,實際可以讓程序從任何一個函數開始執行,而不一定非得是前面的WinMain、main等,這在《C++從零開始(十九)》中說明。
對於後面的說明,應知道程序從main函數開始運行,如下:
long a; void main(){ short b; b++; } long c;
上面實際先執行的是long a;和long c;,不過不用在意,實際有意義的語句是從short b;開始的。
函數(Function)
機器手焊接轎車車架上的焊接點,給出焊接點的三維坐標,機器手就通過控制各關節的馬達來使焊槍移到准確的位置。這裡控制焊槍移動的程序一旦編好,以後要求機器手焊接車架上的200個點,就可以簡單地給出200個點的坐標,然後調用前面已經編好的移動程序200次就行了,而不用再對每次移動重復編寫代碼。上面的移動程序就可以用一個函數來表示。
函數是一個映射元素。其和變量一樣,將一個標識符(即函數名)和一個地址關聯起來,且也有一類型和其關聯,稱作函數的返回類型。函數和變量不同的就是函數關聯的地址一定是代碼的地址,就好像前面說明的標號一樣,但和標號不同的就是,C++將函數定義為一種類型,而標號則只是純粹的二進制數,即函數名對應的地址可以被類型修飾符修飾以使得編譯器能生成正確的代碼來幫助程序員書實現上面的功能。
由於定義函數時編譯器並不會分配內存,因此引用修飾符“&”不再其作用,同樣,由數組修飾符“[]”的定義也能知道其不能作用於函數上面,只有留下的指針修飾符“*”可以,因為函數名對應的是某種函數類型的地址類型的數字。
前面移動程序之所以能被不同地調用200次,是因為其寫得很靈活,能根據不同的情況(不同位置的點)來改變自己的運行效果。為了向移動程序傳遞用於說明情況的信息(即點的坐標),必須有東西來完成這件事,在C++中,這使用參數來實現,並對於此,C++專門提供了一種類型修飾符——函數修飾符“()”。在說明函數修飾符之前,讓我們先來了解何謂抽象聲明符(Abstract Declarator)。
聲明一個變量long a;(這看起來和定義變量一樣,後面將說明它們的區別),其中的long是類型,用於修飾此變量名a所對應的地址。將聲明變量時(即前面的寫法)的變量名去掉後剩下的東西稱作抽象聲明符。比如:long *a, &b = *a, c[10], ( *d )[10];,則變量a、b、c、d所對應的聲明修飾符分別是long*、long&、long[10]、long(*)[10]。
函數修飾符接在函數名的後面,括號內接零個或多個抽象聲明符以表示參數的類型,中間用“,”隔開。而參數就是一些內存(分別由參數名映射),用於傳遞一些必要的信息給函數名對應的地址處的代碼以實現相應的功能。聲明一個函數如下:
long *ABC( long*, long&, long[10], long(*)[10] );
上面就聲明了一個函數ABC,其類型為long*( long*, long&, long[10], long(*)[10] ),表示欲執行此函數對應地址處開始的代碼,需要順序提供4個參數,類型如上,返回值類型為long*。上面ABC的類型其實就是一個抽象聲明符,因此也可如下:
long AB( long*( long*, long&, long[10], long(*)[10] ), short, long& );
對於前面的移動程序,就可類似如下聲明它:
void Move( float x, float y, float z );
上面在書寫聲明修飾符時又加上了參數名,以表示對應參數的映射。不過由於這裡是函數的聲明,上述參數名實際不產生任何映射,因為這是函數的聲明,不是定義(關於聲明,後面將說明)。而這裡寫上參數名是一種語義的體現,表示第一、二、三個參數分別代表X、Y、Z坐標值。
上面的返回類型為void,前面提過,void是C++提供的一種特殊數字類型,其僅僅只是為了保障語法的嚴密性而已,即任何函數執行後都要返回一個數字(後面將說明),而對於不用返回數字的函數,則可以定義返回類型為void,這樣就可以保證語法的嚴密性。應當注意,任何類型的數字都可以轉換成void類型,即可以( void )( 234 );或void( a );。
注意上面函數修飾符中可以一個抽象修飾符都沒有,即void ABC();。它等效於void ABC( void );,表示ABC這個函數沒有參數且不返回值。則它們的抽象聲明符為void()或void(void),進而可以如下:
long* ABC( long*(), long(), long[10] );
由函數修飾符的意義即可看出其和引用修飾符一樣,不能重復修飾類型,即不能void A()(long);,這是無意義的。同樣,由於類型修飾符從左朝右的修飾順序,也就很正常地有:void(*pA)()。假設這裡是一個變量定義語句(也可以看成是一聲明語句,後面說明),則表示要求編譯器在棧上分配一塊4字節的空間,將此地址和pA映射起來,其類型為沒有參數,返回值類型為void的函數的指針。有什麼用?以後將說明。
函數定義
下面先看下函數定義,對於前面的機器手控制程序,可如下書寫:
void Move( float x, float y, float z )
{
float temp;
// 根據x、y、z的值來移動焊槍
}
int main()
{
float x[200], y[200], z[200];
// 將200個點的坐標放到數組x、y和z中
for( unsigned i = 0; i < 200; i++ )
Move( x[ i ], y[ i ], z[ i ] );
return 0;
}
上面定義了一個函數Move,其對應的地址為定義語句float temp;所在的地址,但實際由於編譯器要幫我們生成一些附加代碼(稱作函數前綴——Prolog,在《C++從零開始(十五)》中說明)以獲得參數的值或其他工作(如異常的處理等),因此Move將對應在較float temp;之前的某個地址。Move後接的類型修飾符較之前有點變化,只是把變量名加上以使其不是抽象聲明符而已,其作用就是讓編譯器生成一映射,將加上的變量名和傳遞相應信息的內存的地址綁定起來,也就形成了所謂的參數。也由於此原因,就能如此書寫:void Move( float x, float, float z ) { }。由於沒有給第二個參數綁定變量名,因此將無法使用第二個參數,以後將舉例說明這樣的意義。
函數的定義就和前面的函數的聲明一樣,只不過必須緊接其後書寫一個復合語句(必須是復合語句,即用“{}”括起來的語句),此復合語句的地址將和此函數名綁定,但由於前面提到的函數前綴,函數名實際對應的地址在復合語句的地址的前面。
為了調用給定函數,C++提供了函數操作符“()”,其前面接函數類型的數字,而中間根據相應函數的參數類型和個數,放相應類型的數字和個數,因此上面的Move( x[ i ], y[ i ],