C++模板全方位深入解讀

1.泛型編程

如何實現一個通用的交換函數?

這點函數重載可以做到,比如一下Swap函數的重載,分別重載瞭倆種不同參數類型的Swap

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
void Swap(char& x, char& y)
{
	char tmp = x;
	x = y;
	y = tmp;
}

但是這也帶來瞭幾點不好的地方:

1.重載的函數僅僅是類型不同,代碼的復用率比較低,隻要有新類型出現,就需要增加對應的函數

2.代碼的可維護性比較低,一個出錯可能所有的重載均出錯

那麼有什麼好的解決方法嗎?我們能否告訴編譯器一個模子,讓編譯器根據不同類型利用該模子自己去生成相應的代碼呢?

當然能,這就是泛型編程,即編寫與類型無關的通用代碼,這是代碼復用的一種手段。而模板是泛型編程的基礎。模板分為兩種,函數模板和類模板。

2.函數模板

概念

函數模板代表瞭一個函數傢族,該函數模板與類型無關,在使用時被參數化,根據實參類型產生函數的特定類型版本。

函數模板的格式

template<typename T1, typename T2,……,typename Tn>

即:返回值類型 函數名(參數列表){}

其中typename可以改成class(不能用struct)

例:

template <typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

函數模板的原理

函數模板是一個藍圖,它本身並不是函數,是編譯器用使用方式產生特定具體類型函數的模具。所以其實模板就是將本來應該我們做的重復的事情交給瞭編譯器。

在編譯器編譯階段,對於模板函數的使用,編譯器需要根據傳入的實參類型來推演生成對應類型的函數以供調用。比如:當用double類型使用函數模板時,編譯器通過對實參類型的推演,將T確定為double類型,然後產生一份專門處理double類型的代碼,對於字符類型也是如此。

函數模板的實例化

用不同類型的參數使用函數模板時,稱為函數模板的實例化。模板參數的實例化分為兩種:隱式實例化和顯式實例化。

隱式實例化

讓編譯器自己推演函數參數的類型。

需要註意的是隱式實例化的參數一定要匹配,否則可能產生分歧導致編譯器無法識別。比如:

template<typename T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, b1 = 20;
	double a2 = 10.0, b2 = 20.0;
	Add(a1, b2);
	return 0;
}

編譯器報錯,該語句不能通過編譯,因為無法確定T是int還是double。

如何處理?有兩種方式:

1.強制類型轉換!但值得註意的是,強轉會產生臨時變量,臨時變量是具有常性的,需要const修飾一下!

2.使用顯式實例化

顯式實例化

在函數名後的< >中指定模板參數的實際類型。

template<typename T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, b1 = 20;
	double a2 = 10.0, b2 = 20.0;
	cout<<Add<int>(a1, b2)<<endl;
	return 0;
}

模板參數的匹配原則

1.一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化為這個非模板函數。

2.對於非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數, 那麼將選擇模板

3.模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換

  • 如果有定義出來的函數,且類型完全匹配調用時實參類型,則執行定義出來的函數.如果定義出來的函數,不符合,則執行模板推演.
  • 這部分沒啥難度,不再舉例說明,總的來說,對於函數調用的優先級就是:完全匹配 >模板匹配 >轉換匹配。

3.類模板

(1) 類模板的定義格式

template<class T1, class T2, ..., class Tn>
class 類模板名
{
 // 類內成員定義
}; 

註意:

類模板中函數放在類外進行定義時,需要加模板參數列表;

template <typename T>
class Stack
{
public:
	Stack(int capacity = 4)
		:_a(new T[capacity])
		,_top(0)
		,_capacity(capacity)
	{}
	~Stack()
	{
		delete[] _a;
		_top = _capacity = 0;
	}
	void Push(T x);
private:
	T* _a;
	int _top;
	int _capacity;
};
// 註意:類模板中函數放在類外進行定義時,需要加模板參數列表
template <typename T>
void Stack<T>::Push(T x)
{
	if (_top == _capacity)
	{
		_capacity *= 2;
		T* tmp = (T*)realloc(_a, sizeof(int) * _capacity);
		if (tmp == nullptr)
		{
			cout << "realloc fail" << endl;
			exit(-1);
		}
		_a = tmp;
	}
	_a[_top++] = x;
}

對於普通類,類名就是類型;對於類模板,類名不是類型,類型是Class < T >

(2) 類模板的實例化

類模板實例化與函數模板實例化不同,類模板實例化需要在類模板名字後跟 < >,然後將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果才是真正的類。

int main()
{
	// Stack隻是類名,不是類型,Stack<int>才是類型
	Stack<int> s1;
	Stack<char> s2;
	return 0;
}

4.非類型模板參數

類型參數:就是在模板的參數列表中在class後面加上參數的類型名稱。

非類型參數:就是用一個常量作為類(函數)模板的一個參數,在類(函數)模板中可將該參數當成常量來使用。

註意兩點:

1.浮點數、類對象以及字符串是不允許作為非類型模板參數的。

2.非類型的模板參數必須在編譯期就能確認結果。

template<class T =int, size_t N = 10>
class array
	{
		 private:
	 	 T _array[N];
		 size_t _size;
	 }

5.模板特化

(1)函數模板的特化

當針對某一情景或者某一類型,函數模板無法滿足要求,模板需要有特殊的處理,這個時候就需要用到模板的特化。

比如咱們要比較兩個字符串是否相同:

template<class T>
bool IsEqual(T str1, T str2)
{
	return str1 == str2;
}
int main()
{
	char str1[] = "hello";
	char str2[] = "hello";
	if (IsEqual(str1, str2))
		cout << "true";
	else
		cout << "false";
}

上述代碼輸出false, 不滿足咱們的要求, 因為調用函數IsEqual()時傳遞過去的是兩個char*類型,他們兩個比較的不是字符串的內容,而是指針的地址,所以返回false。

此時模板特化派上用場瞭:如果要比較char*, 可以用strcmp來對這個情況進行特殊處理

template<>
bool IsEqual<char*>(char* str1, char* str2)
{
	return strcmp(str1, str2) == 0;
}

此時就返回true, 符合預期瞭。

函數模板的特化步驟:

  • 必須要先有一個基礎的函數模板
  • 關鍵字template後面接一對空的尖括號<>
  • 函數名後跟一對尖括號,尖括號中指定需要特化的類型
  • 函數形參表: 必須要和模板函數的基礎參數類型完全相同

(2)類模板的特化

類也是同理,如果需要有特殊情景也需要特化處理

以如下類舉例(後邊全特化、偏特化都針對它):

template<class T1, class T2>
class test
{
public:
	test()
	{
		cout << "test<T1, T2>" << endl;
	}
private:
	T1 _x;
	T2 _y;
};

全特化

全特化即是將模板參數列表中所有的參數都確定化。

例如:

這裡對test<int,double>版本特化

template<>
class test<int, double>
{
public:
	test()
	{
		cout << "test<int, double>" << endl;
	}
private:
	int _x;
	double _y;
};
int main()
{
	test<double, double> t1;
	test<int, double> t2;
}

偏特化

偏特化即是任何針對模版參數進一步進行條件限制設計的特化版本

偏特化有兩種表現方式,一種是部分參數特化,一種是參數修飾特化

部分參數特化

這裡對第二個參數特化,隻要第二個參數是double就會調用對應特化版本

template<class T1>
class test<T1, double>
{
public:
	test()
	{
		cout << "test<T1, double>" << endl;
	}
private:
	T1 _x;
	double _y;
};
int main()
{
	test<double, double> t1;
	test<int, double> t2;
}

參數修飾特化

比如用指針或者引用來修飾類型,也可以進行特化

template<class T1, class T2>
class test<T1*, T2*>
{
public:
	test()
	{
		cout << "test<T1*, T2*>" << endl;
	}
private:
	T1* _x;
	T2* _y;
};
int main()
{
	test<int*, double*> t;
}

6.模板的分離編譯

對於一個代碼量比較多的項目,通常都會采用聲明與定義分離的方法,比如在頭文件進行聲明,在源文件完成代碼的實現,最後通過鏈接的方法鏈接成單一的可執行文件。但是C++的編譯器卻不支持模板的分離編譯,一旦進行分離編譯,就會出現鏈接錯誤。

問題分析

//頭文件a.h
template<class T>
bool IsEqual(const T& str1, const T& str2);
-------------
//源文件a.cpp
template<class T>
bool IsEqual(const T& str1, const T& str2)
{
	return str1 == str2;
}
--------------
//test.c
#include<iostream>
#include"a.h"
using namespace std;
int main()
{
	cout << IsEqual(3, 5);
	cout << IsEqual('a', 'b');
}

這裡看上去是沒有問題的,但是涉及到瞭模板的實例化規則。

當主函數調用這個函數的時候他就會去頭文件中找到函數的聲明,再通過聲明找到a.h中的實現。

但是對於模板卻並不會這樣,因為上一章說過,模板的實例化隻會在其第一次使用的時候才會進行,例如這裡IsEqual(3, 5),他就會去頭文件中尋找,但是頭文件中隻有聲明,沒有定義,無法將其實例化。他又想通過找到a.cpp中的函數定義來進行實例化,但是遺憾的是,a.cpp中隻有IsEqual(const T& str1, const T& str2)的定義,沒有IsEqual(const int & str1, const int T& str2),因為在a.cpp中並沒有使用到該類型的實例,所以自然也不會為其實例化出來,這時test.cpp中就根本無法找到這個函數的實現,就導致瞭鏈接失敗。

解決方法:

這個問題其實沒有什麼完美的解決方法

  • 將聲明和定義放到同一個頭文件中。
  • 類模板顯式實例化。

到此這篇關於C++模板全方位深入解讀的文章就介紹到這瞭,更多相關C++模板內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: