首頁 > 軟體

C語言的模板與泛型程式設計你瞭解嗎

2022-03-31 16:00:28

模板與泛型程式設計淺談

摘要(Effective C++):

​ C++template的最初發展動機很直接:讓我們得以建立“型別安全”的容器如vector,list和map。然而當愈多人用上templates時,他們發現template有能力完成愈多可能的變化。容器當然很好,但泛型程式設計(generic programming)——寫出的程式碼和其所處理的物件型別彼此獨立——更好。STL演演算法如for_each,find和merge就是這一類程式設計的結果。最終人們發現,C++template機制自身是一部完整的圖靈機:它可以用來計算任何可計算的值。於是匯出了模板超程式設計(template mataprogramming),創造出“在C++編譯器內執行並於編譯完成時停止執行”的程式。

模板與泛型程式設計簡單介紹

​ 物件導向程式設計(OOP)和泛型程式設計都可以處理編寫程式時不知道型別的情況;二者的不同之處在於:OOP能處理型別在程式執行之前都未知的情況;而在泛型程式設計中,在編譯時就能獲知型別了

​ 我們所常用的STL標準庫中,每一個容器都提供了單一的,泛型的定義,例如我們所常用的vector,我們可以定義很多型別的vector

vector<int> vi; // vi是裝載int型別的vector容器的範例
vector<string> vs; // vs是裝載string型別的vector容器的範例
vector<double> vd; // vd是裝載double型別的vector容器的範例

模板是泛型程式設計的基礎,一個模板就是一個建立類或者函數的藍圖或者公式

函數模板

// 簡單的比較函數模板
template<typename T>
int cmp(const T& v1,const T& v2) {
    if(v1<v2)
        return -1;
    else if(v1>v2)
        return 1;
    else
        return 0;
}

函數定義以關鍵字template開始,後跟一個模板參數列,這是一個逗號分隔的一個或多個模板引數的列表,用尖括號包圍起來

**注:**在模板定義中,模板參數列不能為空

模板參數列表示在類或函數定義中用到的型別或者值。當我們使用模板的時候,我們可以(顯式或隱式地)指定模板實參,將其繫結到模板引數上

簡單瞭解模板的範例化過程

​ 眾所周知,當你覺得模板程式設計十分智慧的時候,一定是有東西在為你負重前行,C++提供了模板與泛型程式設計的這個能力,這便意味著有一個東西在為你動態地實現模板的功能,而這一定是比C++這個高階語言層面更為底層的東西,而我們所瞭解的知識中,比C++高階語言較為底層的東西,除了作業系統,便是編譯器了。

​ 當我們呼叫一個函數模板的時候,編譯器(通常)用函數實參來為我們推斷模板實參。簡單來講,便是我們在呼叫函數模板的時候,編譯器通過使用實參的型別來確定繫結到模板引數T的型別

cout<<cmp(1,0)<<endl; // T為int

在上訴程式碼中,函數cmp的實參型別是int,編譯器便會推斷出模板實參為int,並將它繫結到模板引數T上

簡單來說,編譯器用推斷出的模板引數來為我們範例化一個特定版本的函數

模板編譯

當編譯器遇到一個模板定義的時候,它並不會生成程式碼。只有我們範例化出模板的一個特定的版本時,編譯器才會生成其對應的程式碼。當我們使用(而不是定義)模板時,編譯器才會生成程式碼。這個特性影響我們如何組織程式碼以及錯誤何時才可以被檢測到

通常來說,我們將類定義和函數說明放在標頭檔案中,而普通函數和類的成員函數的定義放在原始檔中

模板則不盡相同:為了生成一個範例化的版本,編譯器需要掌握函數模板或類別範本成員函數的定義

總結與非模板程式碼不同,模板的標頭檔案通常既包括宣告也包括定義即函數模板和類別範本成員函數的定義通常放在標頭檔案中

大多數編譯錯誤出現的時機 

  • 第一階段,編譯模板本身時,該時期所出現的錯誤大多數為語法錯誤
  • 第二階段,編譯器遇到模板使用時
  • 第三階段,模板範例化時,而只有在這個階段才能發現型別相關的問題

**注意事項:**保證傳遞給模板的實參支援模板所要求的操作,以及這些操作在模板中能正確的工作,是呼叫者的責任

類別範本

​ 類別範本是用來生成類的藍圖的。與函數模板不同之處是,編譯器不能為類別範本推斷模板引數型別。 所以我們必須在模板名後的尖括號中提供額外的資訊——用來替代模板引數的模板實參列表

vector<int> vi;
deque<double> dd;
pair<string,int> key_val;

定義類別範本

template<typename T>
class T_vector {
public:
	typedef T value_type;
    // 建構函式
    T_vector() =default;
    T_vectot(std::initializer_list<T> il);
    // 容器的元素數目
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // 新增元素
    void push_back(const T& val) { 
        data->push_back(val);
    }
    void push_back(T &&val) {
        data->push_back(std::move(val));
    }
private:
    std::shared_ptr<std::vector<T> > data;
    // 若data[i]無效,則丟擲msg
    void check(size_type i,const std::string &msg) const;
}

類似函數模板,類別範本以關鍵字template開始,後跟模板參數列。在類別範本(及其成員)的定義中,我們將模板引數當作替身,代替使用模板時使用者需要提供的型別或值

**注:**一個類別範本的每一個範例都形成一個獨立的類,而類別範本的每個範例都有其自己版本的成員函數

​ 所以,我們可能會出現一個單一模板並不能滿足所有型別的需求,而模板特例化就出現了

類別範本成員函數的範例化

​ 預設的情況下,一個類別範本的成員函數只有在程式用到它的時候才會範例化

// 範例化T_vector和接受initializer_list<int>的建構函式
T_vector<int> T_vi = { 0,1,2,3,4,5 };

如果一個成員函數沒有被使用,則它將不會被範例化

為什麼我們需要模板特例化?

當我們編寫單一的模板時,使其對任何可能的模板實參都是最合適的,都能範例化,但者往往都是過於理想化的情況。在某些特殊的情況下,通用的模板的定義可能對特定的型別是不合適的,通用定義的模板可能會出現編譯失敗或者做得不夠完善的情況。

​ 故,當我們不能(或者不希望)使用模板版本的時候,可以定義類或函數模板的一個特例化版本

定義函數模板特例化

// 原先cmp函數的特殊版本,用來處理特殊的字元陣列的指標template<>int cmp(const char* const& p1,const char* const& p2) {    return strcmp(p1,p2);}// 原先cmp函數的特殊版本,用來處理特殊的字元陣列的指標
template<>
int cmp(const char* const& p1,const char* const& p2) {
    return strcmp(p1,p2);
}

函數過載與模板特例化的區別

​ 當定義函數模板的特例化版本時,我們本質上接管了編譯器的工作。即,我們為原先的模板的其中一個特殊的範例提供了定義。簡而言之,特例化的本質是範例化一個模板,而非過載它,因此特例化並不影響函數匹配

注意事項:

  • 為了特例化一個模板,原模版的宣告必須在作用域中
  • 在任何使用模板範例的程式碼之前,特例化版本的宣告也必須在作用域中
  • 所有同名模板的宣告應該放在前面,然後是這些模板的特例化版本

類別範本部分特例化

與函數模板不同的是,類別範本的特例化不必為所有模板引數提供實參。一個類別範本的部分特例化本身是一個模板,使用它時使用者還必須為那些在特例化版本中指定的模板引數提供實參

我們只能部分特例化類別範本,而不能部分特例化函數模板

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!


IT145.com E-mail:sddin#qq.com