C++模板详解


1. 模板概念

模板提供了一种通用的方法来开发可重用的代码,即可创建参数化的C++类型。通俗来讲,模板就是建立通用的模具,大大提高复用性。

模板的特点:

  1. 模板不可以直接使用,它只是一个框架
  2. 模板的通用并不是万能的

模板的分类:

C++包含一种编程思想——泛型编程,主要利用的技术就是模板。C++提供两种模板机制:函数模板类模板

2. 函数模板

2.1 函数模板作用

建立一个通用函数,其函数返回值类型和形参类型可以不具体定制,用一个虚拟的类型来代表。

2.2 语法

template<typename T>
函数声明或定义

template – 声明创建模板

typename – 表明其后面的符号是一种数据类型,可以用class代替

T – 通用的数据类型,名称可以替换,通常为大写字母

2.3 举例

#include<iostream>
using namespace std;
//函数模板:实现两个数交换的函数
template<typename T> //声明一个模板,告诉编译器T是一个通用的数据类型
void mySwap(T &a,T &b){
    T temp=a;
    a=b;
    b=temp;
}
void test01(){
    //函数模板实现有两种方法
    //1.自动类型推导
    int a=10;
    int b=20;
    cout<<"a="<<a<<" "<<"b="<<b<<endl;
    mySwap(a,b);
    cout<<"交换后:"<<endl;
    cout<<"a="<<a<<" "<<"b="<<b<<endl;
    cout<<endl;
    //2.显示指定类型
    double c=3.2;
    double d=4.5;
    cout<<"c="<<c<<" "<<"d="<<d<<endl;
    mySwap<double>(c,d);
    cout<<"交换后:"<<endl;
    cout<<"c="<<c<<" "<<"d="<<d<<endl;
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

2.4 注意事项

  1. 自动类型推导必须推导出一致的数据类型T,才可以使用

  2. 模板必须要确定出T的数据类型,才可以使用

    举例:

    #include<iostream>
    using namespace std;
    template<typename T>
    void func(){
        cout<<"func() 调用"<<endl;
    }
    void test01(){
        //func();//没有与参数列表匹配的 函数模板 "func" 实例 
        func<int>();//应显示指定类型
    }
    int main(){
        test01();
        system("pause");
        return 0;
    }
    

    在这里插入图片描述

小结:

使用模板时必须要确定出通用数据类型T,并且能够推导出一致的类型。

2.5 普通函数与函数模板的区别

  1. 普通函数调用时可以发生自动类型转换(隐式类型转换)
  2. 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  3. 函数模板用显示指定类型,可以发生隐式类型转换

举例:

#include<iostream>
using namespace std;
int myAdd01(int a,int b){ //普通函数
    return a+b;
}
template<typename T> //函数模板
T myAdd02(T a,T b){
    return a+b;
}
void test01(){
    int a=10;
    char b='c';
    cout<<"普通函数发生隐式类型转换:"<<myAdd01(a,b)<<endl; //普通函数:变量b发生隐式类型转换,将字符型隐式地转化为整型
    //cout<<myAdd02(a,b)<<endl; //函数模板中的自动类型推导:报错:没有与参数列表匹配的 函数模板
    cout<<"函数模板显示指定类型中发生了隐式类型转换:"<<myAdd02<int>(a,b)<<endl; //函数模板中的显示指定类型:发生隐式类型转换
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

2.6 普通函数与函数模板的调用规则

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板

举例:

#include<iostream>
using namespace std;
void myPrint(int a,int b){
    cout<<"调用普通函数"<<endl;
}
template<typename T>
void myPrint(T a,T b){
    cout<<"调用函数模板"<<endl;
}
template<typename T>
void myPrint(T a,T b,T c){
    cout<<"调用重载的函数模板"<<endl;
}
void test01(){
    int a=10;
    int b=20;
    myPrint(a,b);//1.如果函数模板和普通函数都可以实现,优先调用普通函数
    myPrint<>(a,b);//2.通过空模板参数列表来强制调用函数模板
    myPrint(a,b,100);//3.函数模板也可以发生重载
    char c1='a';
    char c2='b';
    myPrint(c1,c2);//4.如果函数模板可以产生更好的匹配,优先调用函数模板
    //编译器认为将c1和c2推导出T为char型,比调用普通函数进行强制类型转换简单
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

小结

既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性。

2. 模板特化

2.1 模板的局限性

模板的通用性并不是万能的。

比如:

template<typename T>
T sum(T data[],int nSize){
    T sum=0;
    for (int i = 0; i < nSize; i++)
    {
        sum+=data[i];
    }
    return sum;
}

上述代码具有一定的局限性,那就是它只能对数组元素求和,假如想要实现对链表、集合等元素的求和,它就不能实现了,这也是STL的思维方式想要解决的问题。为了解决这一问题,我们提出了模板特化的概念。

2.2 模板特化的概念

概念:

在实例化模板时,对特定类型的实参进行特殊处理,即实例化一个特殊的版本。

举例:

#include<iostream>
#include<string>
using namespace std;
//对比两个数据是否相等的函数
class Person{
public:
    string m_name;
    int m_age;
public:
    Person(string name,int age){
        m_name=name;
        m_age=age;
    }
};
template<typename T>
bool myCompare(T &a,T &b){
    if (a==b)
    {
        return true;
    }
    else{
        return false;
    }
}
//利用模板特化实现Person类的比较,模板特化会优先调用
template<>bool myCompare(Person &p1,Person &p2){
    if (p1.m_name==p2.m_name&&p1.m_age==p2.m_age)
    {
        return true;
    }
    else{
        return false;
    }  
}
void test01(){
    int a=10;
    int b=20;
    bool ret=myCompare(a,b);
    if (ret)
    {
        cout<<"a=b"<<endl;
    }
    else{
        cout<<"a!=b"<<endl;
    }  
}
void test02(){
    Person p1("Tom",20);
    Person p2("Tom",30);
    bool ret=myCompare(p1,p2);
    if (ret)
    {
        cout<<"p1=p2"<<endl;
    }
    else{
        cout<<"p1!=p2"<<endl;
    }   
}
int main(){
    test01();
    test02();
    system("pause");
    return 0;
}

在这里插入图片描述

小结:

  1. 利用具体化的模板,可以解决自定义类型的通用化。
  2. 学习模板并不是为了写模板,而是为了在STL中能够运用系统提供的模板。

3. 类模板

3.1 类模板概述

类模板作用:

建立一个通用类,类中成员的数据类型可以不具体指定,用一个虚拟的类型来代表,在创建对象时再去指定类中成员的数据类型。

语法:

template<class T>

template – 声明创建模板

class – 表明其后面的符号是一种数据类型,可以用typename代替

T – 通用的数据类型,名称可以替换,通常为大写字母

举例:

#include<iostream>
#include<string>
using namespace std;
template<class NameType,class AgeType>
class Person{
public:
    NameType m_name;
    AgeType m_age;
public:
    Person(NameType name,AgeType age){
        m_name=name;
        m_age=age;
    }
    void showPerson(){
        cout<<"name:"<<m_name<<" "<<"age:"<<m_age<<endl;
    }
};
void test01(){
    Person<string,int> p1("Tom",20);//在将类模板实例化时,必须显式定义具体类型,编译器无法为我们自动推导
    p1.showPerson();
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

小结:

  1. 在将类模板实例化时,必须显式定义具体类型,编译器无法为我们自动推导。
  2. 类模板和函数模板语法相似,在声明模板template后面加类,即为类模板。

3.2 类模板和函数模板的区别

区别:

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数

举例:

#include<iostream>
#include<string>
using namespace std;
template<class NameType,class AgeType=int>//类模板在模板参数列表中可以有默认参数,默认为int类型
class Person{
public:
    NameType m_Name;
    AgeType m_Age;
public:
    Person(NameType name,AgeType age){
        m_Name=name;
        m_Age=age;
    }
    void showPerson(){
        cout<<"name:"<<m_Name<<" "<<"age:"<<m_Age<<endl;
    }
};
void test01(){
    //Person p1("Tom",20);//报错,因为类模板没有自动类型推导的使用方式
    Person<string,int> p1("Tom",20);
    p1.showPerson();
    Person<string> p2("Lisa",24);//因为m_age的默认类型为int,故这里不用显示指定参数类型
    p2.showPerson();
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

3.3 类模板中成员函数的创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  1. 普通类中的成员函数一开始就可以创建
  2. 类模板中的成员函数在调用时才创建

举例:

#include<iostream>
using namespace std;
class Person1{
public:
    void showPerson1(){
        cout<<"Person1 show"<<endl;
    }
};
class Person2{
public:
    void showPerson2(){
        cout<<"Person2 show"<<endl;
    }
};
template<class T>
class MyClass{
public:
    T obj;
public:
    void func1(){
        obj.showPerson1();
    }    
    void func2(){
        obj.showPerson2();
    }    
};
void test01(){
    MyClass<Person1> m;
    m.func1();
    //m.func2();//报错,Person1中没有函数showPerson2。因为类模板中的成员函数在调用时才创建
    //给类模板传入Person1类型的对象,类模板确定了T为Person1时,才创建自己的成员函数
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

3.4 类模板对象做函数参数

当类模板实例化出的对象,向函数传参时,一共有三种传入方式:

  1. 指定传入的类型 – 直接显示对象的数据类型
  2. 参数模板化 – 将对象中的参数变为模板进行传递
  3. 整个类模板化 – 将这个对象模板化后进行传递

举例:

#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 showPerson(){
        cout<<"name:"<<m_name<<" "<<"age:"<<m_age<<endl;
    }
};
//1.传入指定类型
void printPerson1(Person<string,int> &p){
    p.showPerson();
}
void test01(){
    Person<string,int> p1("Tom",20);
    printPerson1(p1);
}
//2.参数模板化
template<typename T1,typename T2>
void printPerson2(Person<T1,T2>&p){
    p.showPerson();
}
void test02(){
    Person<string,int>p2("Lisa",24);
    printPerson2(p2);
}
//3.整个类模板化
template<typename T>
void printPerson3(T &p){
    p.showPerson();
}
void test03(){
    Person<string,int>p3("David",30);
    printPerson3(p3);
}
int main(){
    test01();
    test02();
    test03();
    system("pause");
    return 0;
}

在这里插入图片描述

3.5 类模板与继承

当类模板碰到继承时,需要注意以下几点:

  1. 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  2. 如果不指定,编译器无法给子类分配内存
  3. 如果想灵活指定出父类中T的类型,子类也需变为类模板

举例:

#include<iostream>
#include<string>
using namespace std;
template<class T>
class Base{
    T m;
};
class Son1:public Base<int>{//指定出父类中T的类型

}; 
void test01(){
    Son1 s1;
}
template<class T1,class T2>
class Son2:public Base<T2>{//想灵活指定出父类中T的类型,子类也需变为类模板
    T1 obj;
public:
    void showSon2(){
        cout<<"T1的数据类型为:"<<typeid(T1).name()<<endl;
        cout<<"T2的数据类型为:"<<typeid(T2).name()<<endl;
    }
};
void test02(){
    Son2<int,char> s2;//将int传给T1,应用于Son2类;将char传给T2,应用于Base类
    s2.showSon2();
}
int main(){
    test01();
    test02();
    system("pause");
    return 0;
}

在这里插入图片描述

小结:

如果父类是类模板,子类需要指定出父类中的数据类型。

3.6 类模板中成员函数的类外实现

#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);
    void showPerson();
};
//构造函数的类外实现
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>::showPerson(){
        cout<<"name:"<<m_Name<<endl;
        cout<<"age:"<<m_Age<<endl;
}
void test01(){
    Person<string,int> p("Tom",20);
    p.showPerson();
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

小结:

类模板中的成员函数类外实现时,需要加上模板参数列表。

3.7 类模板分文件编写

问题提出:

类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决方式:

  1. 直接包含.cpp源文件
  2. 将声明和实现写到同一个文件中,并更改后缀名为.hpp.hpp是约定的名称,并不是强制

举例:

解决方式1:直接包含.cpp源文件

Person.h:

#pragma once
#include<iostream>
using namespace std;
template<class T1,class T2>
class Person{

    T1 m_Name;
    T2 m_Age;
public:
    Person(T1 name,T2 age);
    void showPerson();
};

Person.cpp:

#include<iostream>
#include"Person.h"
using namespace std;
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>::showPerson(){
    cout<<"name:"<<m_Name<<" "<<"age:"<<m_Age<<endl;
}

Demo12.cpp:

#include<iostream>
using namespace std;
#include"Person.cpp"
void test01(){
    Person<string,int> p("Tom",20);
    p.showPerson();
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp

Person.hpp:

#pragma once
#include<iostream>
using namespace std;
template<class T1,class T2>
class Person{

    T1 m_Name;
    T2 m_Age;
public:
    Person(T1 name,T2 age);
    void showPerson();
};
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>::showPerson(){
    cout<<"name:"<<m_Name<<" "<<"age:"<<m_Age<<endl;
}

Demo12.cpp:

#include<iostream>
using namespace std;
#include"Person.hpp"
void test01(){
    Person<string,int> p("Tom",20);
    p.showPerson();
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

3.8 类模板与友元

全局函数类内实现:

直接在类中声明友元即可。

#include<iostream>
using namespace std;
template<class T1,class T2>
class Person{
    //全局函数类内实现
    friend void printPerson(Person<T1,T2> p){
        cout<<"姓名:"<<p.m_name<<" 年龄:"<<p.m_age<<endl;
    }
    string m_name;
    int m_age;
public:
    Person(T1 name,T2 age){
        m_name=name;
        m_age=age;
    }
};
void test01(){
    Person<string,int> p("tom",20);
    printPerson(p);
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

全局函数类外实现:

需要提前让编译器知道全局函数的存在。

#include<iostream>
using namespace std;
template<class T1,class T2>
class Person;//提前告知编译器类模板的存在
template<class T1,class T2>//提前让编译器知道全局函数的存在
void printPerson(Person<T1,T2> p){
        cout<<"姓名:"<<p.m_name<<" 年龄:"<<p.m_age<<endl;
    }
template<class T1,class T2>
class Person{
    //全局函数类外实现(记得加空模板)
    friend void printPerson<>(Person<T1,T2> p);
    string m_name;
    int m_age;
public:
    Person(T1 name,T2 age){
        m_name=name;
        m_age=age;
    }
};
void test01(){
    Person<string,int> p("tom",20);
    printPerson(p);
}
int main(){
    test01();
    system("pause");
    return 0;
}

在这里插入图片描述

小结:建议全局函数做类内实现,用法简单,而且编译器可以直接识别。


参考视频:https://www.bilibili.com/video/BV1et411b73Z?p=167

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值