文章目錄
- 前言
- 泛型編程(模板)
- 1. 函數模板
- 1.1 函數模板格式
- 1.2 函數模板的實例化
- 隱式實例化
- 顯式指定模板參數實例化
- 1.3 函數模板實例化的原理
- 1.4 模板參數的匹配原則
- 2. 類模板
- 2.1 類模板的格式
- 2.2 類模板的實例化
- 2.3 類模板實例化的原理
- 2.4 類模板的匹配原則
- 模板的特化
- 3. 函數模板特化
- 4. 類模板特化
- 4.1 全特化
- 4.2 偏特化
- 4.3 類模板特化應用示例
- 模板分離編譯
- 5.什么是分離編譯
- 5.1 函數模板的分離編譯
- 5.2 類模板的分離編譯
- 非類型模板參數
- 模板優缺
- 總結
前言
通過00【C++ 入門基礎】前言得知,C++是為了解決C語言在面對大型項目的局限而誕生:
C語言面對的現實工程問題(復雜性、可維護性、可擴展性、安全性)
C語言面臨的具體問題:
struct 數據公開暴露,函數數據分離,邏輯碎片化。(復雜性、安全性)
修改數據結構,如 struct 新增字段,可能導致大量相關函數需要修改。(可維護性)
添加新功能常需修改現有函數或結構體,易引入錯誤。(可擴展性)
資源(內存、文件句柄)需手動管理,易泄漏或重復釋放。(安全性)
之前我們知道了,函數重載,可以幫我們解決:類似功能(加法)的函數命名域污染問題,這解決了我們代碼的復雜性問題,但是沒有很好的實現對代碼的復用性。
本文講解的泛型編程,就是C++用于對代碼復用、代碼可維護性、類型安全的手段之一,不僅對于函數,對于類也可以。
泛型編程(模板)
我們嘗試實現一個簡單的交換函數:
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}
void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}
我們可以根據參數類型不同,重載多個功能相同的同名函數,命名重復的問題我們通過函數重載的問題解決了,但是有多少個類似的應用場景,我們就需要手動的實現多少個,所以它就有以下一些不好的地方:
- 重載的函數僅僅是類型不同,代碼復用率比較低,只要有新類型出現時,就需要用戶自己增加對應的函數。
- 代碼的可維護性比較低,一個出錯可能所有的重載均出錯。
那能否告訴編譯器一個模子,讓編譯器根據不同的類型利用該模子來生成代碼呢?
如果在C++中,也能夠存在這樣一個模具,通過給這個模具中填充不同材料(類型),來獲得不同材料的鑄件(即生成具體類型的代碼),那將會節省許多頭發。
這個“模具”(函數模版、類模版),就是我們要編寫的通用代碼,最終我們通過傳不同的參數,讓編譯器自動生成對應的“鑄件”(函數、類)。
泛型編程:編寫與類型無關的通用代碼,最終是代碼復用的一種手段。模板是泛型編程的基礎。
1. 函數模板
函數模板代表了一個函數家族,該函數模板與類型無關,在使用時被參數化,根據實參類型產生函數的特定類型版本。
1.1 函數模板格式
template<typename T1, typename T2,…,typename Tn>
返回值類型 函數名(參數列表){}
現在我們使用函數模版,再去實現一個交換函數:
template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
注意:typename是用來定義模板參數關鍵字,也可以使用class(切記:不能使用struct代替class)
1.2 函數模板的實例化
用不同類型的參數使用函數模板時,稱為函數模板的實例化。模板參數實例化分為:隱式實例化和顯式實例化。
注意:與我們類從類型去實例化出一個對象不同,函數模版的實例化,是從模版實例化出一個函數。
隱式實例化
讓編譯器根據實參推演模板參數的實際類型。
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);/*該語句不能通過編譯,因為在編譯期間,當編譯器看到該實例化時,需要推演其實參類型通過實參a1將T推演為int,通過實參d1將T推演為double類型,但模板參數列表中只有一個T,編譯器無法確定此處到底該將T確定為int 或者 double類型而報錯注意:在模板中,編譯器一般不會進行類型轉換操作,因為一旦轉化出問題,編譯器就需要背黑鍋Add(a1, d1);*/// 此時有兩種處理方式:// 1. 用戶自己來強制轉化Add(a1, (int)d1); // 2. 使用顯式實例化//TODO...return 0;
}
顯式指定模板參數實例化
在函數名后的<>中指定模板參數的實際類型,相當于給模版傳一個類型參數。
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a = 10;double b = 20.0;// 顯式傳參實例化Add<int>(a, b);return 0;
}
如果類型不匹配,編譯器會嘗試進行隱式類型轉換,如果無法轉換成功編譯器將會報錯。
1.3 函數模板實例化的原理
函數模板是一個藍圖,它本身并不是函數,是我們寫給編譯器看的模具,最終編譯器再根據我們給這個模具傳入的材料(類型),去生成最終的函數。
所以其實模板就是將本來應該我們做的重復的事情(重復實現多個功能類似的函數)交給了編譯器去做。
函數的隱式實例化:
在編譯器編譯階段,對于模板函數的使用,編譯器需要根據傳入的實參類型來推演生成對應類型的函數以供調用。
比如:當用double類型的參數傳入函數模板使用時,編譯器通過對實參類型的推演,將T確定為double類型,然后產生一份專門處理double類型的代碼,對于字符類型也是如此。
我們知道普通的一個函數調用,編譯器會根據AST樹的域節點向上查找,直到找到與調用處函數名稱相同的聲明,然后根據函數的參數,生成唯一的函數符號,這個函數符號將會持續到鏈接過程。但是我們的模板呢?
如果編譯器找到與函數調用處同名的是一個函數模板,那么如果遇到函數的模版,編譯器就會根據函數調用處的參數,自動的推導出這個函數的聲明,并自動生成這個函數的定義,然后給函數調用處推導出一個符號,這個符號照樣會持續到鏈接過程,最終由鏈接器,生成不同的函數地址:
顯式指定模板參數實例化:
實例化行為與隱式推導完全相同,只是顯示傳入參數。
1.4 模板參數的匹配原則
我們知道編譯器會自動去查找到函數調用處同名的函數聲明或者定義,其實如果有多個同名,它會怎么辦呢?
編譯器的處理步驟:??
-
?名稱查找?:
編譯器在main函數中看到MyNS::foo(42)。
它首先解析MyNS命名空間,然后在MyNS的作用域內查找名為foo的所有函數。
它成功找到了三個候選函數:#1, #2, #3。至此,名稱查找完成。 -
重載解析?:
編譯器開始分析調用foo(42),實參是int類型的42。
它將實參int(42)與每個候選函數的形參進行匹配:
?匹配 #1 foo(int)??:?精確匹配?!不需要任何轉換。這是最好的情況。
?匹配 #2 foo(double)??:需要標準轉換?(int-> double)。匹配度良好,但不如精確匹配。
匹配 #3 foo(char)??:需要標準轉換?(int-> char)。但這是一個窄化轉換(可能丟失信息),在重載解析中優先級通常低于提升或標準算術轉換,幾乎不會被選擇。
?匹配 #4 template foo(T)??:通過模板轉換成foo(int)。匹配度良好,但不如精確匹配。
結果?:編譯器確定#1是最佳匹配。
- 代碼生成?:一旦重載解析完成,編譯器就確切地知道該調用哪個函數了。
?此時,編譯器才會根據這個具體函數的簽名(名稱+參數類型)去進行名稱修飾,生成一個唯一的鏈接符號(例如,_ZN4MyNS3fooEi可能代表MyNS::foo(int))。
此時如何選中模板,就會根據模板和選中的參數生成代碼,
這個修飾后的符號會被寫入目標文件,鏈接器后續會使用它來找到函數的定義。
其實編譯器在查找的時候,會找到所有與函數調用處同名的候選者,編譯器會把所有找到的同名函數(無論參數如何)都加入到候選列表中,最后選舉出與調用處最匹配的函數,如果選的是普通函數就正常的生成符號,并在調用處(Call)生成的符號;如果選的是模板就根據類型生成定義然后再在調用處生成(Call)符號。
(1)一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數
// 專門處理int的加法函數
int Add(int left, int right)
{return left + right;
}
// 通用加法函數
template<class T>
T Add(T left, T right)
{return left + right;
}
void Test()
{Add(1, 2); // 與非模板函數匹配,編譯器不需要特化Add<int>(1, 2); // 調用編譯器特化的Add版本
}
(2)對于非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數, 那么將選擇模板
// 專門處理int的加法函數
int Add(int left, int right)
{return left + right;
}// 通用加法函數
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return left + right;
}
void Test()
{Add(1, 2); // 與非函數模板類型完全匹配,不需要函數模板實例化Add(1, 2.0); // 模板函數可以生成更加匹配的版本,編譯器根據實參生成更加匹配的Add函數
}
(3)模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換
說的是我們模板函數接收的類型參數不可以私自轉換,但是我們普通函數接受的參數,可以強轉為別的類型。
2. 類模板
2.1 類模板的格式
template<class T1, class T2, ..., class Tn>
class 類模板名
{// 類內成員定義
};
2.2 類模板的實例化
類模板實例化與函數模板實例化不同,類模板實例化需要在類模板名字后跟<>,然后將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果才是真正的類。
// 動態順序表
// 注意:Vector不是具體的類,是編譯器根據被實例化的類型生成具體類的模具
template<class T>
class Vector
{
public:Vector(size_t capacity = 10): _pData(new T[capacity]), _size(0), _capacity(capacity){}// 使用析構函數演示:在類中聲明,在類外定義。~Vector();void PushBack(const T& data);void PopBack();// ...size_t Size() { return _size; }T& operator[](size_t pos){assert(pos < _size);return _pData[pos];}private:T* _pData;size_t _size;size_t _capacity;
};
// 注意:類模板中函數放在類外進行定義時,需要加模板參數列表
template <class T>
Vector<T>::~Vector()
{if (_pData)delete[] _pData;_size = _capacity = 0;
}// Vector類名,Vector<int>才是類型
int main()
{Vector<int> s1;Vector<double> s2;
}
2.3 類模板實例化的原理
類模板的實例化和函數的顯示實例化原理相似,編譯器通過名稱的查找到同名類型,然后通過顯示傳入的參數,去生成對應的實體代碼。
2.4 類模板的匹配原則
類模板的成員函數(尤其是構造函數)在調用時,會經歷與函數模板非常相似的重載解析和實例化過程,但類模板本身也有一些特殊性。??
-
類模板實例化 vs 成員函數調用:??
?類模板實例化:?? 當你使用一個類模板時(如 MyClass obj;),編譯器會根據提供的模板參數(int)?實例化整個類模板。這意味著:
生成一個具體的類類型 MyClass。
實例化該類所有非模板、非虛的成員變量聲明?(類型確定)。
?但不會立即實例化所有成員函數的定義體!?? 這是關鍵。
?成員函數調用:?? 當你調用一個類模板實例的成員函數時(如 obj.doSomething(42);),過程與函數模板調用類似:
?名稱查找:?? 在 MyClass的作用域內查找 doSomething的聲明(包括普通成員函數、成員函數模板、從基類繼承的成員等)。
?重載解析:?? 對找到的候選(包括成員函數模板推導出的簽名)進行匹配,選擇最佳匹配。
?實例化:?? ?只有當選中的成員函數是一個模板,或者它屬于一個尚未完全實例化的類模板時,編譯器才會在需要時實例化該特定成員函數的定義體。這就是 ??“惰性實例化” (Lazy Instantiation)?? 原則:只實例化真正被用到的成員函數。 -
構造函數的重載解析:??
構造函數的調用(如 MyClass obj(42);)是類模板成員函數調用的一個特例,也是最常見的場景。
過程完全遵循上述成員函數調用的規則:
名稱查找:在 MyClass中查找構造函數聲明。
重載解析:將實參 42(int) 與所有候選構造函數(包括構造函數模板推導出的簽名)進行匹配,選擇最佳匹配(如 MyClass::MyClass(int))。
實例化:如果選中的構造函數是模板或者尚未實例化,則實例化它的定義體。
?過程是否一樣???
?核心機制相同:?? 成員函數(包括構造函數)的重載解析過程?(名稱查找、生成候選集、參數匹配、選擇最佳)與普通函數和函數模板的機制在邏輯上是一致的。都涉及模板參數推導(如果是模板成員函數)和轉換序列比較。
?作用域不同:?? 名稱查找發生在類作用域?(MyClass::)內,而不是全局或命名空間作用域。
?實例化觸發點:?? 類模板的實例化(生成類類型)是成員函數調用的前提。成員函數的實例化發生在類實例化之后,并且是按需(惰性)進行的。
?特殊成員:?? 類模板的友元函數可能涉及 ADL,規則更復雜一些。
?總結(類模板):??
類模板本身的實例化(MyClass)會生成一個具體的類類型。
對該類實例的成員函數的調用?(包括構造函數),會觸發一個與函數模板調用非常相似的重載解析和按需實例化過程。
核心區別在于作用域(類作用域)和實例化的層次(先實例化類,再按需實例化成員函數)。
模板的特化
概念: 通常情況下,使用模板可以實現一些與類型無關的代碼,但對于一些特殊類型的可能會得到一些錯誤的結果,需要特殊處理,比如:實現了一個專門用來進行小于比較的函數模板
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; // 可以比較,結果正確Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; // 可以比較,結果正確,內部會調用Date類的operate<賦值重載函數.Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比較,結果錯誤,指針之間的比較沒有意義,要正確比較的邏輯,我們就需要對該模板特化一下.return 0;
}
可以看到,Less絕對多數情況下都可以正常比較,但是在特殊場景下就得到錯誤的結果。上述示例中,p1指向的d1顯然小于p2指向的d2對象,但是Less內部并沒有比較p1和p2指向的對象內容,而比較的是p1和p2指針的地址,這就無法達到預期而錯誤。
此時,就需要對模板進行特化。
模板的特化: 在原模板類的基礎上,針對特殊類型所進行特殊化的實現方式。模板特化中分為函數模板特化與類模板特化。
3. 函數模板特化
函數模板的特化步驟:
- 必須要先有一個基礎的函數模板
- 關鍵字template后面接一對空的尖括號<>
- 函數名后跟一對尖括號,尖括號中指定需要特化的類型
- 函數形參表: 必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
// 函數模板 -- 參數匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
// 對Less函數模板進行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
int main()
{cout << Less(1, 2) << endl;Date d1(2022, 7, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl;Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 調用特化之后的版本,而不走模板生成了return 0;
}
注意:一般情況下如果函數模板遇到模板特化比較復雜,類型不能處理或者處理有誤的情況,通常直接將該函數給出更加簡單方便。
bool Less(Date* left, Date* right)
{return *left < *right;
}
4. 類模板特化
類模板只可以使用顯示的傳類型實例化,不可以隱式的推導類型,因為我們調用構造函數傳參的過程,不一定會傳入類型。
4.1 全特化
全特化即是將模板參數列表中所有的參數都確定化。
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
private:int _d1;char _d2;
};
void TestVector()
{Data<int, int> d1;Data<int, char> d2;
}
4.2 偏特化
偏特化:任何針對模版參數進一步進行條件限制設計的特化版本。偏特化可以從兩個方面。
比如對于以下模板類:
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};//(1). 將模板參數類表中的一部分參數特化:
// 將第二個參數特化為int
template <class T1>
class Data<T1, int>
{
public:Data() { cout << "Data<T1, int>" << endl; }
private:T1 _d1;int _d2;
};//(2). 參數更進一步的限制:
//兩個參數偏特化為指針類型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }private:T1 _d1;T2 _d2;
};
//兩個參數偏特化為引用類型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout << "Data<T1&, T2&>" << endl;}private:const T1& _d1;const T2& _d2;
};
void test2()
{Data<int, double> d2; // 調用基礎的模板 Data<double, int> d1; // 調用特化的int版本Data<int*, int*> d3; // 調用特化的指針版本Data<int&, int&> d4(1, 2); // 調用特化的指針版本
}
類模板的偏特化,可以從兩個方面:
- 將模板參數類表中的一部分參數特化
- 參數更進一步的限制
4.3 類模板特化應用示例
有如下專門用來按照小于比較的類模板Less,使用重載operate()實現像使用函數一樣使用該類:
#include<vector>
#include <algorithm>
using namespace std;
template<class T>
struct Less
{bool operator()(const T& x, const T& y) const{return x < y;}
};int main()
{Date d1(2022, 7, 7);Date d2(2022, 7, 6);Date d3(2022, 7, 8);vector<Date> v1;v1.push_back(d1);v1.push_back(d2);v1.push_back(d3);// 可以直接排序,結果是日期升序sort(v1.begin(), v1.end(), Less<Date>());vector<Date*> v2;v2.push_back(&d1);v2.push_back(&d2);v2.push_back(&d3);// 可以直接排序,結果錯誤日期還不是升序,而v2中放的地址是升序// 此處需要在排序過程中,讓sort比較v2中存放地址指向的日期對象// 但是走Less模板,sort在排序時實際比較的是v2中指針的地址,因此無法達到預期sort(v2.begin(), v2.end(), Less<Date*>());return 0;
}
通過觀察上述程序的結果發現,對于日期對象可以直接排序,并且結果是正確的。但是如果待排序元素是指針,結果就不一定正確。因為:sort最終按照Less模板中方式比較,所以只會比較指針,而不是比較指針指向空間中內容,此時可以使用類版本特化來處理上述問題:
// 對Less類模板按照指針方式特化
template<>
struct Less<Date*>
{bool operator()(Date* x, Date* y) const{return *x < *y;}
};
特化之后,在運行上述代碼,就可以得到正確的結果。
這種通過operate()重載,讓類的使用和函數一樣的,我們稱它為仿函數,仿函數最常用的使用場景是在算法中傳入,代替函數指針使排序順序改變。
為什么函數重載沒有偏特化?
- 因為函數有函數重載,而類沒有,所以函數不需要特化,而類需要。
- 如果允許函數模板偏特化,會導致規則沖突:
template <typename T> void foo(T); // #1 主模板
template <typename T> void foo(T*); // #2 指針的重載
template <typename T> void foo<int>(T); // #3 偽偏特化(如果允許)foo(42); // 應匹配#1還是#3?
foo(&x); // 應匹配#2還是#1的特化?
模板分離編譯
5.什么是分離編譯
我們知道一個C++程序(項目)由若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最后將所有目標文件鏈接起來形成單一的可執行文件,這個過程我們稱為分離編譯模式。
5.1 函數模板的分離編譯
假如有以下場景,模板的聲明與定義分離開,在頭文件中進行聲明,源文件中完成定義:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
解決方法:
- 模板的定義直接放在頭文件中 “xxx.hpp” 或者"xxx.h"里面,這樣在包含頭文件的地方,編譯器就能看到完整的定義,從而在需要時實例化模板。(推薦這種做法)
// a.h
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
- 模板定義的位置顯式實例化,這樣編譯器會針對a.cpp中的顯示實例化,生成對應類型的普通函數實體,這樣就可以像普通的函數一樣,具有外部連接屬性,在最后被main.cpp鏈接的時候找到并使用。
// a.h
template<class T>
T Add(const T& left, const T& right);// a.cpp
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
template int Add<int>(const int&, const int&);
template double Add<double>(const double&, const double&);
template float Add<float>(const float&, const float&);// main.cpp
#include"a.h"
int main()
{Add(1, 2);Add(1.0, 2.0);return 0;
}
推薦第一種做法,因為第一種做法可以最大限度的發揮我們使用模板的優勢,第二種方式有點華而不實了。
5.2 類模板的分離編譯
類模板的分離編譯和函數模板類似。
非類型模板參數
模板參數分類類型形參與非類型形參。
類型形參即:出現在模板參數列表中,跟在class或者typename之類的參數類型名稱。
非類型形參,就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。
// 定義一個模板類型的靜態數組
template<class T, size_t N = 10>
class array
{
public:T& operator[](size_t index) { return _array[index]; }const T& operator[](size_t index)const { return _array[index]; }size_t size()const { return _size; }bool empty()const { return 0 == _size; }private:T _array[N];size_t _size;
};
注意:
- 浮點數、類對象以及字符串是不允許作為非類型模板參數的。
模板優缺
【優點】
- 模板復用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生
- 增強了代碼的靈活性
【缺陷】 - 模板會導致代碼膨脹問題,也會導致編譯時間變長
- 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤
總結
- 泛型編程在C++中就是模板,是一種代碼復用、代碼可維護性的手段,它會根據傳入的參數自動的推導類型。
- 函數模板可以隱式實例化和顯示傳入參數的實例化。
- 函數模板的匹配原則在C++編譯器自動查找函數調用的前提下,根據推導出的模板參數(如 double或 char)去實例化該模板的完整定義?(函數體代碼)。
- 類模板的匹配原則在整體類型的實例化上和函數的過程相似,但是其內部模板成員函數的調用,也依舊遵循模板函數的匹配原則,即作用域中查找和按需求實例化,沒有使用到的模板成員變量和模板成員函數是不會被實例化的。
- 模板我們可以提前按照一些特殊情況寫出一些特殊的模板,讓編譯器在查找的過程中根據優先匹配原則,優先匹配到,特殊的去實例化,即模板的特化。
- 函數模板不可以偏特化,因為已經有函數重載了。
- 類模板可以偏特化,因為它沒有重載。
- 類模板的特化可以分為針對部分參數的類型指定和對參數的進一步限制。
- 模板的分離編譯會導致編譯器掃描不到不同編譯單元之間的模板定義,從而無法根據定義生成最終的代碼,從而找不到類、函數的定義,從而報錯。
- 非類型
本文章為作者的筆記和心得記錄,順便進行知識分享,有任何錯誤請評論指點:)。