11 类型泛化
- 1、函数模版
-
- [1.1 前言](#1.1 前言)
- [1.2 函数模版](#1.2 函数模版)
- [1.3 隐式推断类型实参](#1.3 隐式推断类型实参)
- [1.4 函数模板重载](#1.4 函数模板重载)
- [1.5 函数模板类型形参的默认类型(C++11标准)](#1.5 函数模板类型形参的默认类型(C++11标准))
- 2、类模版
-
- [2.1 类模板的成员函数延迟实例化](#2.1 类模板的成员函数延迟实例化)
- [2.2 类模板的静态成员](#2.2 类模板的静态成员)
- [2.3 类模板的递归实例化](#2.3 类模板的递归实例化)
- [2.4 类模板类型形参缺省值](#2.4 类模板类型形参缺省值)
- 3、类模板的扩展
-
- [3.1 模板型成员变量](#3.1 模板型成员变量)
- [3.2 模板型成员函数](#3.2 模板型成员函数)
- [3.3 模板型成员类型](#3.3 模板型成员类型)
- [3.4 类模板中成员虚函数](#3.4 类模板中成员虚函数)
- 4、模板的特殊用法
-
- [4.1 数值型的类型形参](#4.1 数值型的类型形参)
- [4.2 模板型的类型形参](#4.2 模板型的类型形参)
- 5、模板的经典错误(补)
-
- [5.1 模板二次编译](#5.1 模板二次编译)
- [5.2 嵌套依赖](#5.2 嵌套依赖)
- [5.3 利用类型形参调用成员函数模板](#5.3 利用类型形参调用成员函数模板)
- [5.4 子类模板访问基类模板](#5.4 子类模板访问基类模板)
1、函数模版
1.1 前言
首先C++为强类型语言
-优点:有很多的数据类型(基本类型、类类型)在类型安全性方面是无可比拟的
-缺点:因为数据类型的多样性,在很大程度上会给程序员编写通用代码带来麻烦
这就使得程序员需要为每一种数据类型编写完全相同或者近乎完全相同的代码,即便它们在抽象层面是一致的。
c++
int Max(int x, int y){
return x > y ? x : y;
}
double Max(double x, double y){
return x > y ? x : y;
}
string Max(string x, string y){
return x > y ? x : y;
}
int main(){
int nx = 10, ny = 11;
cout << Max(nx, ny) << endl;
double dx = 1.0, dy = 1.1;
cout << Max(dx, dy) << endl;
string sx = "1.0", sy = "1.1";
cout << Max(sx, sy) << endl;
...如果有其他类型的比较大小,我们需要为每一种类型编写相同的代码
}
1.2 函数模版
- 定义语法:
c++
template<class|typename 类型形参1,class|typename类型形参2,...>返回值类型 函数模板名(调用形参1,调用形参2,...){...}
示例:
template<typename T>T Max(T x,Ty){
return x y?x:y;
}
**注意:**可以使用任何标识符作为类型形参的名称,但使用"T"己经称为一种惯例,"T"表示的是,调用者在使用函数模板时指定的任意类型
- 使用:使用函数模板必须对函数模板进行实例化
- 语法
c++
函数模板名<类型实参1,类型实参2,>(调用实参1...);
- 示例
c++
// 定义函数模板
template<typename T>T Min(T x, T y){
return x > y ? y : x;
}
int main(){
int nx = 10, ny = 11;
// 使用函数模板
cout << Min<int>(nx, ny) << endl;
double dx = 1.0, dy = 1.1;
cout << Min<double>(dx, dy) << endl;
string sx = "1.0", sy = "1.1";
cout << Min<string>(sx, sy) << endl;
return 0;
}
- 分析
- 编译器并没有 把函数模板翻译成一个可以处理任何数据类型的单一实体
- 编译器在实例化函数模板时根据类型实参从函数模板中产生一个真正的函数实体
- 函数模板并不是一个函数实体,通过实例化才能产生真正的函数实体
- 函数模板可以看成是编译器生成函数实体的一个依据而已
- 这种用具体数据类型替换 函数模板类型形参的过程叫做实例化,这个过程将产生一个函数模板的实例(函数实体)
- 原则上来说可以使用任何类型来实例化函数模板,不管其为基本类型还是类类型但前提是这个类型必须支持函数模板所要执行的操作
1.3 隐式推断类型实参
- 概念
- 若函数模板的调用形参和类型形参相关,那么在实例化函数模板时即使不显式指明类型实参,编译器也有能力根据调用实参的类型隐式推断出类型实参的类型
- 获得和调用普通函数一致的书写形式
c++
template<typename T>T Min(T x, T y){
return x > y ? y : x;
}
int main(){
int nx = 10, ny = 11;
// 调用形参和类型形参相关
cout << Min(nx, ny) << endl;// --> cout << Min<int>(nx, ny) << endl;
double dx = 1.0, dy = 1.1;
cout << Min(dx, dy) << endl;
string sx = "1.0", sy = "1.1";
cout << Min(sx, sy) << endl;
return 0;
}
- 三种情况不能做隐式推断
1: 调用参数和类型参数不完全相关
c++
template <typename T, typename D>void Foo(D x){
cout << x << endl;
}
int main(){
int nx = 10, ny = 11;
//Foo(nx);// 不能推断出T的类型
Foo<float>(nx);
return 0;
}
2: 隐式推断类型实参不能同时隐式类型转换
c++
template <typename T>void Foo1(T x,T y){
cout << x << endl;
}
int main(){
// Foo1(11,13.5);// 隐式推断类型实参不能同时隐式类型转换
Foo1(11, (int)13.5);
return 0;
}
3: 返回值类型不能隐式推断
c++
template<typename R,typename T>
R Bar(T x){
R r;
return t;
}
// 返回值类型不能隐式推断
void C11_06(){
int a = 10;
//Bar(10);// 根据实参能推断出形参T的类型,但是无法推断出R的类型
Bar<float>(10);
}
1.4 函数模板重载
- 普通函数 和能够实例化出该函数的函数模板 构成重载关系
- 在调用参数类型匹配度相同情况下编译器优先选择普通函数
- 除非函数模板可以产生调用参数类型匹配度更好的函数
- 隐式推断类型实参不能同时隐式类型转换但普通函数可以,如果在传递参数时如果需要编译器做隐式类型转换,则编译器选择普通函数。
- 可以在实例化时用<>强行通知编译器选择函数模板。
c++
void MaxType(int x, int y){
cout <<"1.MaxType" << endl;
}
template<typename T>void MaxType(T x, T y){
cout << "2.MaxType" << endl;
}
int main(){
int nx = 10, ny = 11;
MaxType(nx,ny); // 调用普通函数 在调用参数类型匹配度相同情况下编译器优先选择普通函数
double dx = 1.0, dy = 1.1;
MaxType(dx, dy);// 调用函数模版 函数模板可以产生调用参数类型匹配度更好的函数
MaxType(nx,dy);// 调用函数模版 隐式推断类型实参不能同时隐式类型转换但普通函数可以
MaxType<>(nx, ny);// <>强行通知编译器选择函数模板。
return 0;
}
1.5 函数模板类型形参的默认类型(C++11标准)
- 函数模板的类型形参可以带有默认类型
- 在实例化函数模板时,如果提供了类型实参则用所提供的类型实参来实例化函数模板
- 在实例化函数模板时,如果没提供类型实参则用类型形参的缺省类型来实例化函数模板
- 如果某一个类型形参带有缺省类型,则其后的类型形参都必须带有缺省类型
c++
template <typename T=short, typename D=float>void Foo2(int x=0){
T t;
D d;
cout << "t的类型" << typeid(t).name() << " d的类型" << typeid(d).name() << endl;
}
int main(){
Foo2<int, double>(100);// 用提供的类型
Foo2(100);// 用默认类型
return 0;
}
2、类模版
- 形式:
c++
// 书写
template<tyepname类型形参1,...>
class 类模板名{
...
}
// 示例
template<typename A,typename B>
class CMath{
public:
A m a;
B func0{...}
}
- 使用:类模板必须 对类模板进行实例化(产生真正的类 )
- 类模板本身并不代表一个确定的类型(即不能用于定义对象)
- 只有通过类型实参实例化成真正的类后才具备类的特性(即可以定义对象)。
c++
template<typename T>
class CMath{
public:
CMath(T t1, T t2) :m_t1(t1), m_t2(t2){}
T add(){ return m_t1 + m_t2; }
T sub();// 声明
private:
T m_t1;
T m_t2;
};
// T sub()定义 template<typename T>不能丢,要用类名引成员,切记不用要类模板引成员
template<typename T>
T CMath<T>::sub(){
return m_t1 - m_t2;
}
int main(){
int nx = 10, ny = 11;
CMath<int>m1(nx, ny);
cout << m1.add() << endl;
double dx = 1.0, dy = 1.1;
CMath<double>m2(dx, dy);
cout << m2.add() << endl;
string sx = "1.0", sy = "1.1";
CMath<string>m3(sx, sy);
cout << m3.add() << endl;
return 0;
}
2.1 类模板的成员函数延迟实例化
类模板被实例化产生真正类的一刻,类模板中的成员函数并没有实例化, 成员函数只有在被调用时才会被实例化(即产生真正成员函数),称之为成员函数的延迟实例化
注意: 成员虚函数除外
某些类型虽然并没有提供类模板所需要的全部功能 ,但照样可以用它来实例化类模板,只要不调用那些未提供功能的成员函数即可。
c++
int main(){
// CMath<int> 编译到这是,类中只有成员变量,没有成员函数
// CMath<int>m1(nx,ny) // 此时编译器才会给类中添加构造函数
// m1.add() // 此时才会去类中添加add的成员函数
return 0;
}
2.2 类模板的静态成员
- 类模板中的静态成员既不是每个对象拥有一份也不是类模板拥有一份 ,应该是由类模板实例化出的每一个真正的类各自拥有一份 ,且为该实例化类定义的所有对象共享
c++
template <typename T> class A
{
public:
static T m_t;// 声明
static void foo(){
cout << "A<T>::foo()" << endl;
}
};
template <typename T> T A <T> ::m_t=T();// 定义 T()零值初始化
// 类模板中的静态成员即不是每个对象拥有一份也不是类模板拥有一份,
// 应该是由类模板实例化出的每一个真正的类各自拥有一份,且为该实例化类定义的所有对象共享
int main(){
A<int> x, y;
cout << "&A<int>::m_t:" << &A<int>::m_t << endl; // &A<int>::m_t:00DB53F8
cout << "&x.m_t:" << &x.m_t << " &y.m_t:" << &y.m_t << endl; //&x.m_t:00DB53F8 &y.m_t:00DB53F8
A<double> m, n;
cout << "&A<double>::m_t:" << &A<double>::m_t << endl; // &A<double>::m_t:00DB5400
cout << "&m.m_t:" << &m.m_t << " &m.m_t:" << &m.m_t << endl;// &m.m_t:00DB5400 &m.m_t:00DB5400
return 0;
}
2.3 类模板的递归实例化
- 概念
- 利用类模板实例化产生的类来实例化类模板自身,这种做法称之为类模板的递归实例化
- 应用
- 通过这种方法可以构建空间上具有递归特性的数据结构
(例如:多维数组)
- 通过这种方法可以构建空间上具有递归特性的数据结构
- 示例
- Array<Array<int> >
c++
template<typename T>class Array{
public:
T& operator[](size_t i){
return arr[i];
}
private:
T arr[10];
};
# include <iomanip>
int main(){
Array<int> a; // a对象当成是一维数组看待
for (int i = 0; i < 10; i++){
a[i] = 1000 + i;
}
for (int i = 0; i < 10; i++){
cout << a[i] << " ";
}
cout << endl;
Array< Array<int> > b; // 类模板的递归实例化
for (int i = 0; i < 10; i++){
for (int j = 0; j < 10; j++){
b[i][j] = i*j;
}
}
for (int i = 1; i < 10; i++){
for (int j = 1; j <= i; j++){
cout << i << "X" << j << "=";
cout << left << setw(2) << setfill(' ') << b[i][j] << " ";
}
cout << endl;
}
return 0;
}
2.4 类模板类型形参缺省值
- 类模板的类型形参可以带有缺省类型
- 在实例化类模板时,如果提供了类型实参则用所提供的类型实参来实例化类模板
- 在实例化类模板时,如果没提供类型实参则用类型形参的缺省类型来实例化类模板
- 如果某一个类型形参带有缺省类型,则其后的类型形参都必须带有缺省类型
c++
template<typename T=short,typename D=int>
class CCMath{
private:
T m_t;
D m_d;
public:
void print(){
cout << "m_t的类型:" << typeid(m_t).name() << endl;
cout << "m_d的类型:" << typeid(m_d).name() << endl;
}
};
int main(){
CCMath<>m1;
CCMath<int>m2;
CCMath<int,double>m3;
m1.print();
m2.print();
m3.print();
return 0;
}
3、类模板的扩展
3.1 模板型成员变量
成员变量 ,但其类型是由类模板实例化的未知类,称之为模板型成员变量
- 示例
c++
template <typename T> class Array{...}
template <typename D> class Sum{
public :
Array<D> m_d; // 模板型成员变量
}
c++
template<typename T>class Array2{
public:
T& operator[](size_t i){
return arr[i];
}
private:
T arr[10];
};
template<typename D>class Sum{
public:
Sum(const Array2<D>& v) :m_a(v){}
D add(){ // 求和器
D d = D();
for (int i = 0; i < 10; i++){
d += m_a[i];
}
return d;
}
private:
Array2<D> m_a;//模板型成员变量
};
int main(){
Array2<int> a;
for (int i = 0; i < 10; i++){
a[i] = i+1;
}
Sum<int> s(a);
cout << s.add() << endl;
return 0;
}
3.2 模板型成员函数
模板型成员函数 又名 成员函数模板
示例:
c++
template<typename T> class BB{
public:
template<typename D>void foo(){ // 成员函数模板
cout << "AA::foo<D>" << endl;
}
template<typename D>void bar();// 声明
};
template<typename T> template<typename D> void BB<T>::bar(){// 定义
cout << "BB :: bar" << endl;
};
void C11_15(){
AA a;
a.foo<int>();
BB<int>b; // 对类模板实例化
b.foo<int>();// 对函数模板实例化
b.bar<int>();
}
3.3 模板型成员类型
c++
template<typename X>class A1{
public:
template<typename Y>class B1{
public:
template<typename X> void foo(){ // 模板型成员类型
cout << "foo() " << endl;
}
};
};
int main(){
A1<int>::B1<int>b;
b.foo<int>();
return 0;
}
3.4 类模板中成员虚函数
c++
template <typename T> class Base{
public:
virtual void foo(){//虚函数
cout << "Base<T>::foo" << endl;
}
};
// 只有在父子类中,才可以将模板进行合并
template<typename T, typename D>class Derived :public Base<T>{
public:
void foo(){
cout << "Derived<T,D>::foo" << endl;
}
};
int main(){
Derived<int, double> dr;
Base<int>* pdase = &dr;
pdase->foo();
return 0;
}
注意: 类模板中可以有虚函数,也可以表现出多态性,但是不能有成员虚函数模板
这是因为根据成员虚函数的多态机制,需要一个虚函数表(表中保存成员虚函数的入口地址),而这个表是编译器在实例化类模板时就创建,而成员函数模板的实例化(即产生真正的函数实体)需要编译器处理完并且调用后才会完成,这时才出现成员虚函数的地址,所以成员函数模板的延迟实例化,阻碍了虚函数表的构建
4、模板的特殊用法
4.1 数值型的类型形参
模板的类型形参也可以是数值类型 (只能是整数),可以有缺省值
c++
template<typename T,int S=10>class Array{
public:
T& operator[](size_t i){
return arr[i];
}
int size(){
return S;
}
private:
T arr[S];
};
int main(){
Array<int, 30> a;
for (int i = 0; i < a.size(); i++){
a[i] = 1000 + i;
}
for (int i = 0; i < a.size(); i++){
cout << a[i] << ' ';
}
return 0;
}
4.2 模板型的类型形参
模板的类型形参也可以是类模版,可以有缺省值
c++
template<typename T>class Array{
public:
T& operator[](size_t i){
return arr[i];
}
int size(){
return 10;
}
private:
T arr[10];
};
template<typename D, template<typename M> class C = Array> class SumArray{
public:
SumArray(const C<D>& v) :m_a(v){};
D add(){
D a = D();
for (int i = 0; i < m_a.size(); i++){
a += m_a[i];
}
return a;
}
private:
C<D> m_a;
};
int main(){
Array<int> a;
for (int i = 0; i < a.size(); i++){
a[i] = 1000 + i;
}
SumArray<int, Array> s(a);
cout << s.add() << endl;
}
5、模板的经典错误(补)
5.1 模板二次编译
如果都过不了第一次编译,那么就谈不上第二次编译
- 编译器对模板会进行两次编译
- 第一次编译:发生在实例化模板之前(产生真正函数或真正类之前)只检查模板本身内部代码(只检查基本词法 是否正确)
- 模板内部出现的所有标识符是否均有声明
- 对于已知类型的调用要检查调用是否有效
- 对于未知类型调用认为都合理
- 第二次编译:第二次编译发生在实例化模板之后(产生真正函数或真正类之后)结合所使用的类型实参,再次检查模板代码,查看所有调用是否真的都有效
c++
template<typename T>void func(){
// 第一次编译时,编译器针对未知类型调用采用隐忍态度,尽量认为都合理
//fdf; //错误 模板内部出现的所有标识符是否均有声明 第一次编译
A3 a; a.foo(); // 对于已知类型的调用要检查调用是否有效 第一次编译
//a.fdfddf();// 错误 在A3类中没有fdfddf 第一次编译
T t;
t.jsjhdeasde();// 第一次编译不会报错,这是因为对于未知类型调用认为都合理 ,第二次编译会报错,这是因为第二次编译时,编译器结合类型实参,再次检查,所有调用是否真的合理
// t.fds<fd>fd();// 这种的第一次编译也会报错
}
int main(){
func<int>();
return 0;
}
5.2 嵌套依赖
- 问题:由于模板要经过两次编译,在第一次编译模板的代码时,类型形参的具体类型尚不明确,编译器将把类型形参的嵌套类型理解为某个未知类型的静态成员变量,因此编译器看到使用这样的标识符声明变量时会报告错误,这就叫嵌套依赖。
- 解决办法:在类型形参的前面增加一个typename标识符,意在告诉编译器其后是一个类模板的嵌套使用。
c++
class A{
public:
class B{
public:
void foo(){
cout << "A::B::foo" << endl;
}
};
};
template<typename T>void func1(){
// 第一次编译时,会把T::B看做成B是T的静态成员变量,编译器看到使用这样的标识符声明变量时会报告错误
// T::B 类型形参的嵌套类型 typename 告诉编译器B是T(未知类)的嵌套类型
typename T::B b;
b.foo();
}
int main(){
func1<A>();
return 0;
}
5.3 利用类型形参调用成员函数模板
- 问题:利用未知类定义的对象来访问成员函数模板时,编译器在第一次编译时无法解析成员函数模板的类型参数列表的<>而报告编译错误。
- 解决方法:在成员函数模板之前增加template关键字,意在告诉编译器其后是一个函数模板实例,编译器就可以正确理解<>了。
c++
class A{
public:
template <typename T> void foo(){
cout << "A::f00" << endl;
}
};
template <typename T> void func2(){
T t;
// 编译器第一次编译时,未知类型定义的对象,调用方法时,不能带有<>
// template告诉编译器后面的是函数模板的实例化
t.template foo<int>();
}
int main(){
func2<A>();
return 0;
}
5.4 子类模板访问基类模板
- 问题:在子类模板中访问基类模板的成员,编译器第一次编译时只在子类模板和全局域中搜索使用的标识符号,不会到基类模板中搜索
- 解决方法:在子类模板中可以通过使用作用域限定符或显式使用this指针
c++
template <typename T > class BaseA{
public:
int m_i;
void foo(){ cout << "BaseA<T>::foo()" << endl; }
};
template<typename T, typename D> class Deri : public BaseA<T>{
public:
void bar(/* this*/ ){
//m_i = 100; 报错
//foo();
BaseA::foo();
BaseA::m_i = 100;
this->foo();
this->m_i = 100;
}
};
int main(){
Deri<int, int >d;
d.bar();
return 0;
}