1. 程式人生 > 其它 >泛型、模板

泛型、模板

一、泛型、模板

知乎搜尋:如何通俗地理解C++的模板?

個人認為比較容易接受的回答:

模板就是建立通用的模具,大大提高複用性。

模板的特點:

  • 模板不可以直接使用,它只是一個框架
  • 模板的通用並不是萬能的
  • 根本目的是為了程式碼複用
  • C++提供兩種模板機制:函式模板和類模板

另外有趣的解釋:

  • 公式
  • 類是例項物件的媽,負責執行期生成物件;模板是類、函式的媽,負責編譯期生成類、函式。

二、函式模板

基本語法

作用:建立一個通用函式,其函式返回值型別和形參型別可以不具體指定,用一個虛擬的型別來代表。

函式模板關鍵字:template

函式模板宣告/定義:template<typename T>

1、template —— 宣告建立模板

2、typename —— 表明其後面的符號是一種資料型別,可以使用class代替

3、T —— 通用的資料型別,名稱可以替換、通常為大寫字母

使用函式模板有兩種方式:自動型別推導、顯示指定型別

模板的目的是為了提高複用性、將型別也引數化。

#include <iostream>
using namespace std;

// 函式模板
// 交換兩個整數
void SwapIntNum(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}
// 交換兩個浮點數
void SwapDoubleNum(double& a, double& b) {
    double temp = a;
    a = b;
    b = temp;
}

// 基於以上兩個函式,可以看出,除了引數的資料型別不同,程式碼邏輯都是一樣的。基於這種場景,出現了下面的函式模板:
// 注意這裡先定義形參的型別
// 宣告一個模板,告訴編譯器後面程式碼中緊跟的T不要報錯,T是一個通用資料型別,泛指,不具體表示某一具體型別
// 注意 <> 括號裡面的typename 可以替換成class
template<typename T>
void SwapNum(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

void test_case01() {
    int a = 20;
    int b = 30;
    
    SwapIntNum(a, b);
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    
    double c = 1.11;
    double d = 2.34;
    SwapDoubleNum(c, d);
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;    
}

void test_case02() {
    int a = 20;
    int b = 30;
    // 1、自動型別推導:根據a、b的資料型別int自動設定T為int
    SwapNum(a, b);
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    
    double c = 1.11;
    double d = 2.34;
    // 2、顯示指定型別:指定模板中資料型別T為double型。更推薦這種寫法
    SwapNum<double>(c, d);
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;       
}

int main() {
    test_case01();
    cout << "使用模板函式 ===>>>" << endl;
    test_case02();
}

輸出結果:

bzl@bzl ~ o ./a.out 
a = 30
b = 20
c = 2.34
d = 1.11
使用模板函式 ===>>>
a = 30
b = 20
c = 2.34
d = 1.11
bzl@bzl ~ o

注意事項:

  1. 自動型別推導:必須推匯出一致的資料T才可以使用
  2. 模板必須要確定T的資料型別,才可以使用
#include <iostream>
using namespace std;

// 模板必須要確定出T的資料型別,才可以使用
template<class T>
void func() {
    cout << "func 呼叫" << endl;
}

void test_case01() {
	func();  // 錯誤:模板必須要確定T的資料型別才可以使用
    // func<int>();  // 正確:指定了模板的資料型別為int
}

int main() {
    test_case01();
    return 0;
}

普通函式與函式模板的區別:

  1. 普通函式呼叫時可以發生自動型別轉換(隱式型別轉換)
  2. 函式模板呼叫時,如果利用自動型別推導,不會發生隱式型別轉換
  3. 如果利用顯示指定型別的方式,可以發生隱式型別轉換
  4. 建議使用顯示指定型別的方式,呼叫函式模板。因為可以自己確定通用型別T

普通函式與函式模板呼叫規則:

  1. 如果函式模板和普通函式都可以實現,優先呼叫普通函式
  2. 可以通過空模板引數列表來強制呼叫函式模板
  3. 函式模板可以產生更好的匹配,優先呼叫函式模板
  4. 既然提供了函式模板,最好就不要提供普通函式,否則容易出現二義性
  5. 函式模板也可以發生過載

三、類模板

作用:建立一個通用類,類中的成員資料可以不具體指定,用一個虛擬的型別來代表

語法註釋:

  1. template —— 宣告建立模板
  2. typename —— 表明後面的符號是一種資料型別,可以用class代替
  3. T —— 通用的資料型別,名稱可以替換,通常為大寫字母
#include <iostream>
using namespace std;

template<class NameType, class AgeType>
class Person {
    public:
    	Person(NameType name, AgeType age) {
            // 建構函式賦初值
            this->name = name;
            this->age = age;
        }
    	void ShowPersonInfo() {
            cout << "name: " << this->name << " age: " << this->age << endl;
        }
    private:
    	NameType name;
    	AgeType age;
};

void test_case01() {
    // <> 裡面是模板的引數列表
    Person<string, int> person_obj("孫武", 23);
	person_obj.ShowPersonInfo();    
}

int main(){
	test_case01();
    return 0;
}

輸出結果:

bzl@bzl ~ o ./a.out 
name: 孫武 age: 23
bzl@bzl ~ o 

類模板與函式模板的區別:

  1. 類模板沒有自動型別推導的使用方式
  2. 類模板在模板引數列表中可以有預設引數
#include <iostream>
using namespace std;

// 類模板在模板引數列表中可以有預設引數
template<class NameType, class AgeType = int>
class Person {
    public:
    	Person(NameType name, AgeType age) {
            // 建構函式賦初值
            this->name = name;
            this->age = age;
        }
    	void ShowPersonInfo() {
            cout << "name: " << this->name << " age: " << this->age << endl;
        }
    private:
    	NameType name;
    	AgeType age;
};

void test_case01() {
    // <> 裡面是模板的引數列表
    Person<string, double> person_obj01("孫武1", 23.5);
	person_obj01.ShowPersonInfo();
    // age 使用預設引數int
    Person<string> person_obj02("孫武2", 23.5);
	person_obj02.ShowPersonInfo();    
}

int main(){
	test_case01();
    return 0;
}

輸出結果:

bzl@bzl ~ o ./a.out 
name: 孫武1 age: 23.5
name: 孫武2 age: 23
bzl@bzl ~ o 

類模板中成員函式建立時機:

  1. 類模板中成員函式和普通類中成員函式的建立時機是有區別的
  2. 普通類中的成員函式一開始就可以建立
  3. 類模板中的成員函式在呼叫時才建立
#include <iostream>
using namespace std;

class Person1 {
    public:
    	void ShowPersonInfo() {
            cout << "Person1 show" << endl;
        }
};

class Person2 {
    public:
    	void ShowPersonInfo() {
            cout << "Person2 show" << endl;
        }
};

template<class T>
class PersonTemplate {
    public:
    	T obj;
        // 類模板中的成員函式
        void func() {
            // 類模板中的成員函式在呼叫時才建立
            obj.ShowPersonInfo();
        }
};

void test_case01() {
    // 根據類模板實際傳參的不同,在func函式中呼叫不同class的成員函式 ShowPersonInfo 
    PersonTemplate<Person1> person_obj;
    person_obj.func();
}

int main() {
	test_case01();
    return 0;
}

類模板物件做函式引數:

  1. 類模板例項化出的物件,向函式傳參的方式,一共有三種傳入方式:

    1、指定傳入的型別 —— 直接顯示物件的資料型別

    2、引數模板化 —— 將物件中的引數變為模板進行傳遞

    3、整個類模板化 —— 將這個物件型別 模板化 進行傳遞

// 使用比較廣泛的是第一種:指定傳入型別
#include <iostream>
#include <string>
#include <typeinfo>
using namespace std;

// 類模板物件用做函式引數
template<class T1, class T2>
class Person {
    private:
    	T1 name;
    	T2 age;
    public:
    	Person(T1 name, T2 age) {
            this->name = name;
            this->age = age;
        }
    	// 注意:除了建構函式解構函式外,其餘的函式必須指定返回值型別
    	void ShowPersonInfo() {
            cout << "姓名: " << this->name << " 年齡: " << this->age << endl;
        }
};

// 1、指定傳入型別
void PrintPerson1(Person<string, int>& p) {
    p.ShowPersonInfo();
}

void test_case01() {
    Person<string, int> p("孫武打的",  100);
    PrintPerson1(p);
}

// 2、引數模板化
template<class T1, class T2>
void PrintPerson2(Person<T1, T2>& p){
    p.ShowPersonInfo();
    cout << "T1的型別為: " << typeid(T1).name() << endl;
    cout << "T2的型別為: " << typeid(T2).name() << endl;
}

void test_case02() {
    Person<string, int> p("八戒", 56);
    PrintPerson2(p);
}

// 3、整個類模板化
template<class T>
void PrintPerson3(T& p) {
    p.ShowPersonInfo();
    cout << "T的型別為: " << typeid(T).name() << endl;
}

void test_case03() {
    Person<string, int> p("唐山", 30);
    PrintPerson3(p);
}

int main() {
	test_case01();
	test_case02();
	test_case03();
    return 0;
}

輸出結果:

bzl@bzl ~ o ./a.out 
姓名: 孫武打的 年齡: 100
姓名: 八戒 年齡: 56
T1的型別為: NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
T2的型別為: i
姓名: 唐山 年齡: 30
T的型別為: 6PersonINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEiE
bzl@bzl ~ o

類模板與繼承:

  1. 當子類繼承的父類是一個類模板時,子類在宣告的時候,要指出父類中T的型別
  2. 如果不指定父類中T的型別,編譯器無法給子類分配記憶體
  3. 如果想靈活指定出父類中T的型別,子類也需要變為類模板
#include <iostream>
using namespace std;

// 類模板與繼承
template<class T>
class Base {
    T m;
};

// 注意這裡:Base在被繼承的時候也需要指定模板型別
// class Son:public Base 報錯:必須要知道父類中T型別,才能繼承給子類,
// 因為編譯器不知道給子類多少個記憶體空間,如果T是int型別給1個位元組,
// 如果T是double型給4個位元組
class Son:public Base<int> {
    
};

void test_case01() {
    Son s1;
}

// 如果想靈活指定父類中T型別,子類也需要變類模板
template<class T1, class T2>
class Son2:public Base<T2> {
    public:
    	Son2() {
            cout << "T1的型別為: " << typeid(T1).name() << endl;
            cout << "T2的型別為: " << typeid(T2).name() << endl;
        }
    	T1 obj;
};

void test_case02() {
	// T1為int,即obj為int型,T2為char型,即m為char型
    Son2<int, char>S2;
}

int main() {
    test_case01();
    test_case02();
    return 0;
}

類模板成員函式類外實現:

  1. 類模板成員函式類外實現規則:

    類模板成員函式類外實現時,需要加上模板引數列表

#include <iostream>
#include <string>
using namespace std;

// 類模板成員函式類內實現
template<class T1, class T2>
class Person {
    private:
    	T1 name;
    	T2 age;
    public:
    	// 建構函式宣告
    	Person(T1 name, T2 age);
    	// 普通函式宣告
	    void ShowPerson();
};

// 建構函式類外實現
// Person<T1, T2>說明這是一個Person類模板的類外成員函式實現,
// Person::Person(T1 name, T2 age) 表示類的函式的類外實現,
// Person(T1 name, T2 age) 表示建構函式
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
    this->name = name;
    this->age = age;
}

// 成員函式類外實現
template<class T1, class T2>
void Person<T1, T2>::ShowPerson() {  // 有<T1, T2>表示是類模板的成員函式類外實現, Person表示是Person作用域的ShowPerson函式。
    cout << "姓名: " << this->name << " 年齡: " << this->age << endl;
}

void test_case01() {
    Person<string, int> p1("張三", 18);
    p1.ShowPerson();
}

int main() {
    test_case01();
    return 0;
}

輸出結果:

zhi@ubuntu ~/ros2-gtest-gmock o ./a.out 
姓名: 張三 年齡: 18
zhi@ubuntu ~/ros2-gtest-gmock o

類模板分檔案編寫

簡介:

  1. 類模板中成員函式建立時機是在呼叫階段,導致分檔案編寫時連結不到。
  2. 解決方式:
    1. 直接包含.cpp原始檔
    2. 將宣告和實現寫到同一個檔案中,並更改字尾名為 .hpp,.hpp是約定的名稱,並不是強制。
  3. 主流的解決方式是第二種,將類模板成員函式寫到一起,並將字尾名改為 .hpp。
// 類模板沒有分檔案編寫
#include <iostream>
#include <string>
using namespace std;

template<class T1, class T2>
class Person {
    private:
    	T1 name;
    	T2 age;
    public:
    	Person(T1 name, T2 age);
    	void ShowPerson();
};

// 類模板-建構函式
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
    this->name = name;
    this->age = age;
}

// 類模板-普通函式
template<class T1, class T2>
void Person<T1, T2>::ShowPerson() {
    cout << "姓名: " << this->name << " 年齡:" << this->age << endl;
}

void test_case01() {
    Person<string, int> p("Jerry", 18);
    p.ShowPerson();
}

int main() {
    test_case01();
    return 0;
}

輸出結果:

zhi@ubuntu ~/ros2-gtest-gmock o ./a.out 
姓名: Jerry 年齡:18
zhi@ubuntu ~/ros2-gtest-gmock o
// 類模板分檔案寫:方式一
// person.h
#pragma once  // 防止標頭檔案重複包含

#include <iostream>
#include <string>
using namespace std;

template<class T1, class T2>
class Person {
    private:
    	T1 name;
    	T2 age;
    public:
    	Person(T1 name, T2 age);
    	void ShowPerson();
};

// person.cpp
#include "person.h"

template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
    this->name = name;
    this->age = age;
}

template<class T1, class T2>
void Person<T1, T2>::ShowPerson() {
    cout << "姓名: " << this->name << " 年齡: " << this->age << endl;
}
// main.cpp
#include <iostream>

// 不能使用 #include "person.h", 如果僅包含 #include "person.h",
// 由於類模板中的成員函式並沒有建立,編譯器並不會去找 Person(T1 name, T2 age) 和
// void ShowPerson() 這兩個函式的定義。
// 如果包含 #include "person.cpp" 就會看到 Person(T1 name, T2 age)

#include "person.cpp"

using namespace std;

void test_case01() {
    Person<string, int> p("Jerry", 18);
    p.ShowPerson();
}

int main() {
    test_case01();
    return 0;
}

輸出結果:

bzl@bzl ~/cppcode/template-test o ./test-person 
姓名: Jerry 年齡: 18
bzl@bzl ~/cppcode/template-test o

第二種方式:將 .h 和 .cpp 中的內容寫到一起,將字尾名改為 .hpp 檔案

// person.hpp 檔案
#pragma once 

#include <iostream>
#include <string>

using namespace std;

template<class T1, class T2>
class Person {
    private:
    	T1 name;
    	T2 age;
    public:
    	Person(T1 name, T2 age);
    	void ShowPerson();
};

// 建構函式 類外實現
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
    this->name = name;
    this->age = age;
}

// 成員函式 類外實現
template<class T1, class T2>
void Person<T1, T2>::ShowPerson() {
    cout << "姓名" << this->name << " 年齡: " << this->age << endl;
}

// main.cpp
#include <iostream>

#include "person.hpp"

void test_case01() {
    Person<string, int> p("Jerry", 18);
    p.ShowPerson();
}

int main() {
    test_case01();
    return 0;
}