1. 模板的概念
C++的模板(Templates)是泛型编程的基础,它允许编写与类型无关的代码,从而提高代码的复用性和灵活性。通过模板,你可以编写一种通用的函数或类,而不需要为每种特定的数据类型单独定义多个函数或类。这种机制主要分为函数模板和类模板。
模板就是建立通用的模具,大大提高复用性
2. 函数模板
函数模板可以用于编写可以接受不同类型参数的函数。你定义一次模板,编译器会根据你调用时的参数类型自动生成相应的函数版本。其基本语法如下:
template <typename T>
T add(T a, T b) {return a + b;
}
解释:
template
– 声明创建模板typename
– 表明其后面的符号是一种数据类型,可以用class
代替T
– 通用的数据类型,名称可以替换,通常为大写字母
这里的template <typename T>
声明了一个模板,T
是模板参数,表示一种泛型类型。函数add
会根据实际调用时的参数类型推导出T
,比如当你传递两个整数时,T
是int
类型;当你传递两个浮点数时,T
是float
类型:
int x = add(5, 3); // T是int
double y = add(5.2, 3.8); // T是double
2.1 两种方式使用函数模板
- 自动类型推导:
myswap(a,b);
- 显示指定类型:
myswap<int>(a,b);
注意事项:
- 自动类型推导,必须推导出一致的数据类型
T
,才可以使用 - 模板必须要确定出
T
的数据类型,才可以使用
2.2 普通函数与函数模板区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
2.3 普通函数与函数模板的调用规则
-
如果函数模板和普通函数都可以实现(同名且参数个数相同),优先调用普通函数
-
可以通过空模板参数列表来强制调用函数模板
void myPrint(int a,int b){cout<<"调用普通函数"<<endl; }template<typename T> void myPrint(T a,T b){cout<<"调用模板函数"<<endl; }int main(){myPrint(a,b); //优先调用普通函数myPrint<>(a,b); //通过空模板参数列表强制调用函数模板 }
-
函数模板也可以发生重载
-
如果函数模板可以产生更好的匹配,优先调用函数模板
2.4 模板的局限性
模板并不是万能的,有些特定数据类型,需要用具体化方式做特殊实现
3. 类模板
3.1 基本概念
类模板与函数模板类似,但用于类的定义。使用类模板,可以创建一个通用的类,允许在实例化时为类指定不同的数据类型。其基本语法如下:
template <typename T>
class Box {
private:T value;
public:Box(T v) : value(v) {}T getValue() { return value; }
};
在上面的示例中,Box
是一个类模板,T
是一个泛型类型。你可以在创建Box
对象时为T
指定不同的类型:
Box<int> intBox(123); // T是int
Box<double> doubleBox(45.6); // T是double
3.2 类模板与函数模板区别
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
template<class NameType,class AgeType = int> //模板参数列表有默认参数
class Person {
public:Person(NameType name, AgeType age) {this->name = name;this->age = age;}void showPerson() {cout << "姓名:" << name << ",年龄:" << age << endl;}NameType name;AgeType age;
};
int main()
{//Person p("孙悟空",1000); //错误,无法用自动类型推导//Person<string, int> p("孙悟空",1000); //正确,只能用显示指定类型Person<string> p("猪八戒",999); //由于模板参数列表有默认参数,所以只需要指出string即可,函数模板不能这么用p.showPerson();return 0;
}
3.3 类模板中成员函数创建时机
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
3.4 类模板对象做函数参数
template<class T1,class T2>
class Person{
public:T1 name;T2 age;Person(T1 name,T2 age){this->name = name;this->age = age;}void showPerson(){cout << "name:"<<this->name<<" age:"<<this->age<<endl;}
};
类模板实例化出的对象,向函数传参的方式
-
指定传入的类型 – 直接显示对象的数据类型(使用广泛)
void printPerson1(Person<string,int>&p){ //指定传入的类型p.showPerson(); } int main() {Person<string,int> p("孙悟空",18);printPerson1(p); }
-
参数模板化 - 将对象中的参数变为模板进行传递
template<class T1,class T2> void printPerson2(Person<T1,T2>&p){ //参数模板化p.showPerson();cout << "T1 的类型为:" << typeid(T1).name() << endl; //得到T1的类型 } int main() {Person<string,int> p("白骨精",18);printPerson2(p); }
-
整个类模板化 - 将这个对象类型模板化进行传递
template<class T> void printPerson3( T &p ){ //整个类模板化p.showPerson(); } int main() {Person<string,int> p("牛魔王",18);printPerson3(p); }
3.5 类模板与继承
注意:
-
当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中
T
的类型template<class T> class Base {T m; }; //class Son : public Base { };//报错:必须要知道父类中的T类型才能继承给子类 class Son : public Base<int> {}; int main() {Son s1; }
-
如果不指定,编译器无法给子类分配内存
-
如果想灵活指定出父类中
T
的类型,子类也需变为类模板template<class T> class Base {T m; };template<class T1,class T2> class Son2 : public Base<T1> {T2 obj; };int main() {Son2<int,char> S2; }
3.6 类模板成员函数类外实现
template<class T1,class T2>
class Person {
public:Person(T1 name, T2 age);void showPerson();T1 name;T2 age;
};template<class T1,class T2>
Person<T1,T2>::Person(T1 name, T2 age) //注意Person后必须写一个模板参数列表
{this->name = name;this->age = age;
}template<class T1, class T2>
void Person<T1, T2>::showPerson() // //注意Person后必须写一个模板参数列表
{cout << "name:" << this->name << " age:" << this->age << endl;
}int main() {Person<string,int> p("沙和尚",18);p.showPerson();
}
3.7 类模板分文件编写
类模板成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决方法:
- 方法一:直接包含
.cpp
源文件 - 方法二:将函数声明和实现写到同一个文件中,并更改后缀名为
.hpp
,hpp
是约定的名称,并不是强制
3.8 类模板与友元
全局函数类内实现:直接在类内声明友元即可
全局函数类外实现:需要提前让编译器知道全局函数的存在
4. class和typename
在C++模板的声明中,class
和typename
都是用于定义类型参数的关键字,它们的功能几乎完全相同。也就是说,在模板的上下文中,使用class
和typename
来表示模板参数时,它们是可以互换的。
template <class T>
T add(T a, T b) {return a + b;
}template <typename T>
T multiply(T a, T b) {return a * b;
}
上面两个模板函数,一个使用class
声明类型参数,另一个使用typename
,但它们的效果完全相同。T
在这两种情况下都表示一个类型参数,编译器会根据你传递的参数类型来推导T
的具体类型。
区别:
尽管class
和typename
在大多数情况下是可以互换的,但它们之间有一些细微的区别和约定:
-
历史原因:在最早的C++标准中,
class
是最早引入模板时用来定义类型参数的关键字。后来,C++标准委员会为了更加语义化和明确,才引入了typename
。因此,class
作为模板类型参数的关键字更多是历史遗留,而typename
则从语义上更符合“表示类型”的含义。 -
在嵌套依赖类型中,必须使用
typename
:在某些复杂的模板表达式中,特别是涉及到嵌套类型时,typename
是必须的。例如,处理模板参数中包含的嵌套类型时,你必须用typename
来明确告知编译器该嵌套依赖的标识符是一个类型,而不是一个变量或其他符号。例如:
template <typename T> void func() {typename T::value_type val; // 必须用typename,表明T::value_type是一个类型 }
在这里,
typename
是必需的,因为T::value_type
可能是一个类型名称。编译器需要通过typename
来确认这个符号确实是一个类型,而不是某个成员变量。
总结:
虽然class
和typename
可以在模板参数中互换,但很多C++开发者倾向于使用typename
,因为它更能直观地表达模板参数是一个“类型”的含义。class
这个关键字容易让人联想到类,而不是类型(尽管类也是一种类型)。