天天看點

cplus6_第8章_函數模闆(下)

C++擴充了C語言的函數功能。通過将inline關鍵字用于函數定義,并在首次調用該函數前提供其函數定義,可以使得C++編譯器将該函數視為内聯函數。也就是說,編譯器不是讓程式跳到獨立的代碼段以執行函數,而是用相應的代碼替換函數調用。隻有在函數很短時才能采用内聯方式。

引用變量是一種僞裝指針,它允許為變量建立别名。引用變量主要被用作處理結構和類對象的函數的參數。通常,被聲明為特定類型引用的辨別符隻能指向這種類型的資料;然而,如果一個類是從另一個類派生來的,則基類引用可以指向派生類對象。

C++原型讓您能夠定義參數的預設值。如果函數調用省略了相應的參數,則程式将使用預設值;如果函數調用提供了參數值,則程式将使用這個值。隻能在參數清單中從右到左提供預設參數。是以,如果為某個參數提供了預設值,則必須為該參數右邊所有的參數提供預設值。

函數的特征是其參數清單。程式員可以定義兩個同名函數,隻要其特征不同。這被稱為函數多态或函數重載。通常,通過重載函數來為不同的資料類型提供相同的服務。

函數模闆自動完成重載函數的過程。隻需要使用泛型和具體算法來定義函數,編譯器将為程式中使用的特定參數類型生成正确的函數定義。

1. 關于C++内聯函數

内聯函數的編譯與其它正常函數不同,編譯器會使用函數代碼替換函數的調用,使得程式不需要來回跳轉,省去了程式跳轉的開銷,但付出的代價是記憶體的開銷,特别是内聯函數如果體量較大,且使用較多時。是以内聯一般用于函數很短,且調用頻繁時。

使用方法是:将整個定義放在提供函數原型的地方(頭檔案或源檔案開始),并在函數前加上關鍵字​

​inline​

​。例如:

#include <iostream>

inline double square(double x) {return x * x;}  //内聯函數

int main()
{
  ...
}      

Inline是C++新增的特性,是從C的宏定義#define發展來的,但宏定義僅是簡單的文本替換,内聯卻擁有函數的一切特性,例如類型轉換、參數的按值傳遞。例如,同樣以傳回平方功能為例:

#define
a = SQUARE(5.0); //OK
b = SQUARE(2.1 + 3.4);  //false: b = 2.1 + 3.4 * 2.1 + 3.4
c = SQUARE(c++);    //false: c = c++ * c++;      

2. 關于引用變量

C++新增了一種複合類型——引用變量,引用是已定義變量的别名。引用變量的主要用途在于作為函數的形參。通過将引用變量用作參數,函數将使用原始資料,而不是其副本。這樣除指針外,引用也為處理大型結構提供了一種友善途徑。

  • 引用的聲明方法為:int & 變量名 = 某個變量;(必須在聲明時給引用變量指派,且此後該引用将不能再作為其它變量的引用了,他一生将忠于初心)
int rat = 100;
int & rat2 = rat; //rat2是rat的引用,至此之後它們二者指向相同的位址,可以互相交換。
int * const pr = &rat;  //引用rat2扮演的其實就是*pr的角色。      
  • 引用的原理如下:
cplus6_第8章_函數模闆(下)
  • 以交換兩個參數的值為例來解釋引用的具體用法及其與傳值的差別:
#include <iostream>
void swapr(int & a, int & b);
void swapv(int a, int b);

int main()
{
    using namespace std;
    int wallet1 = 100;
    int wallet2 = 200;
    
    swapr(wallet1, wallet2);    //函數調用使用實參初始化形參,即函數的引用參數被初始化為傳遞來的實參。
    cout << "wallet1 = $" << wallet1;
    cout << "wallet2 = $" << wallet2 << endl;
    
    swapv(wallet1, wallet2);
    cout << "wallet1 = $" << wallet1;
    cout << " wallet2 = $" << wallet2 << endl;
}

void swapr(int & a, int & b)    //
{
    int temp;
    temp = a;   //a其實就是wallet1的别名,b就是wallet2的别名。
    a = b;
    b = temp;
}

void swapr(int a, int b)
{
    int temp;
    temp = a;   //a其實就是與wallet1有相同值的另一個副本,a的值得改變不影響wallet1。
    a = b;
    b = temp;
}

--------------------程式輸出結果----------------------------------
wallet1 = $200 wallet2 = $100   //交換成功
wallet1 = $200 wallet2 = $100   //交換失敗      
  • 當函數參數為結構或類等比較大的資料的時候,引用将非常有用。
  • 如果引用參數是const,當函數調用的參數不是左值(常量或某個表達式)或與相應的const引用參數類型不比對,則C++将建立類型正确的匿名變量,将函數調用的參數的值傳遞給該匿名變量,并讓參數來引用該變量。如果引用參數不是const,那麼這些情況會導緻編譯錯誤。
  • C++新增了另一種右值引用。它使用&&聲明。它的主要目的是讓庫設計人員能夠提供有些操作的更有效實作。
double && rref = std::sqrt(36.00);
double i = 15.0;
double && jref = 2.0 * j + 18.2;      

2.1 關于引用結構體

  • 使用結構引用參數的方式與使用基本變量引用相同,隻需要在聲明結構參數時使用引用運算符&即可。
struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float percent;
};

void set_pc(free_throws & ft);  //為結構體使用引用。

void set_pc(const free_throws & ft);    //如果不想改變結構體内容,可以這樣聲明      
  • 函數的傳回類型也可以設定為引用。一般來說return語句将後面的值先暫存到一個臨時記憶體,然後再傳回到調用函數,但如果函數傳回的是結構體,則需要比較大的記憶體來存儲它。是以,通過定義被調用函數的傳回類型為引用的辦法,來避免建立、複制大的結構體内容到一個臨時變量,而是将原來的結構體内容直接傳回。
  • 需要注意的一點是要避免傳回的引用本體為局部變量,因為它們在函數傳回後消逝。是以要麼傳回引用指向的是作為參數傳遞給函數的引用。要麼是用new配置設定的新的存儲空間,但這又涉及到如何釋放的問題。

2.2 關于引用類對象

  • 當使用類型為const的引用的形參時,如果實參的類型與形參不比對,但可以被轉換為引用類型,程式将建立一個引用類型的臨時變量并使用轉換後的實參值來初始化它,然後傳遞一個指向該臨時變量的引用。是以,當形參為​

    ​const string &​

    ​時,實參可以是字元串常量、char數組或指向char的指針。
  • 任何情況下函數都不能傳回一個指向局部變量的引用,因為局部變量在函數調用結束後就被釋放了。
//錯誤示例:
const string & func(string & s1, const string & s2)
{
    string temp;
    temp = s2 + s1 + s1;
    return temp;    //因為函數傳回類型為引用,是以當函數調用結束,其引用的主體也被釋放了
}      
  • 因為ifstream和ofstream分别是istream和ostream類的繼承類,而繼承類有着基類所有屬性和方法,是以在引用基類作為函數參數的時候,可以傳遞基類的繼承類對象作為實參。例如以下函數原型:
void file_it (ostream & os);      

既可以将cout傳遞給他,也可将一個ofstream對象(如fout)傳遞給他。

2.3 何時使用引用參數、按值傳遞、使用指針?

對于使用傳遞的值而不作修改的函數:

  • 如果資料對象很大,則使用const指針或const引用。因為節省了複制整個資料對象所需的時間和空間,進而提高程式運作速度。
  • 如果資料對象很小(基本資料類型或小型結構),建議使用按值傳遞;
  • 如果資料對象是數組,則使用指針,并将指針聲明為指向const的指針。
  • 如果資料對象是類對象(string等),則使用const引用。因為傳遞類對象參數的标準方式就是按引用傳遞。

對于修改調用函數中資料的函數:

  • 如果資料對象是内置資料類型,則使用指針。
  • 如果資料對象是數組,則隻能使用指針;
  • 如果資料對象是結構,則使用指針或引用。
  • 如果資料對象是類對象,則使用引用。

3. 關于預設參數

  • 預設參數指的是當函數調用中省略了實參時自動使用的一個值。通過使用預設參數,在設計類時可以減少要定義的析構函數、方法及方法重載的數量。
  • 預設參數的設定必須通過函數原型,因為編譯器是通過檢視函數原型了解函數使用的參數數目及類型的。例如,left()的原型如下:

​char * left(const char * str, int n = 1);​

​上面的原型将n初始化為1,如果省略參數n,則它的值預設為1,否則為傳遞給它的值.

  • 對于帶參數清單的函數,必須從右向左添加預設值。即若要為某參數設定預設值,則必須為其右邊的所有參數設定預設值。

4. 關于函數重載

C++允許定義名稱相同的函數,條件是它們的參數清單各異。編譯器将根據調用函數采取的不同被調用函數的參數清單來選擇特定的多态函數。如果使用的參數清單類型不符合多态函數(被調用函數)任一類型的話,編譯器将對個别參數嘗試進行強制類型轉換(但如果有多種轉換方式使之滿足多态函數的多個原型的話,編譯器将拒絕調用任何一個并報錯)。

僅當函數基本上執行相同的任務,但使用不同形式的資料時,才應該使用函數重載。

注意:
  • 由于引用的特殊性,變量及其引用被視為同一種參數類型。因為變量引用其實就是變量自己。
  • 當參數使用const修飾時,可以不區分const和非const變量。因為将非const變量賦給const變量是合法的,但反之則非法。
  • 如果函數參數清單相同,即使函數傳回類型不同,也不構成重載。即函數是否重載決定于參數清單(特征标)。

編譯器通過對不同特征标的重載函數名進行名稱修飾(name decoration)進行差別。這種對參數數目和類型進行編碼後用于對名稱修飾的方法随着編譯器的不同而各異。

5.關于函數模闆

如果需要多個将同一種算法應用于不同類型的函數時可以使用函數模闆。如果不考慮向後相容的問題,請使用typename(typename在C++98之後才出現的,之前使用class)。

template <typename AnyType>   //此處的AnyType可換為你自己喜歡的任何名稱
void Swap (Anytype &a, Anytype &b)
{
    Anytype temp;
    temp = a;
    a = b;
    b = temp;
}      
  • 上述模闆并不建立任何函數,而隻是告訴編譯器如何定義函數,需要交換兩個int數時,編譯器将按照模闆模式建立這樣的函數,并用int代替Anytype。同理,需要交換兩個double數時,編譯器将按照模闆模式建立這樣的函數,并用double代替Anytype。
  • 一般将模闆定義放在頭檔案中,并在需要使用模闆的檔案中包含頭檔案。
  • 隻需要在程式中直接調用帶參數的模闆函數,編譯器會自動檢查參數類型,并生成相應的函數。
  • 函數模闆并不能縮短可執行程式,最終的代碼不包含任何模闆,而隻包含了為程式生成的實際函數。

5.1 模闆函數的重載

需要對多個不同類型變量使用同一種算法時,可使用函數模闆.但并非所有的類型都使用相同的算法,為解決該問題,可以對模闆函數使用重載。

template <typename T> //早期版本使用class代替typename
void Swap (T &a, T &b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;   
}

template <typename T>
void Swap (T a[], T b[], int n)
{
    T temp;
    for (int i = 0; i<n; i++)
    {
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}      

編譯器若發現Swap()有兩個參數時,使用第一個模闆,若發現有兩個數組加一個整數作為參數的Swap()調用時,将調用第二個模闆。

5.2 模闆函數重載的局限性

由于編寫的模闆可能無法處理某些特定類型,例如數組、結構體等。此時就需要提供具體化函數定義——顯示具體化:

5.2.1 ISO/ANSI C++标準的“顯示具體化函數模闆”

  • 對于函數,可以有非模闆函數、模闆函數、顯示具體化模闆函數以及它們的重載版本。
  • 顯示具體化的原型和定義應以​

    ​template <>​

    ​打頭,并通過名稱來指出類型。
  • 具體化優先于正常模闆,而非模闆函數優先于具體化和正常模闆。
  • 下面以交換job結構函數為例,其非模闆函數、模闆函數、顯示具體化模闆函數的原型如下:
struct job
{
    char name[40];
    double salary;
    int floor;
}

//非模闆函數原型
void Swap(job &, job &);

//模闆函數原型
template <typename T> //早期版本使用class代替typename
void Swap(T &, T &);

//顯示具體化模闆函數原型
template <> void Swap<job>(job &, job &);   //<job>也可以省略,因為其參數清單已指明了參數類型為job      

5.2.2 顯示執行個體化函數模闆

在定義了函數模闆後,編譯器隻在代碼調用了帶參數的模闆函數後才隐式執行個體化模闆函數,除此之外,C++還允許顯示的執行個體化模闆函數,其文法是,聲明所需的種類——用​

​<>​

​​符号訓示類型,并在聲明前加上關鍵字​

​template​

​。

template void Swap<int>(int, int);  //執行個體化一個兩個int參數的Swap函數      
在同一檔案中使用同一種類型的顯示執行個體和顯示具體化将出錯。

5.2.3 具體化

隐式執行個體化、顯示執行個體化、顯示具體化統稱為具體化(specialization),它們相同之處在于表示的都是使用具體類型的函數定義,而非通用描述。在聲明中使用字首​

​template​

​​和​

​template <>​

​分别區分顯示執行個體化和顯示具體化。

template <class T>
void Swap (T &, T &);   //模闆原型

template <> void Swap<job>(job &, job &);   //顯示具體化(job類型)
int main(void)
{
    template void Swap<char>(char &, char &); //顯示執行個體化(char類型)
    short a,b;
    ...
    Swap(a, b); //隐式執行個體化(short類型)
    job m, n;
    ...
    Swap(m, n); //顯示具體化(job類型)
    char g,h;
    ..
    Swap(g, h); //顯示執行個體化(char類型)
    ...
}      

5.3 編譯器如何選擇使用哪個函數版本

對于函數重載、函數模闆、函數模闆重載,C++需要一個良好的政策來決定為函數調用哪個版本的函數定義,尤其是有多個參數時。此過程也稱為重載解析。

  1. 建立候選函數清單。包含與被調用函數名稱相同的函數和模闆函數。
  2. 使用候選函數清單建立可行函數清單。這些都是參數數目正确的函數,為此有一個隐式轉換序列,其中包括實參類型與形參類型完全比對的情況。例如float參數的函數調用可以将該參數轉換為double,進而與double形參比對,而模闆可以為float生成一個執行個體。
  3. 确定是否有最佳的可行函數。若有則用,否則報錯。
參數轉換以達到比對時的優先級為:
  1. 完全比對,但正常函數優先于模闆函數;
  2. 提升轉換(例如:char和short自動轉換為int,float自動轉換為double)
  3. 标準轉換(例如:int轉換為char,long轉換為double)。
  4. 使用者定義的轉換(類聲明中定義的轉換)
  • 以下情況均屬于完全比對
從實參 到形參
Type Type &
Type & Type
Type [] * Type
Type (參數清單) Type (*)(參數清單)
Type const Type
Type volatile Type
Type * const Type
Type * volatile Type *

注意:在都屬于完全比對的情況下,有如下優先級

1. 即使兩個函數都完全比對,但指向非const的指針或引用仍然優先于指向const的指針或引用。但const與否的差別僅限于指針和引用,對于其它變量是沒差別的。例如:

int a = 10;
...
recycle(a);
//在這種情況下,下面的原型都是完全比對的
void recycle(int);  //#1
void recycle(const int);  //#2
void recycle(int &);  //#3
void recycle(const int &);  //#4      

以上代碼,如果隻定義了#3和#4,則将選擇#3;如果隻定義了#1和#2,那就會因為二義性(ambiguous)而報錯。

  1. 非模闆函數優先于模闆函數(包括顯示具體化);
  2. 如果都是模闆函數,則較具體的模闆函數優先,即顯示具體化優先于隐式執行個體化。
  3. 最具體優先。例如
template <class Type> void recycle (Type t);  //#1
template <class Type> void recycle (Type * t);  //#2
...
int a = 10;
recycle(&a);  //此時編譯器選擇#2,因為它比起#1更為具體,雖然#1也是完全比對的。      
  1. 使用者自己選擇。通過顯示具體化來調用模闆函數
#include <iostream>

template <class T>
T lesser(T a, T b)  //#1
{
    return a<b ? a : b;
}

int lesser(int a, int b)    //#2
{
    a = a<0 ? -a : a;
    b = b<0 ? -b : b;
    return a<b ? a : b;
}

int main()
{
    using namespace std;
    int m = 20;
    int n = -30;
    double x = 15.5;
    double y = 25.9;
    
    cout << lesser(m, n) << endl;       //use #2
    cout << lesser(x, y) << endl;       //use #1 with double
    cout << lesser<>(m, n) << endl;       //use #1 with int
    cout << lesser<int>(x, y) << endl;    //use #1 with int
    return 0;
}      

程式輸出:

20

15.5

-30

15

5.4 關鍵字decltype

先考慮如下模闆函數定義:

template <class T1, class T2>
void ft(T1 x, T2 y)
{
    ...
    ?type? xpy =x + y;  //xpy應該是什麼類型?
    ...
}      

在上述代碼中,xpy有可能是T1類型,也有可能是T2類型。但我們該如何表示呢?為此,先來看下關鍵字——​

​decltype​

​。其文法如下(聲明一個變量var,其類型為expression表達式的類型):

decltype(expression) var;      
  1. 當expression為單個變量時(不是用括号括起來的表達式),var的類型與該變量相同,包括const等限定符。
  2. 當expression為函數調用時(并不會實際調用函數,而是檢視函數原型來獲知函數傳回類型),則var的類型與函數傳回類型相同;
  3. 當expression為一個左值(為了和第一步區分開來,expression必須是用括号括起來的變量)時,則var為指向其類型的引用。
double xx = 4.4;
decltype((xx)) r1 = xx; //r1 is double &
decltype(xx) r2 = xx; //r2 is double      
  1. 若前面的條件都不滿足,則var的類型與expression類型相同。
int i = 3;
int &k = i;
int &n = i;
decltype(100L) i1;  //i1 type long
decltype(k+n) i2; //i2 type int
decltype(k) i3;   //i3 type int &      
  • 如果需要多處聲明,可以結合typedef和decltype。開始的函數模闆可以重新寫為:
template <class T1, class T2>
void ft(T1 x, T2 y)
{
    ...
    typedef decltype(x+y) xytype;
    xytype xpy =x + y;
    xytype arr[10];
    xytype & rxy = arr[2];
    ...
}      

5.5 利用關鍵字auto,使用後置傳回類型

先看一個問題:

template <class T1, class T2>
?type? ft(T1 x, T2 y)   //函數傳回類型是什麼?
{
    ...
    return x + y;
}      

貌似可以将函數傳回類型定為decltype(x+y),但是函數還未開始運作時還未聲明參數x和y,它們不在作用域内。必須在聲明參數後使用decltype。

此時可以先用關鍵字​

​auto​

​​占個位,然後使用​

​->​

​來定義一個後置傳回類型。是以以上代碼可以寫為:

template <class T1, class T2>
auto ft(T1 x, T2 y) -> decltype(x+y)
{
    ...
    return x + y;
}      

繼續閱讀