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 ], z[ i ] );就是使用了函數操作符,用x[ i ]、y[ i ]、z[ i ]的值作爲參數,並記錄下當前所在位置的地址,跳轉到Move所對應的地址繼續執行,當從Move返回時,根據之前記錄的位置跳轉到函數調用處的地方,繼續後繼代碼的執行。

    函數操作符由於是操作符,因此也要返回數字,也就是函數的返回值,即可以如下:

    float AB( float x ) { return x * x; } int main() { float c = AB( 10 ); return 0; }

    先定義了函數AB,其返回float類型的數字,其中的return語句就是用於指明函數的返回值,其後接的數字就必須是對應函數的返回值類型,而當返回類型爲void時,可直接書寫return;。因此上面的c的值爲100,函數操作符返回的值爲AB函數中的表達式x * x返回的數字,而AB( 10 )將10作爲AB函數的參數x的值,故x * x返回100.

    由於之前也說明了函數可以有指針,將函數和變量對比,則直接書寫函數名,如:AB;。上面將返回AB對應的地址類型的數字,然後計算此地址類型數字,應該是以函數類型解釋相應地址對應的內存的內容,考慮函數的意義,將發現這是毫無意義的,因此其不做任何事,直接返回此地址類型的數字對應的二進制數,也就相當於前面說的指針類型。因此也就可以如下:

    int main() { float (*pAB)( float ) = AB; float c = ( *pAB )( 10 ); return 0; }

    上面就定義了一個指針pAB,其類型爲float(*)( float ),一開始將AB對應的地址賦值給它。爲什麼沒有寫成pAB = &AB;而是pAB = AB;?因爲前面已經說了,函數類型的地址類型的數字,將不做任何事,其效果和指針類型的數字一樣,因此pAB = AB;沒有問題,而pAB = &AB;就更沒有問題了。可以認爲函數類型的地址類型的數字編譯器會隱式轉換成指針類型的數字,因此既可以( *pAB )( 10 );,也能( *AB )( 10 );,因爲後者編譯器進行了隱式類型轉換。

    由於函數操作符中接的是數字,因此也可以float c = AB( AB( 10 ) );,即c爲10000.還應注意函數操作符讓編譯器生成一些代碼來傳遞參數的值和跳轉到相應的地址去繼續執行代碼,因此如下是可以的:

    long AB( long x ) { if( x > 1 ) return x * AB( x - 1 ); else return 1; }

    上面表示當參數x的值大於1時,將x - 1返回的數字作爲參數,然後跳轉到AB對應的地址處,也就是if( x > 1 )所對應的地址,重複運行。因此如果long c = AB( 5 );,則c爲5的階乘。上面如果不能理解,將在後面說明異常的時候詳細說明函數是如何實現的,以及所謂的堆棧溢出問題。

    現在應該瞭解main函數的意義了,其只是建立一個映射,好讓連接器制定程序的入口地址,即main函數對應的地址。上面函數Move在函數main之前定義,如果將Move的定義移到main的下面,上面將發生錯誤,說函數Move沒定義過,爲什麼?因爲編譯器只從上朝下進行編譯,且只編譯一次。那上面的問題怎麼辦?後面說明。

    重載函數

    前面的移動函數,如果只想移動X和Y座標,爲了不移動Z座標,就必須如下再編寫一個函數:

    void Move2( float x, float y );

    它爲了不和前面的Move函數的名字衝突而改成Move2,但Move2也表示移動,卻非要變一個名字,這嚴重地影響語義。爲了更好的從源代碼上表現出語義,即這段代碼的意義,C++提出了重載函數的概念。

    重載函數表示函數名字一樣,但參數類型及個數不同的多個函數。如下:

    void Move( float x, float y, float z ) { }和void Move( float x, float y ) { }

    上面就定義了兩個重載函數,雖然函數名相同,但實際爲兩個函數,函數名相同表示它們具有同樣的語義——移動焊槍的程序,只是移動方式不同,前者在三維空間中移動,後者在一水平面上移動。當Move( 12, 43 );時就調用後者,而Move( 23, 5, 12 );時就調用前者。不過必須是參數的不同,不能是返回值的不同,即如下將會報錯:

    float Move( float x, float y ) { return 0; }和void Move( float a, float b ) { }

    上面雖然返回值不同,但編譯器依舊認爲上面定義的函數是同一個,則將說函數重複定義。爲什麼?因爲在書寫函數操作符時,函數的返回值類型不能保證獲得,即float a = Move( 1, 2 );雖然可以推出應該是前者,但也可以Move( 1, 2 );,這樣將無法得知應該使用哪個函數,因此不行。還應注意上面的參數名字雖然不同,但都是一樣的,參數名字只是表示在那個函數的作用域內其映射的地址,後面將說明。改成如下就沒有問題:

    float Move( float x, float y ) { return 0; }和void Move( float a, float b, float c ) { }

    還應注意下面的問題:

    float Move( float x, char y ); float Move( float a, short b ); Move( 10, 270 );

    上面編譯器將報錯,因爲這裏的270在計算函數操作符時將被認爲是int,即整型,它即可以轉成char,也可以轉成short,結果編譯器將無法判斷應是哪一個函數。爲此,應該Move( 10, ( char )270 );。

    聲明和定義

    聲明是告訴編譯器一些信息,以協助編譯器進行語法分析,避免編譯器報錯。而定義是告訴編譯器生成一些代碼,並且這些代碼將由連接器使用。即:聲明是給編譯器用的,定義是給連接器用的。這個說明顯得很模糊,爲什麼非要弄個聲明和定義在這攪和?那都是因爲C++同意將程序拆成幾段分別書寫在不同文件中以及上面提到的編譯器只從上朝下編譯且對每個文件僅編譯一次。

    編譯器編譯程序時,只會一個一個源文件編譯,並分別生成相應的中間文件(對VC就是。obj文件),然後再由連接器統一將所有的中間文件連接形成一個可執行文件。問題就是編譯器在編譯a.cpp文件時,發現定義語句而定義了變量a和b,但在編譯b.cpp時,發現使用a和b的代碼,如a++;,則編譯器將報錯。爲什麼?如果不報錯,說因爲a.cpp中已經定義了,那麼先編譯b.cpp再編譯a.cpp將如何?如果源文件的編譯順序是特定的,將大大降低編譯的靈活性,因此C++也就規定:編譯a.cpp時定義的所有東西(變量、函數等)在編譯b.cpp時將全部不算數,就和沒編譯過a.cpp一樣。那麼b.cpp要使用a.cpp中定義的變量怎麼辦?爲此,C++提出了聲明這個概念。

    因此變量聲明long a;就是告訴編譯器已經有這麼個變量,其名字爲a,其類型爲long,其對應的地址不知道,但可以先作個記號,即在後續代碼中所有用到這個變量的地方做上記號,以告知連接器在連接時,先在所有的中間文件裏尋找是否有個叫a的變量,其地址是多少,然後再修改所有作了記號的地方,將a對應的地址放進去。這樣就實現了這個文件使用另一個文件中定義的變量。

    所以聲明long a;就是要告訴編譯器已經有這麼個變量a,因此後續代碼中用到a時,不要報錯說a未定義。函數也是如此,但是有個問題就是函數聲明和函數定義很容易區別,因爲函數定義後一定接一複合語句,但是變量定義和變量聲明就一模一樣,那麼編譯器將如何識別變量定義和變量聲明?編譯器遇到long a;時,統一將其認爲是變量定義,爲了能標識變量聲明,可藉助C++提出的修飾符extern.

    修飾符就是聲明或定義語句中使用的用以修飾此聲明或定義來向編譯器提供一定的信息,其總是接在聲明或定義語句的前面或後面,如:

    extern long a, *pA, &ra;

    上面就聲明(不是定義)了三個變量a、pA和ra.因爲extern表示外部的意思,因此上面就被認爲是告訴編譯器有三個外部的變量,爲a、pA和ra,故被認爲是聲明語句,所以上面將不分配任何內存。同樣,對於函數,它也是一樣的:

    extern void ABC( long );  或  extern long AB( short b );

    上面的extern等同於不寫,因爲編譯器根據最後的“;”就可以判斷出來上面是函數聲明,而且提供的“外部”這個信息對於函數來說沒有意義,編譯器將不予理會。extern實際還指定其後修飾的標識符的修飾方式,實際應爲extern"C"或extern"C++",分別表示按照C語言風格和C++語言風格來解析聲明的標識符。

    C++是強類型語言,即其要求很嚴格的類型匹配原則,進而才能實現前面說的函數重載功能。即之所以能幾個同名函數實現重載,是因爲它們實際並不同名,而由各自的參數類型及個數進行了修飾而變得不同。如void ABC(), *ABC( long ), ABC( long, short );,在VC中,其各自名字將分別被變成“?ABC@@YAXXZ”、“?ABC@@YAPAXJ@Z”、“?ABC@@YAXJF@Z”。而extern long a, *pA, &ra;聲明的三個變量的名字也發生相應的變化,分別爲“?a@@3JA”、“?pA@@3PAJA”、“?ra@@3AAJA”。上面稱作C++語言風格的標識符修飾(不同的編譯器修飾格式可能不同),而C語言風格的標識符修飾就只是簡單的在標識符前加上“_”即可(不同的編譯器的C風格修飾一定相同)。如:extern"C" long a, *pA, &ra;就變成_a、_pA、_ra.而上面的extern"C" void ABC(), *ABC( long ), ABC( long, short );將報錯,因爲使用C風格,都只是在函數名前加一下劃線,則將產生3個相同的符號(Symbol),錯誤。

    爲什麼不能有相同的符號?爲什麼要改變標識符?不僅因爲前面的函數重載。符號和標識符不同,符號可以由任意字符組成,它是編譯器和連接器之間溝通的手段,而標識符只是在C++語言級上提供的一種標識手段。而之所以要改變一下標識符而不直接將標識符作爲符號使用是因爲編譯器自己內部和連接器之間還有一些信息需要傳遞,這些信息就需要符號來標識,由於可能用戶寫的標識符正好和編譯器內部自己用的符號相同而產生衝突,所以都要在程序員定義的標識符上面修改後再用作符號。既然符號是什麼字符都可以,那爲什麼編譯器不讓自己內部定的符號使用標識符不能使用的字符,如前面VC使用的“?”,那不就行了?因爲有些C/C++編譯器及連接器溝通用的符號並不是什麼字符都可以,也必須是一個標識符,所以前面的C語言風格才統一加上“_”的前綴以區分程序員定義的符號和編譯器內部的符號。即上面能使用“?”來作爲符號是VC才這樣,也許其它的編譯器並不支持,但其它的編譯器一定支持加了“_”前綴的標識符。這樣可以聯合使用多方代碼,以在更大範圍上實現代碼重用,在《C++從零開始(十八)》中將對此詳細說明。

    當書寫extern void ABC( long );時,是extern"C"還是extern"C++"?在VC中,如果上句代碼所在源文件的擴展名爲。cpp以表示是C++源代碼,則將解釋成後者。如果是。c,則將解釋成前者。不過在VC中還可以通過修改項目選項來改變上面的默認設置。而extern long a;也和上面是同樣的。

    因此如下:

 extern"C++" void ABC(), *ABC( long ), ABC( long, short );
    int main(){ ABC(); }

    上面第一句就告訴編譯器後續代碼可能要用到這個三個函數,叫編譯器不要報錯。假設上面程序放在一個VC項目下的a.cpp中,編譯a.cpp將不會出現任何錯誤。但當連接時,編譯器就會說符號“?ABC@@YAXXZ”沒找到,因爲這個項目只包含了一個文件,連接也就只連接相應的a.obj以及其他的一些必要庫文件(後續文章將會說明)。連接器在它所能連接的所有對象文件(a.obj)以及庫文件中查找符號“?ABC@@YAXXZ”對應的地址是什麼,不過都沒找到,故報錯。換句話說就是main函數使用了在a.cpp以外定義的函數void ABC();,但沒找到這個函數的定義。應注意,如果寫成int main() { void ( *pA ) = ABC; }依舊會報錯,因爲ABC就相當於一個地址,這裏又要求計算此地址的值(即使並不使用pA),故同樣報錯。

    爲了消除上面的錯誤,就應該定義函數void ABC();,既可以在a.cpp中,如main函數的後面,也可以重新生成一個。cpp文件,加入到項目中,在那個。cpp文件中定義函數ABC.因此如下即可:

 extern"C++" void ABC(), *ABC( long ), ABC( long, short );
    int main(){ ABC(); } void ABC(){}

    如果你認爲自己已經瞭解了聲明和定義的區別,並且清楚了聲明的意思,那我打賭有50%的可能性你並沒有真正理解聲明的含義,這裏出於篇幅限制,將在《C++從零開始(十)》中說明聲明的真正含義,如果你是有些C/C++編程經驗的人,到時給出的樣例應該有50%的可能性會令你大吃一驚。

    調用規則

    調用規則指函數的參數如何傳遞,返回值如何傳遞,以及上述的函數名標識符如何修飾。其並不屬於語言級的內容,因爲其表示編譯器如何實現函數,而關於如何實現,各編譯器都有自己的處理方式。在VC中,其定義了三個類型修飾符用以告知編譯器如何實現函數,分別爲:__cdecl、__stdcall和__fastcall.三種各有不同的參數、函數返回值傳遞方式及函數名修飾方式,後面說明異常時,在說明了函數的具體實現方式後再一一解釋。由於它們是類型修飾符,則可如下修飾函數:

 void *__stdcall ABC( long ), __fastcall DE(), *( __stdcall *pAB )( long ) = &ABC;
 void ( __fastcall *pDE )() = DE;

    變量的作用域

    前面定義函數Move時,就說void Move( float a, float b );和void Move( float x, float y );是一樣的,即變量名a和b在這沒什麼意義。這也就是說變量a、b的作用範圍只限制在前面的Move的函數體(即函數定義時的複合語句)內,同樣x和y的有效範圍也只在後面的Move的函數體內。這被稱作變量的作用域。

 //////a.cpp//////
long e = 10;
void main()
{
    short a = 10;
    e++;
    {
        long e = 2;
        e++;
        a++;
    }
    e++;
}

    上面的第一個e的有效範圍是整個a.cpp文件內,而a的有效範圍是main函數內,而main函數中的e的有效範圍則是括着它的那對“{}”以內。即上面到最後執行完e++;後,long e = 2;定義的變量e已經不在了,也就是被釋放了。而long e = 10;定義的e的值爲12,a的值爲11.

    也就是說“{}”可以一層層嵌套包含,沒一層“{}”就產生了一個作用域,在這對“{}”中定義的變量只在這對“{}”中有效,出了這對“{}”就無效了,等同於沒定義過。

    爲什麼要這樣弄?那是爲了更好的體現出語義。一層“{}”就表示一個階段,在執行這個階段時可能會需要到和前面的階段具有相同語義的變量,如排序。還有某些變量只在某一階段有用,過了這個階段就沒有意義了,下面舉個例子:

  float a[10];
    // 賦值數組a
    for( unsigned i = 0; i < 10; i++ )
        for( unsigned j = 0; j < 10; j++ )
            if( a[ i ] < a[ j ] )
            {
                float temp = a[ i ];
                a[ i ] = a[ j ];
                a[ j ] = temp;
            }

    上面的temp被稱作臨時變量,其作用域就只在if( a[ i ] < a[ j ] )後的大括號內,因爲那表示一個階段,程序已經進入交換數組元素的階段,而只有在交換元素時temp在有意義,用於輔助元素的交換。如果一開始就定義了temp,則表示temp在數組元素尋找期間也有效,這從語義上說是不對的,雖然一開始就定義對結果不會產生任何影響,但應不斷地詢問自己——這句代碼能不能不要?這句代碼的意義是什麼?不過由於作用域的關係而可能產生性能影響,這在《C++從零開始(十)》中說明。

    下篇將舉例說明如何已知算法而寫出C++代碼,幫助讀者做到程序員的最基本的要求——給得出算法,拿得出代碼。

   

 

發佈了3 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章