0. 模板
C++中除了有面向对象的思想,还有泛型编程的思想,泛型编程利用的主要技术就是模板。
C++中两种模板:函数模板、类模板
语法:
template<typename T>
函数声明或定义
含义:
- template 固定的,声明创建模板
- typename 表明后边的T是一种数据类型,也可以用class代替
- T 类似形参,任意的合法字符都可以,习惯上用 T
模板有两种类型推导的方式,一种是编译器自动推导,另外一种是程序猿手动指定:
#include <iostream>
using namespace std;
template<typename T>
void func(T &a, T &b){
T s = a;
a = b;
b = s;
}
int main(){
int a = 10, s = 9;
// func(a, s); // 自动类型推导
// func<int>(a, s); // 指定类型推导
cout << a << endl;
cout << s << endl;
return 0;
}
1. 函数模板注意事项
- 自动类型推导,必须推导出一致的数据类型才可以使用。
- 模板必须要确定 T 的数据类型,才可以使用。
eg:二分排序的模板实现
template<typename T>
void mySort(T a[], int len, int l, int r){
// quickSort
if(l >= r) return ;
int x = a[(l + r) / 2];
int i = l - 1, j = r + 1;
while(i < j){
while(a[++i] < x);
while(a[--j] > x);
if(i < j) swap(a[i], a[j]);
}
mySort(a, len, l, j);
mySort(a, len, j + 1, r);
}
2. 普通函数与函数模板对比
2.1 区别
- 普通函数调用时,参数可以发生自动类型转换(隐式转换)
- 函数模板,在自动类型推导形式下,不会自动类型转换。
- 函数模板,在指定类型形式下,会发生自动类型转换。
2.2 调用规则
- 如果普通函数和函数模板同时存在,则会调用普通函数,如果普通函数只有声明,没有实现,那也不会去调用函数模板,会直接报错
- 可以用过空模板参数列表,强制调用函数模板
- 函数模板也可以重载
- 如果函数模板可以更好的匹配调用,会优先调用函数模板
#include <iostream>
using namespace std;
void fuc(int a, int b){
cout << "普通" << endl;
}
template<class T>
void fuc(T a, T b){
cout << "模板" << endl;
}
template<class T> // 重载函数模板
void fuc(T a, T b, T c){
cout << "模板 重载" << endl;
}
void test(){
int a = 1;
int b = 2;
fuc(a, b); // 调用普通函数
fuc<>(a, b); // 空模板参数列表,调用函数模板
fuc<>(a, b, 1); // 模板 重载
char c1 = 'a';
char c2 = 's';
fuc(c1, c2); // 传入char型,普通函数需要做自动类型转换,而函数模板不需要转换,所以优先调用函数模板。
}
int main(){
test();
return 0;
}
3. 模板的局限性
比如模板是一个赋值操作,那么传入的是数组的话,就不正确了。
再比如,比较两个对象的大小关系,类型不确定,比较方式也不确定。
为了解决上边问题,C++提供了模板的重载,可以为特定的类型提供具体化的模板(当然,也可以在重载特定类型的运算符)
语法:
template<class T>
void fuc(T a, T b){
cout << "原模板" << endl;
}
// test是自定义的数据类型
// 这里将函数模板为test数据类型具体化,如果传入是test类型的话,优先调用具体化的模板
template<> void fuc(test a, test b){
// ... code
}
4. 类模板
作用:建立一个通用类,类的成员的数据类型可以不指定,用一个虚拟的类型来代表。
语法:和函数模板一样
template<typename T>
类
例程:
#include <iostream>
#include <string>
using namespace std;
template<class nameType, class ageType>
class Person{
public:
Person(nameType name, ageType age) : m_name(name), m_age(age){
};
void print(){
cout << m_name << endl;
cout << m_age << endl;
}
string m_name;
int m_age;
};
int main(){
Person<string, int> a("张三", 18);
a.print();
return 0;
}
5. 类模板与函数模板区别
- 类模板没有自动类型推导,必须指定类型。
- 类模板在参数列表中,可以有默认参数,在C++11标准以后,函数模板也可以有默认参数了。
上边的代码可以改成这:
template<class nameType = string, class ageType = int>
6. 类模板中成员函数创建的时机
类模板中的成员函数与普通类的成员函数创建时机有所区别:
- 普通类的成员函数一开始就可以创建。
- 类模板的成员函数在调用时才创建。
类模板的成员函数在调用时才创建的原因如下:
因为类中的某些类型并不确定,传入的类型到底能不能执行成员函数中的动作也不知道,所以不能提前创建。
#include <iostream>
#include <string>
using namespace std;
class Person1{
public:
void f1(){
cout << "f1" << endl;
}
};
class Person2{
public:
void f2(){
cout << "f2" << endl;
}
};
template<class T>
class tt{
public:
T obj;
void f1(){
obj.f1();
}
void f2(){
obj.f2();
}
};
int main(){
tt<Person1> s;
s.f1(); // 合法
s.f2(); // 不合法 因为类模板指定的类型是Person1,而该类型没有f2的方法。
return 0;
}
7. 类模板实例化的对象 做 函数参数
函数参数有三种类型进行接收:
- 指定类型 : 对象是什么类型,直接将该类型作为函数参数
- 把类模板的参数 模板化: 将对象中的参数变为模板
- 把 类模板整体 模板化
第一种在实际开发中最常用,因为后两种需要函数模板配合类模板,麻烦。
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class Person{
public:
T1 m_name;
T2 m_age;
Person(T1 name, T2 age){
m_name = name;
m_age = age;
}
void print(){
cout << "name : " << m_name << " age : " << m_age << endl;
}
};
//1. 指定类型
void printP1(Person<string, int> &a){
a.print();
}
// 2. 参数模板化
template<class T1, class T2>
void printP2(Person<T1, T2> &a){
a.print();
}
// 3. 类模板 模板化
template<class T>
void printP3( T &a){
a.print();
}
int main(){
Person<string, int> a("张三", 33);
printP1(a);
printP2(a);
printP3(a);
return 0;
}
8. 类模板遇到继承
- 在继承时,如果父类是一个类模板,那么必须指定父类模板的数据类型。
- 如果想灵活继承父类模板,那么子类也需要做成类模板。
9. 类模板成员函数类外实现
template<class T1, class T2>
class Person{
public:
T1 m_name;
T2 m_age;
Person(T1 name, T2 age);
void print();
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age){
m_name = name;
m_age = age;
}
template<class T1, class T2>
void Person<T1, T2>::print(){
cout << "name : " << m_name << " age : " << m_age << endl;
}
10. 类模板分文件编写
类模板的声明在.h文件中,类模板的成员函数在相关联的.cpp文件中实现。 这种情况下,如果在别的文件中(比如main.cpp)引入该 .h文件,使用该类,会无法编译(无法解析外部符号),为什么?
原因是,类模板的成员函数在调用时才会创建。main.cpp引入了.h文件,该文件中只有类模板的声明,没有具体实现。
为了解决这个问题,有两种解决方法:
- 法1:不include类的.h文件,而是include类的.cpp文件。
- 法2:可以将类模板的声明和实现写在同一个文件中,并且习惯上把该文件的后缀名写成 .hpp。
实际中,第二种方法更加普遍。
11. 类模板的友元
- 类内定义:与普通类定义友元一样。
- 类内声明,类外定义:类外定义的话,肯定要声明称模板,而类内的声明语句,会认为是一个普通的函数,所以,类内的声明要加上空参数列表
friend test<>(test<T1, T2>);
说明这是一个模板,并不是一个普通的函数。
例程:
#include <iostream>
using namespace std;
// 声明有Person类 要不然test2找不到Person类
template<class T>
class Person;
// 声明test2是个模板函数,要不然下边的Person类中找不到test2这个模板函数
template<class T>
void test2(Person<T> a){
cout << a.a << endl;
}
template<class T>
class Person{
// 1. 类内实现友元
friend void test1(){ // 友元函数声明 + 定义,如果不加friend的话,相当于一个普通的成员
cout << 123 << endl;
}
// 2. 类外实现友元 这里声明要加空参数列表 说明需要的是一个模板函数
friend void test2<>(Person<T>);
public:
Person(T age){
a = age;
}
T a;
};
// test1函数的声明 供main调用。
void test1();
int main(){
Person<int> a(10);
test2(a);
return 0;
}