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;
}
有的兄弟(集美)有的!
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
PS:泛型编程泛型编程,意思就是针对广泛的类型进行的编程,不是只针对单一类型编程。
1. 函数模板
1.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

1.2 函数模板格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
假设我们想要实现一个数据结构 vector ,vector 的多个对象,v1 存 int,v2 存 float,等等。在没有模板之前,我们得针对各个类型写很多个 vector 类。而这些函数和类,逻辑都是一样的,只是处理的对象类型不同。
Vector 类模板声明
-
template关键字用于声明模板 -
<class T1, class T2, ...>是模板参数列表,可以使用class或typename关键字template<class T>//注意:class是用来定义模板参数关键字 //也可以使用typename(切记:不能使用struct代替class) void swap(T& x1, T& x2) { T x = x1; x1 = x2; x2 = x; } int main() { int a = 0, b = 1; Swap(a, b); float c = 1.11, d = 2.22; Swap(c, d); char e = 'e', f = 'f'; Swap(e, f); return 0; }1.3 模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
-
所以我们在调用的时候,调用的并不是同一个函数
模板的实例化:
在编译器编译阶段,使用模板函数,编译器需要根据传入的实参类型来推演生成对应的函数以供调用。
例子:当double类型的函数使用该模板时,编译器通过传入的实参,会将T确定为double类型,然后生成一份专门处理double类型的代码,对于其他类型也是如此
ps:不是在调用时生成,而是在编译前就生成了,模板实例化是在预编译阶段进行的,编译器不会把模板的代码编译放入代码段,而是把模板实例化成对应的函数和类,再把这些函数和类编译成指令放进进程代码段
1.4 函数模板的实例化
1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
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); reuturn 0; }但是我们不能在 main 函数中调用 Add(a1, d1)。
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型,通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。
(注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅)
此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);2. 显式实例化:在函数名后的<>中指定模板参数的实际类型
int main(void) { int a = 10; double b = 20.0; // 显式实例化 Add<int>(a, b); return 0; }如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
1.5 模板参数的匹配原则
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理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. 类模板
2.1 类模板的定义格式与实例化
template<class T1, class T2, ..., class Tn> class 类模板名 { // 类内成员定义 }; -
template关键字用于声明模板 -
<class T1, class T2, ...>是模板参数列表,可以使用class或typename关键字 -
类模板不是具体的类,而是编译器生成具体类的"模具"
2.2类模板的简单实现示例
假设我们想要实现一个数据结构 vector ,vector 的多个对象,v1 存 int,v2 存 float,等等。在没有模板之前,我们得针对各个类型写很多个 vector 类。而这些函数和类,逻辑都是一样的,只是处理的对象类型不同。
Vector 类模板声明
template<class T>
class vector // 动态增长的数组
{
public:
vector(); // 默认构造函数
vector(size_t n); // 带参数的构造函数
~vector(); // 析构函数
// 成员函数声明
// 类里面声明,类外面定义
// 模板传参能用引用就用引用,减少传值
void push_back(const T& x);
void push_front(const T& x);
void pop(const T& x);
size_t size();
T operator[](size_t i);
private:
T* _pData; // 存储数据的指针
size_t _size; // 当前元素数量
size_t _capacity; // 当前容量
};
构造函数实现
// 默认构造函数
template<class T>
vector<T>::vector()
: _pData(nullptr)
, _size(0)
, _capacity(0)
{}
// 带参数的构造函数
template<class T>
vector<T>::vector(size_t n)
: _pData(new T[n])
, _size(0)
, _capacity(n)
{}
析构函数实现
template<class T>
vector<T>::~vector()
{
delete[] _pData;
_pData = nullptr;
_size = 0;
_capacity = 0;
}
成员函数的实现
push_back 方法实现
template<class T>
void vector<T>::push_back(const T& x)
{
// 如果空间不足,需要进行增容
if (_size == _capacity)
{
// 计算新容量
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
// 分配新空间
T* tmp = new T[newcapacity];
// 拷贝旧数据到新空间
if (_pData != nullptr)
{
// 注意:memcpy 拷贝自定义类型时可能导致浅拷贝问题
// 这里只是简单实现,不做探究
memcpy(tmp, _pData, sizeof(T) * _size);
delete[] _pData;
}
// 更新指针和容量
_pData = tmp;
_capacity = newcapacity;
}
// 添加新元素
_pData[_size] = x;
++_size;
}
其他成员函数实现
// 获取元素数量
template<class T>
size_t vector<T>::size()
{
return _size;
}
// 下标运算符重载
template<class T>
T vector<T>::operator[](size_t i)
{
assert(i < _size);
return _pData[i];
}
2.3 类模板的使用
实例化类模板
int main()
{
// 实例化 int 类型的 vector
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
// 遍历 int vector
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i] << " ";
}
cout << endl;
// 实例化 float 类型的 vector
vector<float> f;
f.push_back(1.2);
f.push_back(2.3);
f.push_back(3.4);
f.push_back(4.5);
// 遍历 float vector
for (size_t i = 0; i < f.size(); ++i)
{
cout << f[i] << " ";
}
cout << endl;
return 0;
}
2.4 重要注意事项
1. 类外定义成员函数的语法
在类模板外部定义成员函数时,需要在每个函数前加上模板声明,并使用类名<T>的形式:
template<class T>
void vector<T>::push_back(const T& x)
{
// 函数实现
}
2. 模板参数传递建议
-
尽量使用引用传递,避免不必要的拷贝
-
对于不会修改的参数,使用 const 引用
3. 嵌套依赖类型与 typename 关键字
什么是嵌套依赖类型?
嵌套依赖类型(Nested Dependent Type)是指在一个模板中,一个类型名称依赖于模板参数,并且是该模板参数内部的嵌套类型。
具体来说,当一个类型满足以下两个条件时,它就是嵌套依赖类型:
-
它嵌套在另一个类型内部(如
Container::iterator) -
外层类型依赖于模板参数(如
T::inner_type或Container<T>::iterator)
比如 list 部分代码list类中:
template <class T>
void print_list(const list<T>& l)
{
typename list<T>::const_iterator it = l.begin(); // 这里 list<T> 依赖于模板参数 T
while (it != l.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
list<T>::const_iterator 就是一个嵌套依赖类型,因为:
-
const_iterator嵌套在list<T>内部 -
list<T>依赖于模板参数T
为什么需要 typename 关键字?
C++ 编译器在解析模板时,需要知道一个依赖名称是类型还是值。由于模板可能被特化,编译器在实例化之前无法确定依赖名称的性质。
使用 typename 关键字可以明确告诉编译器:"这个依赖名称是一个类型"。
其他需要使用 typename 的情况
除了在函数内部声明嵌套依赖类型的变量外,还有以下几种情况需要使用 typename:
- 作为函数返回类型
template <class T> typename T::value_type get_first(const T& container) { return *container.begin(); } -
作为函数参数类型
template <class T> void process_element(typename T::element_type elem) { // 处理元素 } -
在类模板中声明成员变量类型
template <class Container> class Wrapper { public: typename Container::value_type first_element; // 使用 typename }; -
使用模板模板参数中的嵌套类型
template <template <class> class Container, class T> void print_size(const Container<T>& c) { typename Container<T>::size_type s = c.size(); // 使用 typename cout << s << endl; } -
在继承中指定基类类型
template <class T> class Derived : public typename T::BaseType { // 使用 typename // ... };不需要使用 typename 的情况
以下情况不需要使用
typename:
-
非依赖类型
template <class T> void func() { std::string s; // 不需要 typename,因为 std::string 不依赖于 T int i; // 不需要 typename } -
基类列表和成员初始化列表
template <class T> class Derived : public T::Nested { // 这里不需要 typename public: Derived() : T::Nested() {} // 这里也不需要 typename };代码示例
#include <iostream> #include <vector> #include <list> using namespace std; // 1. 函数返回类型中使用 typename template <class Container> typename Container::value_type get_first(const Container& c) { return *c.begin(); } // 2. 函数参数中使用 typename template <class Container> void print_element(typename Container::value_type elem) { cout << elem << " "; } // 3. 类模板中使用 typename template <class Container> class ContainerInfo { public: typedef typename Container::value_type value_type; // 使用 typename typedef typename Container::iterator iterator; // 使用 typename static void print_info() { cout << "Container value_type: " << typeid(value_type).name() << endl; } }; int main() { vector<int> vec = {1, 2, 3}; list<double> lst = {1.1, 2.2, 3.3}; // 测试函数返回类型中的 typename cout << "First element of vector: " << get_first(vec) << endl; cout << "First element of list: " << get_first(lst) << endl; // 测试类模板中的 typename ContainerInfo<vector<int>>::print_info(); ContainerInfo<list<double>>::print_info(); return 0; }总结
-
嵌套依赖类型是指依赖于模板参数的嵌套类型
-
使用
typename关键字告诉编译器某个依赖名称是类型而不是值 -
在函数返回类型、参数类型、变量声明等地方都可能需要使用
typename -
只有在处理依赖类型时才需要使用
typename,非依赖类型不需要
更多推荐



所有评论(0)