C++primer函數進階

一、內聯函數

內聯函數是爲了提高效率而出現的,它和普通函數的的不同不表現在編寫形式上,而是組合到整個程序當中的方式不同。函數的每一次調用都需要開闢棧空間,跳轉等一系列的操作。對於一些函數代碼比較少,並且多次使用的函數,可以把他們聲明爲內聯函數,內聯函數在 程序中直接把函數調用的地方替換成函數的實際代碼,這樣一來就會提高效率,但是需要佔用更多的內存。一般來說,內聯函數將會丟棄聲明部分,直接在聲明的部分進行函數的聲明和定義。在 定義內聯函數的時候,只需要在函數返回類型的前面加上inline這個關鍵字即可,但是,即使你聲明這是一個內聯函數,但是程序在組合這個函數的時候不一定就真的會按照內聯的特點來處理這個函數,如果這個函數有遞歸調用,或者代碼很長,那麼這些函數即使你聲明瞭爲內聯函數,他也不會把你當作內聯函數處理的,只會當作普通函數。


對於內聯函數來說,他和C語言中的宏定義是由相似之處的。但是兩者的不同主要體現在函數參數的傳遞上。

內聯函數,仍然是嚴格意義上的一個函數,按照函數的值傳遞規則進行傳遞函數參數。

但是宏定義卻不是這樣,在利用宏定義去定義個類似函數的功能時,例如

#define square(x) x * x 

利用宏定義定義一個計算一個數字平方根的函數,加入我有以下三種的調用方式

square(1+5)

square(c++)

這兩者在調用 的時候就會出現問題,第一個會出現運算順序不當造成的結果不正確,第二個會造成c變量在一次的函數調用中+1兩次破壞變量的結果。

爲什麼會出現這種情況,因爲宏定義是通過文本替換來實現的,並不是實際意義上的函數調用。

按理來說,第一個例子傳遞的應該是6,在程序的編譯階段這些常量表達式的值都應該會被算出來並且把相應的位置都替換成結果相同的常量。但是宏定義只是做了簡單的替換。總的來說就是,不夠智能。在定義一個類函數功能的時候,不能按照函數的一些規則去進行實施。


這個時候你用內聯函數就是最好不過了,兩者的行爲幾乎差不多,但是內聯函數畢竟是一個真正的函數,他會按照函數傳遞參數的規則算出傳遞函參數的表達式的值然後再進行傳遞。

所以在對一些函數功能的運用時,還是用內聯函數比較好。


二、引用變量

引用變量也是C++新特性中的一個新加入的複合類型。它有一個準確的定義就是已經定義的變量的別名。當把一個引用變量和一個變量進行綁定的時候,那麼使用本身的變量和使用引用變量是一樣的。

那麼引用變量最大的用處在哪裏呢,就是在函數參數的傳遞上面。

通過上面的簡單論述你可能會知道,引用變量可以使用和他綁定的那個實際的變量,並不是拷貝了一個變量的副本,而是使用這個變量的原本的數據,這就在函數參數的傳遞一些比較大的數組和結構上面有個更大的用處。引用變量的使用方式相對較爲簡單,至少比使用指針的出錯機率要小很多。

而且還能達到和指針一樣的效果。


1、引用的定義

先給一個例子

int a;

int &p = a;

p就是a這個變量的引用。這裏的&運算符在這裏定義引用的時候也算是一種運算符重載的情況。它不再是 取地址符,或者是按位與操作,而是像char *這樣的定義,int &是一個整體,表明了p是一個指向int型數據的一個引用變量。

他是這個變量的一個別名,兩者都指向同一塊內存空間。


引用和指向一個變量的指針還是有區別的。比如

int *ptr = &a;

那麼 ptr和&p是一回事,這裏的&是取地址的作用

並且在引用定義的時候,一個最重要的和指針的區別就是,引用在定義的時候必須進行初始化操作,但是指針不用。

引用更像是一個const的指針,必須在定義的是就初始化爲一個變量,這樣,這個指針或者引用將一直與初始化時候的變量進行綁定,不能夠再重新指向其他的變量。


引用的值的改變,只能在它定義的時候給他初始化。如果你在之後的程序中,通過一個變量的賦值企圖讓引用變量改變之前綁定的變量,那麼 效果只會適得其反。你會因爲這種行爲改變初始化給引用變量的那個變量的值。總而言之,引用變量,只能在初始化的時候指定它所要綁定的變量,不能通過之後的賦值改變其值。

再次重申一次,引用變量一旦被初始化,那麼就再也不能被改變了。


2、引用用作函數參數

很簡單的一個例子就是交換兩個變量的函數

int swap(int &a, int &b)

{

int  t;

t = a;

a = b;

b = t;

}

相信大家都已經知道,這個函數的內部雖然和按值傳遞a b 這個參數的swap函數沒什麼區別,但是按值 傳遞的話,這個函數體內的代碼是不能實現變量轉換的。int swap(int a, int b);如果函數是這樣的話,那麼a b是實參的副本,也就是說拷貝了一份實參,a b是兩個新的變量和調用時傳遞的實參沒有區別。但是前面的例子把變量初始化給引用變量,a b就相當於實參的別名,對ab的操作就是對調用者傳遞的實參進行操作,類似於指針但是和指針的運用方式不一樣。



3、關於傳遞引用參數以及臨時變量的創建

在當前的標準下,僅僅是被調用函數的參數是常量引用變量的時候,在函數傳遞的過程中才會創建臨時變量。

那麼當引用參數是const,又需要什麼進一步的條件才能真正確定的要創建臨時變量呢,有兩種情況:

(1)實參的類型正確,但是不是左值。

這裏面有必要給大家講一個概念就是左值:左值就是可以被引用的數據對象,比如變量,數組元素,結構成員等等,也就是有名的數據對象。現在,常規變量和const變量都可以作爲左值,但是一個是可修改的左值,另一個是不可修改的左值。左值就是可以通過地址來訪問的數據

(2)實參類型不正確,比如要傳遞給一個double的引用,但是實參確是一個int型變量。


上述的兩種情況有如下兩個例子

int temp(const int & t);一個函數原型是這樣的。

double x;

temp(x);

temp(2.5);

第一種調用是實參類型不正確,這個時候編譯器將創建一個臨時的無名變量,然後把x轉化爲int型變量,然後傳遞給引用參數。

第二種是是實參的類型正確,但是不是左值,也就是說傳遞的這個實參是沒有名字的。所以也將創建一個臨時變量。 臨時變量只在函數調用期間存在,一旦用完,即可隨意刪除。

爲什麼對於常量引用就是可行的,但是對於其他的情況就是不可行的呢。首先我們要明確,一個常量引用,它綁定了一個變量,但是我們不能通過這個引用去更改他所綁定的變量,如果這個引用是常規的引用,那就不會是這樣情況。

由於他不能夠用引用去更改變量,那麼 其實就和值傳遞是一樣的,

long a = 5;

long  b = 3;

void swap(int &x, int &y);

swap(a,b)

如果是像這樣執行的話,那麼a b會因爲實參類型不對而創建兩個臨時變量,然後把x y 兩個引用參數綁定到兩個臨時變量上,那麼之後進行變量轉化,轉換的也是臨時變量,和實參的a b是沒有任何關係的。所以在你想修改作爲參數傳遞的實參變量的時候,創建臨時變量會阻止這種事情發生的。唯一的辦法就是禁止創建臨時變量。現在C++的標準規定,只有函數參數有常量引用參數的時候才允許創建臨時變量,不然都會報錯或者警告。


綜上所述,在形參爲const引用的函數中,如果出現上述的兩個條件之一,他們的行爲都類似於值的傳遞,因爲臨時變量的關係他們不會更改原來的實參。


4、將引用用於結構

引用關於結構上的,一個是傳遞結構參數的時候傳遞給引用參數,另外一個就是函數的返回值。

在傳遞結構參數的過程時,有三種方式,傳遞指針,傳遞值,傳遞引用。傳遞值不但效率低而且不能夠改動原來的實參,傳遞指針則較爲複雜 ,傳遞引用是結合了兩者的優點。


函數的返回值,大多數都是和按值傳遞的函數一樣,return算出後面的表達式的值,然後將值賦值給一個臨時的變量,之後調用者去使用這個值。但是傳遞引用就省去了中間要把值傳遞給臨時變量的這個一個環節,直接賦值給接受這個函數的返回值的目的變量即可。

 

而且在返回引用的時候要注意,不要返回函數內部的局部變量的引用,因爲在函數運行結束後,相應的分配給這個函數的棧空間都會一次性的回收,很有可能會返回一個並不存在的變量的引用。

一種辦法是返回參數中的一個引用,因爲參數中的引用是和調用者傳遞的實參綁定的。


在函數的引用類型是string對象的時候,傳遞的實參可以是string對象,或者是char *的指針,字符串的字面量,以空字符結尾的char數組名。

一方面來說,string中定義了可以把C風格的數組轉化爲string的功能。另一方面,如果這時的引用參數是const string,那麼他會創建一個臨時的變量,把實參放進去,然後在綁定一個類型正確的引用。


5、關於類的繼承和引用的聯繫

類的繼承,自然有基類和派生類。在定義一個基類的函數的時候,派生類的對象也是可以調用它的。關於引用的話,假設基類的一個方法的參數是一個基類的對象的引用變量,但是我們在使用派生類對象調用這個方法並且傳遞的實參也是派生類的對象的話一樣可以調用。



三、默認參數

默認參數的定義很簡單,就是在調用一個函數的時候,沒有傳遞相應的實參而自動的使用的一個值。

在程序中 我們可以通過函數的原型來設置默認函數的值。

再給函數設置默認參數的時候唯一的一點就是要注意順序,它是從右向左的順序。

也就說,如果你設置可一個默認參數,那麼你的右邊將都會是默認參數,左邊是常規參數。這是因爲,在調用函數傳遞參數的過程中,必須依次把實參傳遞給對應的形參,如果你的默認參數在中間,程序是沒辦法識別你的意圖的,他只會把要傳遞給後面形參的實參提前傳遞。

定義默認參數一定要在原型中定義,函數的定義部分和之前是沒有區別的。


四、函數重載

默認函數能夠讓你使用不同的參數調用同一個函數,但是函數的重載可以使用不同的參數調用同一個函數。

函數重載的關鍵是函數的參數列表也成爲函數的特稱標。

C++允許定義同名函數,但是他們的參數列表必須是有區別的,這裏的區別是指類型,順序,不是參數的名字。

可以這麼理解,C++是靠函數的特徵標來識別不同的函數。

在重載了一個函數多次得到不同版本的時候,在調用函數的過程中會自動找到與其對應的特徵標的函數進行調用。

但是如果定義的這個函數並且重載了幾個版本之後,函數的某一次調用仍然找不到正確的特徵標進行調用時,C++將會使用強制類型轉換,轉換成一個已有函數的版本進行調用。

但是,在強制類型轉換的過程中也會出現問題。

例如我定義了一個int swap(int do)的函數,並且重載他int swap(char * p)

我在調用的時候使用

float x = 9.0

swap(x)

因爲傳遞的x的值和函數的形參的類型是不對應的,所以要進行強制類型轉換,因爲double不可能轉化爲指針,所以就會嘗試轉化爲int進行繼續調用。

但是如果再重載一次函數,int swap(double dd);

這樣的同時出現了兩種可能的轉化,那麼此時計算機就不知道該怎麼做了。

還有一個要注意的是,在函數重載中,編譯器把變量和其引用看作是同一個特徵標。

也就是說

int swap(int yt)

int swap(int &yt)

int x = 9;

這兩個在函數重載的過程中會出錯。swap(x)將會和上面兩個版本都匹配,所以編譯器不知道要調用哪個函數,這樣就會報錯。

並且在重載函數的函數列表中,不要把加不加const來作爲特徵標的決定性因素,函數的重載是不區分const和常規變量的。

但是在傳遞參數的時候要注意,不能把const的參數賦值給非const的參數,但是反過來是可以的。


函數的重載,區分不同的版本主要是靠參數列表。和函數的類型沒什麼關係,函數的類型可以相同也可以不同,但是如果你的函數名相同,特徵標相同,那麼編譯器就會認定這樣你重複定義了兩個相同的函數。所以要保證特徵標的不同才能做到函數重載。


五、函數模板

函數模板,簡單來說就是要定義一種通用的函數。這個函數中的所有的數據類型,都可以通過之後調用的時候傳遞你想要的數據類型進行計算,那麼這就很方便那些一個算法用於不同種數據類型的函數。

模板只是定義函數但是不創建函數,只是給了一個整體的框架,需要我們傳遞 給他以相應的類型參數才能夠真正的使用它。


如何定義函數模板呢:

temlate <typename anytype>

void swap(anytype &a , anytype &b)

{

anytype x;

x = a;

a = b;

b = x;

}

上述的代碼就定義了一個函數的模板。

需要交換兩個什麼樣的數據類型的數據的時候,anytype就會變成相應的那個數據類型,編譯器就會根據指定的類型根據模板創建這個函數。早期的標準使用class代替 typename.


在定義了函數的模板之後,我們不用管其他的事情,還是按照正常聲明定義使用這三個步驟進行調用函數。當你傳遞的數據類型明確的時候的,編譯器會自動爲你生成和你的數據類型相對應的版本的函數供你調用。在最後的可執行程序中是沒有模板存在的。我們也可以看到了,由模板生成一個確定的函數都是在函數的編譯階段下完成的。所以最後的結果還和我們手工定義的函數一樣,沒什麼區別,但是應用模板最好的好處就是代碼簡潔,可以減少我們的犯錯率。


1、重載模板

需要對多個不同類型使用同一個算法的時候,可以使用函數重載也可以使用模板。但是,如果不同的類型需要不同的算法呢。那麼我們就需要重載模板。重載模板和重載函數其實很相似,重載的重要標誌就是特徵標要不同。當然temlate <typename anytype>應該都是一樣的,所有的模板都是用這個開頭的。


2、模板的侷限性

由於模板函數的通用型,所以很可能一個模板處理不了一些類型。比如函數代碼是a = b * a。如果說是整數或者說是浮點數那麼還好,如果傳進來的ab是兩個字符串的指針呢,這就無法處理了。

一種比較直接的方法就是爲特定的類型指定特定的模板。


3、顯示具體化

 在使用模板代碼的時候,如果我們需要一個不同版本的代碼,但是同時我們又不能夠改變模板中的特徵標,那麼我們就無法通過重載模板來達到目的。所以就要提供一個具體函數的的定義,這種操作叫做顯示具體化。

其中包括我們所需要的代碼,當編譯器找到與函數調用相匹配的具體化定義的時候,就不再調用模板,直接使用該具體化定義。


對於一個給定的函數名來說,可以有常規函數,具體化函數,模板函數。如果上述三種函數同時存在的話,那麼在選擇的時候,優先級依次降低。也就是說,如果能找到對應的常規函數就不用管後兩個,如果在調用函數找到了和函數對應的數據類型也對應的具體化函數,那麼就調用這個,最好在決定調用模板函數。


具體化的函數定義如下

template<> void swap<job>(job&, job&)其中<job>可以省略,這個具體化函數是專門爲job數據類型準備的,如果具體化函數和模板函數同時存在的話,那麼job類型的數據只會調用具體化函數。


4、實例化和具體化

實例化:

函數模板不是函數的定義,只是給編譯器一個創建函數的方案。當我們在以平常使用函數的方式傳遞給函數數據類型的時候,得到的是一個模板的實例化的實體,也就是特定類型的一個函數,這個函數的是通過模板建立的。這種實例化的方式叫做隱式實例化。

那麼什麼是顯示實例化呢,就是template void swap<int>(int&, int&)

與顯示實例化不同的是,具體化使用這樣的聲明方式

template<> void swap<job>(job&, job&)其中<job>

主要的區別就是,具體化的聲明是不要用函數的模板來生成函數的定義,而是單獨的爲他生成一個函數。而顯示實例化呢,是通過模板創建一個特定類型的函數,以及template後面的<>。


另外一種創建顯示實例化的形式就是直接在程序中調用相應的函數,這樣即使與模板的規範不對並且不在之前聲明,編譯器也會正常的生成一個實例化的函數。比如

int x = 3;

double y = 0.9

swap模板顯然和swap(x,y)這樣的 調用是不對應的。所以直接在程序中使用swap<double>(x, y),編譯器會強制的生成一個double的實例化。並且 將x參數的類型轉化爲double型,以便於的個參數匹配。

這裏傳遞的僅僅是數字是沒有問題的,如果函數模板中的參數是引用呢。

還是swap<double>(x,y)的調用。雖然會生成一個double的顯示實例化swap<double>(double &a, double &b),但是之後再傳入參數的時候,整數類型的數據是不能夠綁定到double數據類型的引用上的。


顯示 具體化,顯示實例化,隱式實例化都是具體化。對於隱式實例化和顯示具體化,都是根據相應的調用來決定去使用哪一個,顯示具體化是我們手動要在程序運行之前定義好的。但是顯示實例化不是的,如果是顯示實例化,只需要一個聲明,那麼就會使用模板來定義生成這個相應的版本。而隱式實例化是在函數調用的時候根據傳遞的參數的數據類型由編譯器生成的一個函數。


5、關於decltype

decltype在聲明模板函數時,如果碰到不確定使用什麼數據類型將有很大的幫助

比如一個兩數相加賦值給第三個變量的模板函數,三個變量分別是a b c

c = a + b

那麼C的數據類型將是不確定的,因爲由於ab類型的不同,可能會有數據類型上的提升。所以這時候,利用decltype(a+b)c = a + b就可以成功解決問題。將c變爲a+b結果的數據類型。


但是如果碰見這樣的模板函數呢

?type? gt(t x2, t x1)

{

return x2 + x1;

}

如果你想把type處用decltype的話,那麼就錯了,因爲在函數返回類型處,還沒有定義x2 和 x1這兩個參數,所以不能用。

C++爲了處理這種情況,用了一種叫做後置返回類型的辦法。

auto gt(t x2, t x1)->decltype(x+y)

這樣的話先用auto佔位,之後用後面的decltype明確了類型之後來代替前面的auto.

這種辦法也可以用於函數定義

auto swap(T x, T y)->double

則函數的返回類型最後爲double。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章