C++学习_模板

引例:

问题描述:怎样实现一个通用的加法函数?

  1. 函数重载
    函数重载可以手动针对不同的类型,编写相应的函数。但是它有下面这些缺陷:

    • 需要手动添加针对新类型的处理函数,比较麻烦
    • 代码复用率不高,像这个加法函数,仅仅数据类型不同,但是每一种类型都要编写类似的代码
    • 不能解决只有返回值不同的情况
    • 维护起来不方便,试想如果针对每种类型的函数代码都只有数据类型不同,那么这种方法出现问题需要修改,就得修改每一种类型的函数
  2. 公共基类
    创建一个公共基类派生出不同的函数来处理各种数据类型。缺陷:

    • 失去了类型检查的优点
    • 由于处在继承体系中,代码维护起来更加困难。
  3. 宏函数
    由于宏可以忽略类型,所以可以编写宏函数来处理类型不同的情况。
    缺陷:正因为宏没有类型检查,所以安全性不高,并且由于宏的特性,编写复杂的宏函数很容易出错。

总结上面的三种方法,每一种方法都有着难以忽略的缺陷,正因如此,才有了模板

上面的那种问题,其实属于一种编程的范畴内,那就是泛型编程
泛型编程:编写与类型无关的逻辑代码,是代码复用的一种手段之一。而模板是C++泛型编程的基础。

模板又分为函数模板类模板


函数模板

1.函数模板的概念

什么是函数模板呢?
可以暂时理解为函数模板就是一个通用函数,它所用到的数据类型(可以是返回值,参数,局部变量等)可以不具体指定,而是用一个指定的标识符来替代任意一种不确定的类型,等到发生函数调用的时候再根据传入的实参来推演出真正的类型,这就是函数模板。

2.函数模板的定义格式
这里用一个通用的加法函数来示例:

//T是任意起的标识符名,代表某种数据类型
template<typename T,...> 
T Add(const T &left, const T &right)
{
        return left + right;
}

代码说明:typename是用来定义模板参数的关键字,这个关键字也可以用class替代(事实上在typename被提出来以前,这个位置一直都用class来定义模板参数,但是因为class容易和类混淆,所以就推出了typename,所以推荐在定义模板参数的时候使用typename)。这个加法函数模板就是一个通用的加法函数,如果这样调用:Add(1,2),那么T就代表int类型;如果这样调用:Add(1.2,2.3),那么T就代表double类型。这里还有一点需要注意的:为什么参数是const 引用类型?

  • 函数参数使用const类型的原因是指明这个参数是输入性参数,防止该参数在函数内部被意外修改。
  • 函数参数使用引用类型,是避免T代表自定义类型的时候需要调用拷贝构造函数增加额外开销。

有时候为了效率,还可以将函数模板定义为inline类型(内联函数)的:

template<typename T>
inline T add(const T &left, const T &right)
{
    return left + right;
}

3.函数模板的实例化

模板只是一个蓝图,它本身不是类或者函数,编译器用模板产生指定的类或者函数的特定版本,产生模板特定类型的过程称为函数模板实例化。从函数实参确定模板形参类型和值的过程称为模板实参推演,在函数传参的时候多个类型的实参必须完全匹配,否则就会编译失败。

模板的编译分为两个阶段:

  • 实例化以前,检查模板代码本身,是否出现简单语法错误。
  • 实例化后,检查模板生成的代码,是否所有的调用都有效。

4.类型转换
对于普通函数,在函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换有:

  • 内置类型转换:int->floatchar ->intdouble->int等。
  • 派生类向基类的转换,即向上转型
  • 数组和指针的转换:数组在传参的时候会转换成指针。eg: char arr[4]->char *arr
  • 函数名会转换成函数指针
  • 用户自定义的类型转换

而对于模板函数,一般不会转换实参以匹配已有的实例化,相反会产生新的实例,编译器只会执行下面两种类型转换:

  • const转换:接收const 引用或者const指针的函数可以分别用非const对象或指针来调用
  • 数组或函数的转换:数组实参将转换为指向其第一个元素的指针。函数将转换为指向函数类型的指针。

5.模板参数

模板有两种类型参数:模板参数和调用参数

模板形参又分为两类:类型形参和非类型形参

类型形参
  • 模板形参名字只能在模板形参之后到模板声明或定义的末尾之间未使用,使用遵循名称屏蔽规则。
  • 同一个模板形参名字在同一模板形参列表种只能使用一次。
  • 所有模板形参前面必须加上关键字typename / class
非类型形参

1.非类型形参的概念

什么是非类型形参呢?
template<typename T, size_t N> void func(T(&arr)[N]);
上面这行代码中模板参数列表中的N就是非类型形参。它用来传递一个具体的值,而不是类型,它就和一个普通函数的参数相同。当调用一个函数模板或者通过一个类模板创建对象时,非类型形参会被用户提供的,或者编译器推断出的值替代。

2.使用非类型形参的实例

//遍历打印数组
template<typename T, size_t N)
void PrintArray(const T(&arr)[N])
{
    for(int i = 0 ;i < N; i++)
        cout<<arr[i]<<" ";
}

代码解析:我们以前在需要在函数内部遍历形参数组时,往往需要手动传入数组的长度,但是使用了非类型形参后,就可以不用这样做了。函数PrintArray的参数是一个引用类型,引用了一个const T [N]类型的数组。(分析一个引用就和分析一个指针方法相同)

3.非类型形参的限制

设置非类型形参的限制

  • 只能是一个整数或者一个指向对象或函数的指针(也可以是引用)

传递实参时的限制:

  • 当非类型形参是一个整数时,传递给它的实参,或者由编译器推导出的实参必须是一个常量表达式。
  • 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。

类模板

类模板通常用于实现通用的数据结构算法

1.类模板的实现
实例:下面是一个使用类模板封装的顺序表类

template<class T> 
class SeqList 
{ 
public:   
     SeqList(const T* array, size_t size);   
     ~SeqList()  
      {      
            if(_array)       
             {        
                 delete[] _array;       
                 _size = 0;       
                 _capacity = 0;   
             } 
      } 
private:  
   T* _array;  
   size_t _size;   
   size_t _capacity;
};
//类模板成员函数在类外定义时的实现方式
template<class T>
SeqList<T>::SeqList(const T*array, size_t size)
    :_array(new T[size])
    ,_size(size)
    ,_capacity(size)
{
    for(size_t i = 0;i < size; i++){
        _array[i] = array[i];
    }   
}

顺序表中存放的元素类型使用了模板类就可以在创建顺序表的时候指定为需要使用的类型,而不需要大幅度更改代码。
2.类模板的实例化

SeqList<int> s1;
SeqList<double> s2;

这是用intdouble分别代表模板形参,重新编写SeqList类,最后创建了名称为SeqList<int>SeqList<double>的类,对于用户来说,只要知道上面这两行就是定义了两个顺序表即可。

3.模板参数

类模板参数可以用于实现容器适配器

什么是容器适配器呢?

容器适配器就是一个接口转换装置,比方说,你需要实现一个stack类,但是,又不想自己手写stack接口函数的底层实现,你想到SeqList里面的某些接口函数的功能和stack的接口函数非常像,那么你只要把SeqList的接口函数拿来就行了,而模板就可以让你实现这个功能。
//下面是一个实例:

template<class T>
class SeqList
{
    //接口函数
private:
    T _array;
    size_t _size;
    size_t _capacity;
}
//你需要封装的类
template<class T, class Container = SeqList<T> >
class Stack
{
publicvoid Push(const T& data);  
    void Pop(); 
    T& Top();  
    const T& Top()const;  
    size_t Size()const;   
    bool Empty()const; 
private:
    Container _con;//栈的底层是一个顺序表容器
}
//模板实例化
void main(void)
{
    Stack<int, SeqList<int>> s;
}

模板的模板参数也可以用于实现容器适配器
//下面是一个用模板的模板参数实现的容器适配器

template<class T, template<class> class Container = SeqList>
class Stack
{
public:
    //接口函数
private:
    Container<T> _con;
}
//模板的实例化
void main(void)
{
    Stack<int, SeqList> s;
}

下面是这两种实现容器适配器方式的比较:
这里写图片描述

模板的分离编译

下面举一个例子来说明什么是分离编译:

//SeqList.h
template<class T>
class SeqList
{
public:
    //接口函数
    SeqList();
private:
    int _size;
    int _capacity;
    T *_data;
}
//SeqList.cpp
template<class T>
SeqList<T>::SeqList():
    :_size(0)
    ,_capacity(0)
    ,_data(0)
{}

//test.cpp
#include<iostream>
using namespace std;
#include"SeqList.h"
int main(void)
{
    SeqList<int> s;
    return 0;
}

代码解析:上面这段代码看起来似乎没有什么问题,毕竟我们在C语言里就是这么干的,但是编译之后就会发现编译器报了一个链接错误

原因就在于,*.h 文件是不会参与编译的,在预处理的时候,把 SeqList.h 展开在test.cpp 里,但是由于 SeqList.cpp 和 test.cpp 是分离编译的,也就是两个模块。那么在 SeqList.cpp 中并没有看到对应的实例化类型,所以 SeqList< T > 并没有实例化出int类型的类,所以就会出错。

解决方案:

  • 将声明和定义放到一个文件 “xxx.hpp” 里面,推荐使用这种方法
  • 在模板头文件 *.h 里面显示实例化->模板类的定义后面添加 template class SeqList; 一般不推荐这种方法,一方面老编译器可能不支持,另一方面实例化依赖调用者(不推荐)
    • -
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值