一篇文章帶你瞭解C++模板編程詳解

模板初階

泛型編程

在計算機程序設計領域,為瞭避免因數據類型的不同,而被迫重復編寫大量相同業務邏輯的代碼,人們發展的泛型及泛型編程技術。什麼是泛型呢?實質上就是不使用具體數據類型(例如 int、double、float 等),而是使用一種通用類型來進行程序設計的方法,該方法可以大規模的減少程序代碼的編寫量,讓程序員可以集中精力用於業務邏輯的實現。泛型也是一種數據類型,隻不過它是一種用來代替所有類型的“通用類型”

我們通常如何實現一個通用的交換函數呢?

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;
}
......

Swap函數能實現各種類型的變量交換,但是隻要類型不同就需要重新寫一個

使用函數重載雖然可以實現,但是有一下幾個不好的地方:

  • 重載的函數僅僅隻是類型不同,代碼的復用率比較低,隻要有新類型出現時,就需要增加對應的函數
  • 代碼的可維護性比較低,一個出錯可能所有的重載均出錯,那能否告訴編譯器一個模版,讓編譯器根據不同的類型利用該模版來生成代碼呢?

可以的,C++語法中有瞭模板:

函數模板

函數模板概念

所謂函數模板,實際上是建立一個通用函數,它所用到的數據的類型(包括返回值類型、形參類型、局部變量類型)可以不具體指定,而是用一個虛擬的類型來代替(實際上是用一個標識符來占位),等發生函數調用時再根據傳入的實參來逆推出真正的類型。 這個通用函數就稱為 函數模板(Function Template) 。函數模板代表瞭一個函數傢族,該函數模板與類型無關,在使用時被參數化,根據實參類型產生函數的特定類型版本。

函數模板格式

template<typename T1, typename T2,…,typename Tn>
返回值類型 函數名(參數列表){}

template<typename T>
//或者 template<class T>
void Swap(T& x1, T& x2)
{
    T temp = left;
    left = right;
    right = temp;
}

T1,T2等等是什麼類型現在也不確定,一會用的時候才能確定

註意:

typename是用來定義模板參數關鍵字,也可以使用class

函數模板的原理

函數模板本身並不是函數,是編譯器根據調用的參數類型產生特定具體類型函數的模具,所以其實模板就是將本來應該我們做的重復的事情交給瞭編譯器,我們看下面的例子:

template<class T>
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	char A = 'a';
	char B = 'b';
	Swap(A,B);
	return 0;
}

image-20211024163521112

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

然而當我們在寫瞭函數時,不會進入模板函數裡,沒有寫具體的函數時,就會進入模板函數裡,我們看下面的例子:

void Swap(int& x, int& y)
{
	int temp = x;
	x = y;
	y = temp;
}
template<class T>
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	char A = 'a';
	char B = 'b';
	Swap(A,B);
	return 0;
}

我們進行調式:

模板

我們可以看到int類型的交換函數我們寫瞭,調用時調用的是我們寫的,而char類型的我們沒寫,就用瞭模板。

那麼這裡調用的是模板函數嗎?

不是的,實際上這裡會有兩個過程

1、模板推演,推演T的具體類型是什麼

2、推演出T的具體類型後實例化生成具體的函數

上面的代碼實例化生成瞭下面的函數:

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

真正調用的還是兩個函數,但是其中的一個函數不是我們自己寫的,而是我們給瞭編譯器一個模板,然後編譯器進行推演在編譯之前實例化生成三個對應的函數,模板是給編譯器用的,編譯器充當瞭寫函數的工具:

image-20211024161726234

可以看到這裡是調用瞭Swap<char>函數

在C++當中,其實內置類型也可以像自定義類型那樣這樣初始化:

int a(1);
int(2);//匿名

image-20211024162224522

void Swap(T& x1, T& x2)
{
    T temp(x1);
    x1 = x2;
    x2 = x1;
}

所以模板還可以這樣寫,可以使內置類型和自定義類型兼容:

void Swap(T& x1, T& x2)
{
    T temp(x1);
    x1 = x2;
    x2 = x1;
}

我們來具體看一看函數模板的實例化:

函數模板的實例化

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

隱式實例化:讓編譯器根據實參推演模板參數的實際類型

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);
// 此時有兩種處理方式:1. 用戶自己來強制轉化 2. 使用顯式實例化
    Add(a1, d2);
    return 0;
}

該語句是不能夠通過編譯的,因為在編譯期間,當編譯器看到該實例化時,用a1去推T是int,而用d2去推是double,但是模板參數列表裡隻有一個T,編譯器不能明確該T是int還是double,T是不明確的,所以編譯器會報錯

那麼怎麼處理呢?

解決方式:

1、調用者自己強制轉換

//實參去推演形參的類型
Add(a1, (int)d2);
Add((double)a1,d2);

這裡可以將d2先強制類型轉換,然後再進行推演;或者將a1先強制類型轉換再進行推演

2、使用顯式實例化

//實參不需要去推演形參的類型,顯式實例化指定T的類型
Add<int>(a1, d2);
Add<double>(a1,d2);

這種方式是顯式實例化指定T的類型

顯式實例化在哪種場景可用呢?看下面的這種場景:

class A
{
    A(int a=0):_a(a)
    {}
private:
    int _a;
};
template<class T>
T func(int x)
{
    T a(x);
    return a;
}
int main()
{
    func<A>(1);
    func<int>(2);
    return 0;
}

有些函數模板裡面參數中沒用模板參數,函數體內才有用到模板參數,此時就無法通過參數去推演T的類型,這時隻能顯示實例化

上面我們提瞭一點模板參數的匹配原則,下面我們具體看看模板參數的匹配原則:

模板參數的匹配原則

 一個非模板函數可以和一個同名的函數模板同時存在,此時如果調用地方參數與非模板函數完全匹配,則會調用非模板函數

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函數
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
    Add(1,2);//調用自己的函數
    return 0;
}

Add(1,2)參數是int類型,而我們有現成的int參數的Add函數,所以有現成的就用現成的,編譯器也會偷懶

那麼如果我們想讓這裡調用必須用模板呢?顯式實例化:

Add<int>(1,2);

這樣編譯器就強制會用模板去實例化函數

一個非模板函數可以和一個同名的函數模板同時存在,此時如果調用地方參數與非模板函數不完全匹配,則會優先使用模板實例化函數

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函數
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
    Add(1.1,2.2);//使用模板實例化函數
    return 0;
}

模板匹配原則總結:

有現成完全匹配的,那就直接調用,沒有現成調用的,實例化模板生成,如果有需要轉換類型才能匹配的函數(也就是不完全匹配),那麼它會優先選擇去實例化模板生成。

優先級:

完全匹配>模板>轉換類型匹配

類模板

類模板的定義格式

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

我們來看一個類模板的使用場景:

typedef int STDateType;
class Stack
{
private:
    STDateType* _a;
    int _top;
    int _capacity;
};
int main()
{
    Stack st1;
    Stack st2;
    return 0;
}

這是我們定義的棧數據結構,我們創建瞭兩個棧對象,但是現在st1和st2的存儲數據的類型都是int,要是想轉換數據類型呢?

typedef double STDateType;

我們這樣就轉換瞭,但是我們要是想st1為int,st2為double呢:

Stack st1;//int
Stack st2;//double

此時需要寫多個類,名字還得不一樣,如下:

typedef int STDateType1;
typedef double STDateType2;
class IntStack
{
private:
    STDateType1* _a;
    int _top;
    int _capacity;
};
class DoubleStack
{
private:
    STDateType2* _a;
    int _top;
    int _capacity;
};

這樣太麻煩瞭,那麼什麼辦法可以解決呢?類模板可以解決:

//類模板
template<class T>
class Stack
{
private:
    T* _a;
    int _top;
    int _capaticy;
};
int main()
{
    //類模板的使用都是顯式實例化
    Stack<double> st1;
    Stack<int> st2;
    return 0;
}

註意:Stack不是具體的類,是編譯器根據被實例化的類型生成具體類的模具

類模板的實例化

//類模板
template<class T>
class Stack
{
public:
    Stack(int capacity = 4)
        :_a(new T(capacity))
         ,_top(0)
         ,_capacity(capacity)
        {}
    ~Stack()
    {
        delete[] _a;
        _a = nullptr;
        _top = _capacity = 0;
    }
    void Push(const T& x)
    {
        //...
    }
private:
    T* _a;
    int _top;
    int _capaticy;
};
int main()
{
    //類模板的使用都是顯式實例化
    Stack<double> st1;
    Stack<int> st2;
    return 0;
}

註意:類模板的使用都是顯式實例化

假設我們想類裡面聲明和類外面定義成員函數呢?

//類模板
template<class T>
class Stack
{
public:
    Stack(int capacity = 4)
        :_a(new T(capacity))
         ,_top(0)
         ,_capacity(capacity)
        {}
    ~Stack()
    {
        delete[] _a;
        _a = nullptr;
        _top = _capacity = 0;
    }
    //假設我們想類裡面聲明和定義分離呢?
    void Push(const T& x);
private:
    T* _a;
    int _top;
    int _capaticy;
};
//在類外面定義
template<class T>
void Stack<T>::Push(const T& x);
{
    //...
}
int main()
{
    //類模板的使用都是顯式實例化
    Stack<TreeNode*> st1;
    Stack<int> st2;
    return 0;
}
//在類外面定義
template<class T>
void Stack<T>::Push(const T& x);
{
    //...
}

在類外面定義我們必須要加模板的關鍵字,以及需要在實現的函數前面表明域Stack<T>。普通類,類名就是類型,對於類模板,類名不是類型,類型是Stack<T>,需要寫指定

註意:

模板不支持把聲明寫到.h,定義寫到.cpp,這種聲明和定義分開實現的方式,會出現鏈接錯誤

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: