模板的工作原理
模板时C++中的一种特性,允许函数或类(对象)通过泛型的形式表现或者运行,模板分为类模板和函数模板,实例化后分别为模板类和模板函数,一个模板可以实例化为各种数据类型(types)的实例类或者函数,无需为每一种类型分别写一份代码,作为通用的模具,大大提高了复用性,编译器将模板编译的过程类似于宏替换过程,由于编译器不会直接编译模板本身,所以模板的定义通常放在头文件中
模板的实例化
模板是参数化的类或者函数,提供了相应的创建实例的语法框架,实例化的过程就是创建一个真正的类或者函数的过程,用具体数据类型代替模板参数,从而生成一个实例。但如果该数据类型不支持模板定义的操作,就会导致编译错误,又两种实例化的方法,显示实例化于隐式实例化,显式实例化在代码中明确指定要针对哪种类型进行实例化,隐式实例化在首次使用时根据具体情况使用一种合适的类型进行实例化
语法
template<typename T>
template:声明创建模板
typename:表明后面的字符T是一种数据类型,也可以使用class代替
T:通用的数据类型,可以是其它字符,通常使用大写字母
比较最大值的例子
template<typename T>
inline T max(const T& a,const T& b)
{
return a > b? a:b;
}
数据交换的例子
template<typename T>
inline void Swap(T&a,T&b)
{
T temp = a;
a = b;
b = temp;
}
函数重载比较最大值的例子
template<typename T>
inline T max(const T& a,const T& b,const T& c)
{
T temp = const a>b ? a:b;
temp = temp > c ? temp:c;
}
注意:
类型是自动推导的,需要保持一致,若不一致可以用static_cast强制转换
普通函数与模板函数的区别
普通函数调用时可以发生自动类型转换(隐式类型转换)
函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
如果使用显示指定类型的方式,可以发生隐式类型转换
普通函数与函数模板的调用规则
如果函数模板和普通函数都可以实现,优先调用普通函数
可通过空模板参数列表来强制调用函数模板
函数模板可以可发生重载
如果函数模板可以产生更好的匹配,优先调用函数模板
void print(int a, int b)
{
cout << "调用普通函数" << endl;
}
//函数模板
template<typename T>
void print(T a, T b)
{
cout << "调用函数模板" << endl;
}
//函数模板的重载
template<typename T>
void print(T a, T b,T c)
{
cout << "调用重载函数模板" << endl;
}
void test()
{
int a = 10;
int b = 20;
int c = 20;
//优先调用普通函数
print(a, b);
//通过空模板参数列表,强制调用函数模板
print<>(a, b);
//调用函数模板的重载
print(a, b,c);
//如果函数模板可以产生更好的匹配, 优先调用函数模板
char d = 'd';
char e = 'e';
print(d, e);//优先选择可以匹配的,这里可以匹配函数模板}
非类型模板参数
顾名思义,模板参数不是一个类型而是一个具体的值——这个值是常量表达式。
当一个模板被实例化时,非类型参数被一个用户提供的或者编译器推断出的值所代替。正因为模板在编译阶段编译器为我们生成一个对应的版本,所以其值应该能够编译时确定,那么他应该是一个常量或者常量表达式。
template <size_t N, size_t M>
int str_compare(const char (&str1)[N], const char (&str2)[M])
{
return strcmp(str1,str2);
}
使用例子
str_compare("hello","nihao")
M,N由传入的字符串确定,用于开辟指定大小的内存空间,因为编译器在编译阶段就将模板实例化,而内存分配也是在这个阶段
之前普通函数与模板函数那一部分中,以下调用也是非类型模板参数的使用,编译器会自动推导参数是什么类型,从而不用显示的调用模板参数
int a = 10;
int b = 20;
int c = 20;print(a, b);
print<>(a, b);
非类型模板参数的适用范围
非类型模板参数只允许使用整型家族(enum),浮点数,指针或者左值引用
对于左值引用和指针,应该保证必须具有静态的生存期,保证不会被释放
模板类
实例
#include<iostream>
using namespace std;
#include<string>
//类模板
template<class name_type,class age_type>//自定义了名字和年龄的类型
class person
{
public:
person(name_type name, age_type age)
{
this->m_name = name;
this->m_age = age;
}
void show()
{
cout << "name:" << this->m_name << endl;
cout << "age:" << m_age << endl;
}
name_type m_name;
age_type m_age;
};
void test()
{
//类型参数化
person<string, int> per("张三", 21);
p1.show();
}
类模板与函数模板的区别
1.类模板没有自动类型推导,必须在后面的<>中指定类型
2.类模板在模板参数列表中可以有默认参数
template< class name_type,class age_type = int>
类模板对象的传参方式
//1. 指定传入的类型——直接显示对象的数据类型
void printperson1(person<string, int> & p1)
{
p1.showperson();
}
void test01()
{
person<string, int>p1("张三", 20);
printperson1(p1);
}
//2. 参数模板化——将对象中的参数变为模板进行传递
template<class T1,class T2>
void printperson2(person<T1, T2>&p2)
{
p2.showperson();
}
void test02()
{
person<string, int>p2("李四", 22);
printperson2(p2);
}
//3. 整个类模板化——将这个对象类型模板化进行传递
template<class T>
void printperson3(T &p3)
{
p3.showperson();
}
void test03()
{
person<string, int>p3("王五", 23);
printperson3(p3);}
类模板的继承
当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
如果不指定,编译器无法给子类分配内存
如果想灵活指定出父类中T的类型,子类也需变为类模板
template<class T>
class father
{
public:
T m_1;
};
//如果想灵活指定出父类中T的类型,子类也需变为类模板
template<class T1, class T2>
class son :public father<T1>//子类在声明的时候,要指定出父类中T的类型
{
public:
son()
{
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}
T2 m_2;
};
void test()
{
son<int, char>s;
}
typeid().name()包含类型信息
类模板成员函数在类外实现定义
其实和普通类做法类似,在使用类的作用域::的同时使用模板template<>即可
template<class T1,class T2>
class person
{
public:
person(T1 name, T2 age);
void showperosn();
T1 m_name;
T2 m_age;
};
//构造函数的类外实现
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>::showperosn()
{
cout << "姓名:" << m_name << " 年龄:" << m_age << endl;
}
类模板与友元
有三种形式,C表示所有类模板C生成的类都是D的友元
template <typename N>
class D
{
friend A<N>;
friend B<int>;
template <typename T> friend class C;
}
可以将内置类型声明为自己的友元
可以在内部声明
template <typename T>
class People
{
friend int;
};
也可以用参数传入
template <typename T>
class People
{
friend T;
};
在类内实现定义全局函数
template<typename T>
class A
{
//全局函数 类内实现
friend void print(A<T> a)
{
ocut<<"a.name = "<<a.name<<endl;
}
//全局函数 类外实现
//需要在声明前加模板
template<class T>
friend void print2(A<T> a);
private:
T name;
}
在类外实现定义全局函数
friend void print2(A<T> a)
{
ocut<<"a.name = "<<a.name<<endl;
}
模板的特化
template<class T>
bool IsValue(T x, T y)
{
return x == y;
}
函数用于比较两个相同类型的数据是否相等,但不适用于char[]类型,所以类模板对于一些特殊的类型,需要进行一些特殊化的实现方式
函数模板特化步骤
- 首先必须要有一个基础的函数模板。
- 关键字template后面接一对空的尖括号<>。
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
- 函数形参表必须要和模板函数的基础参数类型完全相同,否则不同的编译器可能会报一些奇怪的错误。
//对于char*类型的特化
template<>
bool IsValue<char*>(char* x, char* y)
{
return strcmp(x, y) == 0;
}
//也可以写成普通函数的形式
bool IsValue(char* x, char* y)
{
return strcmp(x, y) == 0;
}
同样的类模板也有特化的方式来解决特殊数据类型的问题,分为全特化和偏特化(半特化)
类模板特化步骤
- 首先必须要有一个基础的类模板。
- 关键字template后面接一对空的尖括号<>。
- 类名后跟一对尖括号,尖括号中指定需要特化的类型。
全特化即是将模板参数列表中所有的参数都确定化。
template<>
class A<int,char>
{
public:
//构造函数
A()
{
cout << "A<int, char>" << endl;
}
private:
int _t1;
char _t2;
};
偏特化将模板参数列表中的部分参数特化
template<class T>
class A<int, T>
{
public:
//构造函数
A()
{
cout << "A<int, T>" << endl;
}
private:
int _t1;
T _t2;
};
偏特化的特殊形式:限制参数
//两个参数偏特化为指针类型
template<class T1, class T2>
class A<T1*, T2*>
{
public:
//构造函数
A()
{
cout << "A<T1*, T2*>" << endl;
}
private:
T1 _t1;
T2 _t2;
};
//两个参数偏特化为引用类型
template<class T1, class T2>
class A<T1&, T2&>
{
public:
//构造函数
A()
{
cout << "A<T1&, T2&>" << endl;
}
private:
T1 _t1;
T2 _t2;
};
问题 :
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决方法:
解决方式1: 直接包含.cpp源文件。
解决方式2: 直接在cpp源文件中显示实例化
解决方式2: 将声明(.h 文件)和实现(.cpp 文件)写到同一个文件中,并更改后缀名为.hpp, hpp是约定的名称,并不是强制。
模板的优缺点
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性。
缺陷:
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。