泛型、模板
阿新 • • 發佈:2021-10-19
一、泛型、模板
知乎搜尋:如何通俗地理解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
注意事項:
- 自動型別推導:必須推匯出一致的資料T才可以使用
- 模板必須要確定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; }
普通函式與函式模板的區別:
- 普通函式呼叫時可以發生自動型別轉換(隱式型別轉換)
- 函式模板呼叫時,如果利用自動型別推導,不會發生隱式型別轉換
- 如果利用顯示指定型別的方式,可以發生隱式型別轉換
- 建議使用顯示指定型別的方式,呼叫函式模板。因為可以自己確定通用型別T
普通函式與函式模板呼叫規則:
- 如果函式模板和普通函式都可以實現,優先呼叫普通函式
- 可以通過空模板引數列表來強制呼叫函式模板
- 函式模板可以產生更好的匹配,優先呼叫函式模板
- 既然提供了函式模板,最好就不要提供普通函式,否則容易出現二義性
- 函式模板也可以發生過載
三、類模板
作用:建立一個通用類,類中的成員資料可以不具體指定,用一個虛擬的型別來代表
語法註釋:
- template —— 宣告建立模板
- typename —— 表明後面的符號是一種資料型別,可以用class代替
- 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
類模板與函式模板的區別:
- 類模板沒有自動型別推導的使用方式
- 類模板在模板引數列表中可以有預設引數
#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
類模板中成員函式建立時機:
- 類模板中成員函式和普通類中成員函式的建立時機是有區別的
- 普通類中的成員函式一開始就可以建立
- 類模板中的成員函式在呼叫時才建立
#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、指定傳入的型別 —— 直接顯示物件的資料型別
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
類模板與繼承:
- 當子類繼承的父類是一個類模板時,子類在宣告的時候,要指出父類中T的型別
- 如果不指定父類中T的型別,編譯器無法給子類分配記憶體
- 如果想靈活指定出父類中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;
}
類模板成員函式類外實現:
類模板成員函式類外實現規則:
類模板成員函式類外實現時,需要加上模板引數列表
#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
類模板分檔案編寫
簡介:
- 類模板中成員函式建立時機是在呼叫階段,導致分檔案編寫時連結不到。
- 解決方式:
- 直接包含.cpp原始檔
- 將宣告和實現寫到同一個檔案中,並更改字尾名為 .hpp,.hpp是約定的名稱,並不是強制。
- 主流的解決方式是第二種,將類模板成員函式寫到一起,並將字尾名改為 .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;
}