日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

c++ 模板教程(c语言中文网) 自己运行实例

發布時間:2024/4/18 c/c++ 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c++ 模板教程(c语言中文网) 自己运行实例 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

注:本文的例子全部都是c語言中文網上的c++教程,自己只不過是運行了一遍而已。個別地方添加了點東西。

17.1模板函數教程

1.自己的第一個運行的程序(c語言中文網上源代碼)

// ConsoleApplication1.cpp : 定義控制臺應用程序的入口點。 //#include "stdafx.h" #include <string> #include <iostream> using namespace std; template<typename T> void swap(T * p1, T *p2) {T temp = *p1;*p1 = *p2;*p2 = temp; }int main() {int n1 = 100, n2 = 29;swap(&n1, &n2);cout << n1 <<" "<< n2 << endl;char c1 = 'm', c2 = 'n';swap(c1,c2);cout << c1 << " " << c2 << endl;double d1 = 1.23, d2 = 2.34;swap(d1,d2);cout << d1 << " " << d2 << endl;return 0; }

運行結果:?

下面我們來總結一下定義模板函數的語法:

template <typename 類型參數1 , typename 類型參數2 , ...>?返回值類型 ?函數名(形參列表){
? ? //在函數體中可以使用類型參數
}

? ? ? ? 類型參數可以有多個,它們之間以逗號,分隔。類型參數列表以< >包圍,形式參數列表以( )包圍。

??typename關鍵字也可以使用class關鍵字替代,它們沒有任何區別。C++ 早期對模板的支持并不嚴謹,沒有引入新的關鍵字,而是用 class 來指明類型參數,但是 class 關鍵字本來已經用在類的定義中了,這樣做顯得不太友好,所以后來 C++ 又引入了一個新的關鍵字 typename,專門用來定義類型參數。不過至今仍然有很多代碼在使用 class 關鍵字,包括 C++ 標準庫、一些開源程序等。

為了加深模板的理解,下面再看一個例子:

// 求三個數的最大的.cpp : 定義控制臺應用程序的入口點。 #include "stdafx.h" #include <string> #include <iostream> #include <memory> using namespace std;//模板的聲明和普通函數聲明也是類似的(我自己理解是:模板函數體去掉,剩下的就是聲明了。) template <typename T> T max(T &, T &, T &);int _tmain(int argc, _TCHAR* argv[]) {int a = 12, b = 189, c = 2002;auto re = max(a,b,c);cout << re << endl;return 0; }template <typename T> T max(T &a, T &b, T &c) {T temp;temp = (a > b ? a : b);temp = (temp > c ? temp : c);return temp; } 函數模板也可以提前聲明,不過聲明時需要帶上模板頭,并且模板頭和函數定義(聲明)是一個不可分割的整體,它們可以換行,但中間不能有分號。

7.2 類模板教程

C++?除了支持函數模板,還支持類模板(Class Template)。函數模板中定義的類型參數可以用在函數聲明和函數定義中,類模板中定義的類型參數可以用在類聲明和類實現中。類模板的目的同樣是將數據的類型參數化。

聲明類模板的語法為:

template<typename 類型參數1 , typename 類型參數2 , …> class 類名{
? ? //TODO:
};

? ? ? ?類模板和函數模板都是以 template 開頭(當然也可以使用 class,目前來講它們沒有任何區別),后跟類型參數;類型參數不能為空,多個類型參數用逗號隔開。

一但聲明了類模板,就可以將類型參數用于類的成員函數和成員變量了。換句話說,原來使用 int、float、char 等內置類型的地方,都可以用類型參數來代替。

假如我們現在要定義一個類來表示坐標,要求坐標的數據類型可以是整數、小數和字符串,例如:

  • x = 10、y = 10
  • x = 12.88、y = 129.65
  • x = "東經180度"、y = "北緯210度"

這個時候就可以使用類模板,請看下面的代碼:

// 第一個例子.cpp : 定義控制臺應用程序的入口點。 //#include "stdafx.h" #include <string> #include <iostream> using namespace std;template <typename T1,typename T2> class Point { public:Point(const T1 & t1, const T2 &t2) :m_x(t1), m_y(t2){} public:T1 getX()const; //獲取x坐標void setX(T1 x); //設置x坐標T2 getY()const; //獲取y坐標void setY(T2 y);//設置y坐標private:T1 m_x;T2 m_y; };int _tmain(int argc, _TCHAR* argv[]) { Point<int, int> p1(10,20);Point<int, float> p2(10,12.5);Point<float, char*> p3(12.5,"東經180度");Point<int,int>* p4 = new Point<int, int>(10, 20);Point<int,double>* p5 = new Point<int, double>(10,15.123);cout << p1.getX() << endl;cout << p1.getY() << endl;cout << p2.getX() << endl;cout << p2.getY() << endl;cout << p3.getX() << endl;cout << p3.getY() << endl;cout << p4->getX() << endl;cout << p4->getY() << endl;cout << p5->getX() << endl;cout << p5->getY() << endl;return 0; }

x 坐標和 y 坐標的數據類型不確定,借助類模板可以將數據類型參數化,這樣就不必定義多個類了。

注意:模板頭和類頭是一個整體,可以換行,但是中間不能有分號。

上面的代碼僅僅是類的聲明,我們還需要在類外定義成員函數。在類外定義成員函數時仍然需要帶上模板頭,格式為:

template<typename 類型參數1 , typename 類型參數2 , …>
返回值類型 類名<類型參數1 , 類型參數2, ...>::函數名(形參列表){
? ? //TODO:
}

第一行是模板頭,第二行是函數頭,它們可以合并到一行,不過為了讓代碼格式更加清晰,一般是將它們分成兩行。

下面就對 Point 類的成員函數進行定義:

template<typename T1,typename T2> T1 Point<T1, T2>::getX()const {return m_x; } template<typename T1,typename T2> void Point<T1, T2>::setX(T1 x) {m_x = x; } template<typename T1,typename T2> T2 Point<T1,T2>::getY()const {return m_y; }template<typename T1,typename T2> void Point<T1,T2>::setY(T2 y) {return m_y; }

使用類模板創建對象

上面的兩段代碼完成了類的定義,接下來就可以使用該類創建對象了。使用類模板創建對象時,需要指明具體的數據類型。請看下面的代碼:

  • Point<int, int> p1(10, 20);
  • Point<int, float> p2(10, 15.5);
  • Point<float, char*> p3(12.4, "東經180度");
  • 與函數模板不同的是

    類模板在實例化時必須顯式地指明數據類型,編譯器不能根據給定的數據推演出數據類型。

    注意:類外定義的成員函數首先要加模板頭,接著要在類名后面的尖括號內添加具體的參數類型。

    所以類內定義的成員函數和類外定義的成員函數是有區別的。


    除了對象變量,我們也可以使用對象指針的方式來實例化:

  • Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
  • Point<char*, char*> *p = new Point<char*, char*>("東經180度", "北緯210度");
  • 需要注意的是,賦值號兩邊都要指明具體的數據類型,且要保持一致。下面的寫法是錯誤的:

  • //賦值號兩邊的數據類型不一致
  • Point<float, float> *p = new Point<float, int>(10.6, 109);
  • //賦值號右邊沒有指明數據類型
  • Point<float, float> *p = new Point(10.6, 109);
  • 7.3? C++函數模板的重載

    ????????注意標準庫也有一個函數叫swap,所以有時候調用會和標準庫的swap沖突,這時候就需要加作用域運算符。

    #include <string> #include <iostream> #include <memory> using namespace std;template <typename T,unsigned LEN> void swap(T a[LEN],T b[LEN]) { for (int i = 0; i !=LEN; ++ i){T temp = a[i];a[i] = b[i];b[i] = temp; } }template<typename T> void swap(T a[],T b[],int len) { for(int i = 0;i != len; ++i){T temp = a[i];a[i] = b[i];b[i] = temp; }}template <typename T> void swap(T & a ,T & b) {T temp = a;a = b;b = temp;cout << "call void swap(T &,T &)." << endl; }int main() { int a[6] = {1,2,3,4,5,6}; int b[] = {11,22,33,44,55,66}; swap(a,b); for(int i = 0;i != sizeof(a) / sizeof(a[0]); ++ i)cout << a[i] << endl;int x = 1.1,y = 1234.2; ::swap(x,y); //這里加::,為了調用全局作用域定義的swap,加了全局作用域運算符:: cout << x << y << endl; int l = sizeof(a) / sizeof(a[0]); swap(a,b,l); for(int i = 0;i != l ;++ i){cout << a[i] << endl;}return 0; }

    在第二個函數模板中,最后一個參數的類型為具體類型(int),而不是泛型。并不是所有的模板參數都必須被泛型化。

    7.6 C++模板的顯式具體化

    ????????C++ 沒有辦法限制類型參數的范圍,我們可以使用任意一種類型來實例化模板。但是模板中的語句(函數體或者類體)不一定就能適應所有的類型,可能會有個別的類型沒有意義,或者會導致語法錯誤。

    例如有下面的函數模板,它用來獲取兩個變量中較大的一個:

    template<class T> const T& Max(const T& a, const T& b){return a > b ? a : b; }

    請讀者注意a > b這條語句,>能夠用來比較?int、float、char 等基本類型數據的大小,但是卻不能用來比較結構體變量、對象以及數組的大小,因為我們并沒有針對結構體、類和數組重載>。

    另外,該函數模板雖然可以用于指針,但比較的是地址大小,而不是指針指向的數據,所以也沒有現實的意義。

    除了>,+-*/==<等運算符也只能用于基本類型,不能用于結構體、類、數組等復雜類型。總之,編寫的函數模板很可能無法處理某些類型,我們必須對這些類型進行單獨處理。

    模板是一種泛型技術,它能接受的類型是寬泛的、沒有限制的,并且對這些類型使用的算法都是一樣的(函數體或類體一樣)。但是現在我們希望改變這種“游戲規則”,讓模板能夠針對某種具體的類型使用不同的算法(函數體或類體不同),這在 C++ 中是可以做到的,這種技術稱為模板的顯示具體化(Explicit Specialization)

    函數模板和類模板都可以顯示具體化,下面我們先講解函數模板的顯示具體化,再講解類模板的顯示具體化。

    函數模板的顯式具體化

    定義:讓模板能夠針對某種具體的類型使用不同的算法(函數體或類體不同),這在 C++ 中是可以做到的,這種技術稱為模板的顯示具體化(Explicit Specialization)

    ????????在講解函數模板的顯示具體化語法之前,我們先來看一個顯示具體化的例子:

    #include <string> #include <iostream> using namespace std;typedef struct{string name;int age;float score; }STU;//function template template<class T> const T& Max(const T& a,const T & b); //function explicit specialization:具體方法是把模板參數列表中的參數清空,然后后面的 //1.函數用顯式具體化,也就是函數名稱后面加<>里面放具體的類型, //2.再者就是函數參數列表中也用具體的類型 template<> const STU& Max<STU>(const STU& a,const STU &b); //overload ostream & operator<<(ostream &,const STU &);int main() { STU s1 = {"liming",12,92.2},s2 = {"wanglin",16,100}; cout << Max(s1,s2) << endl;return 0; }template<class T> const T & Max(const T & a,const T & b) {return a > b ? a : b; } template<> const STU& Max<STU>(const STU & a,const STU & b) {return a.score > b.score ? a : b; } ostream & operator<< (ostream &out,const STU & stu) { out << stu.name << " , " << stu.age << " , " << stu.score;return out; }

    運行結果:

    wanglin , 16 , 100

    ????????本例中,STU 結構體用來表示一名學生(Student),它有三個成員,分別是姓名(name)、年齡(age)、成績(score);Max() 函數用來獲取兩份數據中較大的一份。
    ????????要想獲取兩份數據中較大的一份,必然會涉及到對兩份數據的比較。對于 int、float、char 等基本類型的數據,直接比較它們本身的值即可,而對于 STU 類型的數據,直接比較它們本身的值不但會有語法錯誤,而且毫無意義,這就要求我們設計一套不同的比較方案,從語法和邏輯上都能行得通,所以本例中我們比較的是兩名學生的成績(score)。
    ? ? ? ? 不同的比較方案最終導致了算法(函數體)的不同,我們不得不借助模板的顯示具體化技術對 STU 類型進行單獨處理。第 14 行代碼就是顯示具體化的聲明,第 34 行代碼進行了定義。
    ? ? ? ? 請讀者注意第 34 行代碼,Max<STU>中的STU表明了要將類型參數 T 具體化為 STU 類型,原來使用 T 的位置都應該使用 STU 替換,包括返回值類型、形參類型、局部變量的類型。? ? ? ? ? ??
    ????????Max 只有一個類型參數 T,并且已經被具體化為 STU 了,這樣整個模板就不再有類型參數了,類型參數列表也就為空了,所以模板頭應該寫作template<>。
    ? ? ? ? 另外,Max<STU>中的STU是可選的,因為函數的形參已經表明,這是 STU 類型的一個具體化,編譯器能夠逆推出 T 的具體類型。簡寫后的函數聲明為:

    template<> const STU& Max(const STU& a, const STU& b);

    也就是說,函數模板顯式具體化有2個步驟:

    1.將函數名后面加<>,里面按照模板參數列表逐個實例化類型參數,模板參數列表置空,因為不需要推斷類型了,有了具體的實例化類型后。

    2.將函數中返回類型(函數參數列表,以及局部變量中)的T類型變量都實例化成STU

    3.函數名后面的尖括號和里面的實例化類型可以省略。

    類模板的顯式具體化

    ????????除了函數模板,類模板也可以顯示具體化,并且它們的語法是類似的。

    ? ? ? ?為什么類模板參數具體化呢?有時候,可能需要在為特殊類型實例化時,對模板進行修改,使其行為不同。在這種情況下,可以創建顯示具體化。當具體化模板和通用模板都與實例化請求匹配時,編譯器將選擇具體化版本。

    具體化類模板定義格式為:template<>class 類名<具體類型>

    注意:在上面的顯式具體化例子中,我們為所有的類型參數都提供了實參,所以最后的模板頭為空,也即template<>。另外 C++ 還允許只為一部分類型參數提供實參,這稱為部分顯式具體化。

    部分顯式具體化只能用于類模板,不能用于函數模板。

    仍然以 Point 為例,假設我現在希望“只要橫坐標 x 是字符串類型”就以|來分隔輸出結果,而不管縱坐標 y 是什么類型,這種要求就可以使用部分顯式具體化技術來滿足。請看下面的代碼:
    具體化是將通用模板具體化,因此必須要有通用模板存在才可以使用具體化,否則單獨使用會報錯。下面先來一個具體的例子:

    #include <string> #include <iostream> using namespace std; template <class T1,class T2> class AA{public:void print();}; template<class T1,class T2> //類外定義的模板成員函數類名后面要加尖括號,里面放具體的類型T1,T2 void AA<T1,T2>::print() {cout << "template<class T1,class T2>" << std::endl; }//下面將上面的類模板具體實例化,在原來模板的2個地方做修改即可: //1.模板參數列表置為空 //2.類名后面加上<>,里面放上具體的參數名字。 //3.在類外定義具體實例化的類的成員函數時,不用加類模板了 //4.不過,不論是類模板也好,類模板具體實例化的類也罷,類外定義成員函數都需要 //類名后面加具體的類型的。比如下面的AA<int,int>::,類名后面就有<int,int>template<> class AA<int,int> {public:void print(); }; void AA<int,int>::print() {cout << "template<>" << endl; }int main() { AA<int,int> a1; a1.print(); AA<double,double> a2; a2.print();return 0; }

    運行結果:?

    template<> template<class T1,class T2>

    ? ? ? ? 在《C++類模板》一節中我們定義了一個 Point 類,用來輸出不同類型的坐標。在輸出結果中,橫坐標 x 和縱坐標 y 是以逗號,為分隔的,但是由于個人審美的不同,我希望當 x 和 y 都是字符串時以|為分隔,是數字或者其中一個是數字時才以逗號,為分隔。為了滿足我這種奇葩的要求,可以使用顯示具體化技術對字符串類型的坐標做特殊處理。

    下面的例子演示了如何對 Point 類進行顯示具體化:

    #include <string> #include <iostream> #include <memory> using namespace std;//先定義一個類,方式是: //(1)先來一個模板頭和模板參數列表 //(2) template<typename T1,typename T2> class Point{public:Point(T1 t1,T2 t2):m_x(t1),m_y(t2){}T1 getX()const{return m_x;}T2 getY()const{return m_y;}void setX(T1 a){m_x = a;}void setY(T2 b){m_y = b;}void display()const;private:T1 m_x;T2 m_y; };template <typename T1,typename T2> void Point<T1,T2>::display()const {cout << "x=" << m_x << "m_y:"<< m_y << endl; }//類模板的顯式具體化:針對字符串類型的顯式具體化 //類模板顯式具體化和函數模板顯式具體化步驟類似: //1.在類名后面加尖括號<>,里面放置具體的類型 //2.類中使用的類型參數全部用具體類型的實際參數對應替代。 //3.類作用域template<> class Point<char*,char*> {public: Point(char* t1,char* t2):m_x(t1),m_y(t2){} char* getX(){return m_x;} char* getY(){return m_y;} void setX(char *a){m_x = a;} void setY(char *b){m_y = b;}void display()const;private:char *m_x;char *m_y; }; //因為這個類已經實例化了,有模板參數有具體的參數類型了(char* char*),所以 //具體化的類不能帶模板頭了 void Point<char*,char*>::display()const {cout << "x=" << m_x << " | y=" << m_y << endl; }int main() {Point<int,int> p1(123,123); p1.display(); Point<char*,char*> p2("aaa","bbb"); p2.display();return 0; }

    類模板也可以部分參數具體實例化,如下即是:?

    #include <string> #include <iostream> using namespace std;template<typename T1,typename T2> class AA{public:void print(); };template<typename T1,typename T2> void AA<T1,T2>::print() {cout << "template<class T1,class T2>" << endl; }template<class T1> class AA<T1,int> {public:void print(); };template<class T1> void AA<T1,int>::print() {cout << "template<class T1,int>" << endl; }int main() { AA<double,int> p1; p1.print(); AA<double,double> p2; p2.print(); AA<int,int> p3; p3.print();return 0; }

    也可以如下定義:

    #include <string> #include <iostream> using namespace std;template<typename T1,typename T2> class AA{public:void print(); };template<typename T1,typename T2> void AA<T1,T2>::print() {cout << "template<class T1,class T2>" << endl; }template<class T2> //部分實例化的時候,當類名后面放<>以及內部的2個參數的時候,必須按照原來的類的順序放置。 class AA<int,T2> {public:void print(); };template<class T2> void AA<int,T2>::print() {cout << "template<class int,T2>" << endl; }int main() { AA<double,int> p1; p1.print(); AA<double,double> p2; p2.print(); AA<int,int> p3; p3.print();return 0; }

    注意:在部分具體實例化的類的定義中:

    1.模板頭中僅僅只放類型參數(類型未知的參數)。

    2.類名后面的尖括號內放所有參數,包括類型已知的和未知的。

    ?類名后面之所以要列出所有的類型參數,是為了讓編譯器確認“到底是第幾個類型參數被具體化了”,如果寫作template<typename T2> class Point<char*>,編譯器就不知道char*代表的是第一個類型參數,還是第二個類型參數。

    題外話:如果編譯函數的時候g++ 遇到類似下面的結果,...

    undefined reference to some_function...,那一般的錯誤就是沒有給函數提供定義。這就是一個連接期的錯誤,有的讀者可能說:我提供定義了呀,那么就該檢查下自己定義的函數和聲明的函數是不是同一個(一般是參數類型或者返回類型出現錯誤)。

    r@r:~/coml/c++/review$ g++ 4.cc -o 123 /usr/bin/ld: /tmp/ccIzfftp.o: in function `main': 4.cc:(.text+0x83): undefined reference to `void Swap<int*>(int*, int*, int)' collect2: error: ld returned 1 exit status

    7.7 c++模板中的非類型參數

    模板是一種泛型技術,目的是將數據的類型參數化,以增強 C++ 語言(強類型語言)的靈活性。C++ 對模板的支持非常自由,模板中除了可以包含類型參數,還可以包含非類型參數,例如:

    template<typename T, int N> class Demo{ }; template<class T, int N> void func(T (&arr)[N]);

    T 是一個類型參數,它通過class或typename關鍵字指定。N 是一個非類型參數,用來傳遞數據的值,而不是類型,它和普通函數的形參一樣,都需要指明具體的類型。類型參數和非類型參數都可以用在函數體或者類體中。

    當調用一個函數模板或者通過一個類模板創建對象時,非類型參數會被用戶提供的、或者編譯器推斷出的值所取代。

    在函數模板中使用非類型參數

    在《C++函數模板的重載》一節中,我們通過 Swap() 函數來交換兩個數組的值,其原型為:

    template<typename T> void Swap(T a[], T b[], int len);

    形參 len 用來指明要交換的數組的長度,調用 Swap() 函數之前必須先通過sizeof求得數組長度再傳遞給它。(除非用數組的引用傳遞數組名字,就不用轉換成指針了)

    有讀者可能會疑惑,為什么在函數內部不能求得數組長度,一定要通過形參把數組長度傳遞進去呢?這是因為數組在作為函數參數時會自動轉換為數組指針,而sizeof只能通過數組名求得數組長度,不能通過數組指針求得數組長度,這在《數組和指針絕不等價,數組是另外一種類型》《數組到底在什么時候會轉換為指針》兩節中已經作了詳細講解。

    #include <string> #include <cstring> #include <cstdlib> #include <iostream> //abs函數在 cmath 中 #include <cmath> using namespace std;template<typename T,size_t N> class Array{public:Array();Array(initializer_list<T> li);size_t size()const;size_t get_capacity()const;//把數組的大小擴容n個單位bool capacity(int n);T& operator[](int i);private:size_t m_length; //數組當前大小size_t m_capacity; //數組的容量T* m_ptr; //指向數組首元素地址};template<typename T,size_t N> Array<T,N>::Array():m_length(N),m_capacity(N),m_ptr(new T[N]){}template<typename T,size_t N> size_t Array<T,N>::size()const {return m_length; }template<typename T,size_t N> size_t Array<T,N>::get_capacity()const {return m_capacity; }template<typename T,size_t N> T& Array<T,N>::operator[](int i) {if(i < 0 || i >= m_length){cout << "exception:too big index." << endl;static T temp;return temp;}return m_ptr[i]; }template<typename T,size_t N> bool Array<T,N>::capacity(int n) { if(n > 0){int len = m_length + n;if(len <= m_capacity){m_length = len;return true;}else//數組的容量不夠{T * new_ptr = new T[m_length + 2*n];if(new_ptr == NULL){cout << "Not space to allocate." << endl;return false;}else{//從內存地址m_ptr開始賦值m_length * sizeof(T)//個字節到new_ptr中memcpy(new_ptr,m_ptr,m_length * sizeof(T));//m_capacity equals to total storage sizem_capacity = m_length + 2 * n;m_length = len; delete [] m_ptr;m_ptr = new_ptr;return true;} }} //縮小數組 else{int len = m_length - abs(n);//縮小后的數組長度if(len < 0){cout << "too small Array." << endl;return false;}else{m_length = len;return true; }} }template<typename T,size_t N> void print(const T (&p)[N]) { size_t len = sizeof(p) / sizeof(p[0]); for(auto i = 0;i != len; ++ i){if(i == N-1){cout << p[i] << endl;}else{cout << p[i] << ",";} } }int main() { Array<int,6> a; a[0] = 111; a[2]= 333; a[1] = 222; cout << a[0] << endl; cout << a[1] << endl; a[0] = 123; cout << a[0] << endl; cout << a.get_capacity() << endl; cout << a.size() << endl; cout <<"too big:"<< a[100] << endl; a.capacity(100); cout << a.size()<< endl; cout << a.get_capacity();return 0; }

    運行結果:?

    111 222 123 6 6 too big:exception:too big index. 0 106 206

    7.9將C++模板應用于多文件編程

    ????????在將函數應用于多文件編程時,我們通常是將函數定義放在源文件(.cpp文件)中,將函數聲明放在頭文件(.h文件)中,使用函數時引入(#include命令)對應的頭文件即可。

    ????????編譯是針對單個源文件的,只要有函數聲明,編譯器就能知道函數調用是否正確;而將函數調用和函數定義對應起來的過程,可以延遲到鏈接時期。正是有了鏈接器的存在,函數聲明和函數定義的分離才得以實現。

    ????????將類應用于多文件編程也是類似的道理,我們可以將類的聲明和類的實現分別放到頭文件和源文件中。類的聲明已經包含了所有成員變量的定義和所有成員函數的聲明(也可以是 inline 形式的定義),這樣就知道如何創建對象了,也知道如何調用成員函數了,只是還不能將函數調用與函數實現對應起來,但是這又有什么關系呢,反正鏈接器可以幫助我們完成這項工作。

    ????????總起來說,不管是函數還是類,聲明和定義(實現)的分離其實是一回事,都是將函數定義放到其他文件中,最終要解決的問題也只有一個,就是把函數調用和函數定義對應起來(找到函數定義的地址,并填充到函數調用處),而保證完成這項工作的就是鏈接器。

    ????????基于傳統的編程思維,初學者往往也會將模板(函數模板和類模板)的聲明和定義分散到不同的文件中,以期達到「模塊化編程」的目的。但事實證明這種做法是不對的,程序員慣用的做法是將模板的聲明和定義都放到頭文件中

    ????????模板并不是真正的函數或類,它僅僅是用來生成函數或類的一張“圖紙”,在這個生成過程中有三點需要明確:

    • 模板的實例化是按需進行的,用到哪個類型就生成針對哪個類型的函數或類,不會提前生成過多的代碼;
    • 模板的實例化是由編譯器完成的,而不是由鏈接器完成的;
    • 在實例化過程中需要知道模板的所有細節,包含聲明和定義。
    • 將函數模板的聲明和定義分散到不同的文件

      為了更加深入地說明問題,現在有一個反面教材,它將函數模板的聲明和實現分別放到了頭文件和源文件。

      func.cpp 源碼:
    • //交換兩個數的值template<typename T> void Swap(T &a, T &b){T temp = a;a = b;b = temp;}//冒泡排序算法void bubble_sort(int arr[], int n){for(int i=0; i<n-1; i++){bool isSorted = true;for(int j=0; j<n-1-i; j++){if(arr[j] > arr[j+1]){isSorted = false;Swap(arr[j], arr[j+1]); //調用Swap()函數}}if(isSorted) break;}} //func.h源碼#ifndef _FUNC_H#define _FUNC_Htemplate<typename T> void Swap(T &a, T &b);void bubble_sort(int arr[], int n);#endif //main.cpp源碼#include <iostream>#include "func.h"using namespace std;int main(){int n1 = 10, n2 = 20;Swap(n1, n2);double f1 = 23.8, f2 = 92.6;Swap(f1, f2);return 0;}

      該工程包含了兩個源文件和一個頭文件,func.cpp中定義了兩個函數,func.h中對函數進行了聲明,main.cpp中對函數進行了調用,這是典型的將函數的聲明和實現分離的編程模式。

      運行上面的程序,會產生一個鏈接錯誤,意思是無法找到void Swap<double>(double &, double &)這個函數。主函數 main() 中共調用了兩個版本的 Swap() 函數,它們的原型分別是:

      void Swap<double>(int &, int &);
      void Swap<double>(double &, double &);

      為什么針對 int 的版本能夠找到定義,而針對 double 的版本就找不到呢?

      我們先來說針對 double 的版本為什么找不到定義。當編譯器編譯main.cpp時,發現使用到了 double 版本的 Swap() 函數,于是嘗試生成一個 double 版本的實例,但是由于只有聲明沒有定義,所以生成失敗。不過這個時候編譯器不會報錯,而是對該函數的調用做一個記錄,希望等到鏈接程序時在其他目標文件(.obj 文件或 .o 文件)中找到該函數的定義。很明顯,本例需要到func.obj中尋找。但是遺憾的是,func.cpp中沒有調用 double 版本的 Swap() 函數,編譯器不會生成 double 版本的實例,所以鏈接器最終也找不到 double 版本的函數定義,只能拋出一個鏈接錯誤,讓程序員修改代碼。

      那么,針對 int 的版本為什么能夠找到定義呢?請讀者注意bubble_sort()函數,該函數用來對數組元素進行排序,在排序過程中需要頻繁的交換兩個元素的值,所以調用了 Swap() 函數,這樣做的結果是:編譯生成的func.obj中會有一個 int 版本的 Swap() 函數定義。編譯器在編譯main.cpp時雖然找不到 int 版本的實例,但是等到鏈接程序時,鏈接器在func.obj中找到了,所以針對 int 版本的調用就不會出錯。

      將類模板的聲明和實現分散到不同的文件

      我們再看一個類模板的反面教材,它將類模板的聲明和實現分別放到了頭文件和源文件

    • point.h 源碼:

    • #ifndef _POINT_H#define _POINT_Htemplate<class T1, class T2>class Point{public:Point(T1 x, T2 y): m_x(x), m_y(y){ }public:T1 getX() const{ return m_x; }void setX(T1 x){ m_x = x; }T2 getY() const{ return m_y; };void setY(T2 y){ m_y = y; };void display() const;private:T1 m_x;T2 m_y;};#endif //point.cpp源碼#include <iostream>#include "point.h"using namespace std;template<class T1, class T2>void Point<T1, T2>::display() const{cout<<"x="<<m_x<<", y="<<m_y<<endl;} //main.cpp#include <iostream>#include "point.h"using namespace std;int main(){Point<int, int> p1(10, 20);p1.setX(40);p1.setY(50);cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;Point<char*, char*> p2("東京180度", "北緯210度");p2.display();return 0;}

      該工程包含了兩個源文件和一個頭文件,point.h中聲明了類模板,point.cpp中對類模板進行了實現,main.cpp中通過類模板創建了對象,并調用了成員函數,這是典型的將類的聲明和實現分離的編程模式。

      運行上面的程序,會產生一個鏈接錯誤,意思是無法通過 p2 調用Point<char*, char*>::display() const這個函數。

      類模板聲明位于point.h中,它包含了所有成員變量的定義以及構造函數、get 函數、set 函數的定義,這些信息足夠創建出一個完整的對象了,并且可以通過對象調用 get 函數和 set 函數,所以main.cpp的前 11 行代碼都不會報錯。而第 12 行代碼調用了 display() 函數,該函數的定義位于point.cpp文件中,并且point.cpp中也沒有生成對應的實例,所以會在鏈接期間拋出錯誤。

      總結

      通過上面的兩個反面教材可以總結出,「不能將模板的聲明和定義分散到多個文件中」的根本原因是:模板的實例化是由編譯器完成的,而不是由鏈接器完成的,這可能會導致在鏈接期間找不到對應的實例。

      修復上面兩個項目的方法也很簡單,就是將 func.cpp、point.cpp 的模板定義(實現)部分分別合并到 func.h、point.h 中。

    • 7.10 C++模板的顯式實例化

    ????? 前面講到的模板的實例化是在調用函數或者創建對象時由編譯器自動完成的,不需要程序員引導,因此稱為隱式實例化。相對應的,我們也可以通過代碼明確地告訴編譯器需要針對哪個類型進行實例化,這稱為顯式實例化。

    ????????編譯器在實例化的過程中需要知道模板的所有細節:對于函數模板,也就是函數定義;對于類模板,需要同時知道類聲明和類定義。我們必須將顯式實例化的代碼放在包含了模板定義的源文件中,而不是僅僅包含了模板聲明的頭文件中。

    ????????顯式實例化的一個好處是,可以將模板的聲明和定義(實現)分散到不同的文件中了。

    函數模板的顯式實例化

    以上節講到的 compare() 函數為例,針對 double 類型的顯式實例化代碼為:

    template void Swap(double &a, double &b);

    ????????這條語言由兩部分組成,前邊是一個template關鍵字(后面不帶<>),后面是一個普通的函數原型,組合在一起的意思是:將模板實例化成和函數原型對應的一個具體版本。

    將該代碼放到 func.cpp 文件的最后,再運行程序就不會出錯了。

    另外,還可以在包含了函數調用的源文件(main.cpp)中再增加下面的一條語句:

    extern template void Swap(double &a, double &b);

    ????????該語句在前面增加了extern關鍵字,它的作用是明確地告訴編譯器,該版本的函數實例在其他文件中,請在鏈接期間查找。不過這條語句是多余的,即使不寫,編譯器發現當前文件中沒有對應的模板定義,也會自動去其他文件中查找。

    ????????上節我們展示了一個反面教材,告訴大家不能把函數模板的聲明和定義分散到不同的文件中,但是現在有了顯式實例化,這一點就可以做到了,下面就對上節的代碼進行修復。

    func.cpp 源碼:

    //交換兩個數的值template<typename T> void Swap(T &a, T &b){T temp = a;a = b;b = temp;}//冒泡排序算法void bubble_sort(int arr[], int n){for(int i=0; i<n-1; i++){bool isSorted = true;for(int j=0; j<n-1-i; j++){if(arr[j] > arr[j+1]){isSorted = false;Swap(arr[j], arr[j+1]); //調用Swap()函數}}if(isSorted) break;}}template void Swap(double &a, double &b); //顯式實例化定義

    文件func.h

    #ifndef _FUNC_H#define _FUNC_Htemplate<typename T> void Swap(T &a, T &b);void bubble_sort(int arr[], int n);#endif //main.cpp源碼 #include <iostream>#include "func.h"using namespace std;//顯示實例化聲明(也可以不寫)extern template void Swap(double &a, double &b);extern template void Swap(int &a, int &b);int main(){int n1 = 10, n2 = 20;Swap(n1, n2);double f1 = 23.8, f2 = 92.6;Swap(f1, f2);return 0;}

    ????????顯式實例化也包括聲明和定義,定義要放在模板定義(實現)所在的源文件,聲明要放在模板聲明所在的頭文件(當然也可以不寫)。既然聲明放在模板聲明所在文件,那不是應該放在func.h中嗎?

    類模板的顯式實例化

    ???????? 類模板的顯式實例化和函數模板類似。以上節的 Point 類為例,針對char*類型的顯式實例化(定義形式)代碼為:

    template class Point<char*, char*>;

    相應地,它的聲明形式為:

    extern template class Point<char*, char*>;

    不管是聲明還是定義,都要帶上class關鍵字,以表明這是針對類模板的。

    ????????另外需要注意的是,顯式實例化一個類模板時,會一次性實例化該類的所有成員,包括成員變量和成員函數。

    ????????有了類模板的顯式實例化,就可以將類模板的聲明和定義分散到不同的文件中了,下面我們就來修復上節的代碼。

    point.cpp 源文件:

    #include <iostream>#include "point.h"using namespace std;template<class T1, class T2>void Point<T1, T2>::display() const{cout<<"x="<<m_x<<", y="<<m_y<<endl;}//顯式實例化定義template class Point<char*, char*>;template class Point<int, int>;

    point.cpp的源碼

    #ifndef _POINT_H#define _POINT_Htemplate<class T1, class T2>class Point{public:Point(T1 x, T2 y): m_x(x), m_y(y){ }public:T1 getX() const{ return m_x; }void setX(T1 x){ m_x = x; }T2 getY() const{ return m_y; };void setY(T2 y){ m_y = y; };void display() const;private:T1 m_x;T2 m_y;};#endif

    main.cpp source code

    #include <iostream>#include "point.h"using namespace std;//顯式實例化聲明(也可以不寫)extern template class Point<char*, char*>;extern template class Point<int, int>;int main(){Point<int, int> p1(10, 20);p1.setX(40);p1.setY(50);cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;Point<char*, char*> p2("東京180度", "北緯210度");p2.display();return 0;}

    函數模板和類模板的實例化語法是類似的,我們不妨對它們做一下總結:

    extern template declaration; //實例化聲明 template declaration; //實例化定義

    對于函數模板來說,declaration 就是一個函數原型;對于類模板來說,declaration 就是一個類聲明。

    顯式實例化的缺陷

    ????????C++ 支持顯式實例化的目的是為「模塊化編程」提供一種解決方案,這種方案雖然有效,但是也有明顯的缺陷:程序員必須要在模板的定義文件(實現文件,也就是.cpp文件而非.h文件)中對所有使用到的類型進行實例化。這就意味著,每次更改了模板使用文件(調用函數模板的文件,或者通過類模板創建對象的文件),也要相應地更改模板定義文件,以增加對新類型的實例化,或者刪除無用類型的實例化。
    ??????? 一個模板可能會在多個文件中使用到,要保持這些文件的同步更新是非常困難的。而對于庫的開發者來說,他不能提前假設用戶會使用哪些類型,所以根本就無法使用顯式實例化,只能將模板的聲明和定義(實現)全部放到頭文件中;C++ 標準庫幾乎都是用模板來實現的,這些模板的代碼也都位于頭文件中。

    ????????總起來說,如果我們開發的模板只有我們自己使用,那也可以勉強使用顯式實例化;如果希望讓其他人使用(例如庫、組件等),那只能將模板的聲明和定義都放到頭文件中了。

    總結

    以上是生活随笔為你收集整理的c++ 模板教程(c语言中文网) 自己运行实例的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。