【C++初阶6-模版初阶】“多个愿望一次满足~”

前言

学习C++的第一篇博客,我们就谈到了“函数重载”。现在回头看,它的存在对C++其他语法是至关重要,如运算符重载Swap类似的函数等——帮助我们减少不必要的代码,提高可读性,是很实用的设计。而今天要讲的模版,和它的设计方向一样,做出了不同方面的补充……

博主水平有限,不足之处望请斧正!

零、先导

用函数重载来设计一个交换函数。

void Swap(int& e1, int& e2)
{
    int tmp = e1;
    e1 = e2;
    e2 = tmp;
}

void Swap(double& e1, double& e2)
{
    double tmp = e1;
    e1 = e2;
    e2 = tmp;
}

void Swap(char& e1, char& e2)
{
    char tmp = e1;
    e1 = e2;
    e2 = tmp;
}

int main(int argc, const char * argv[])
{
    int i1 = 10, i2 = 20;
    double d1 = 1.1, d2 = 2.2;
    char c1 = 'a', c2 = 'b';
    
    Swap(i1, i2);
    
    //...
    
    return 0;
}

太冗余了。究其原因,是C++强类型的特性。那么怎么解决?

一、泛型编程

是什么

泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型

换句话说就是,编写和类型无关的代码,

C++中,称“泛型”为 模版。(模版也是C++中进行泛型程序设计的基础)

为什么

“多个愿望,一次满足。”

是什么样的

在这里插入图片描述

一个模具,用不同颜色的材料浇注,产出不同样子的成品。样子形状都一样,只是颜色不同。

如何写出这种“模具”(模版)呢?


二、函数模版

是什么

函数模具。

根据不同的实参列表,推断模版参数,生成特定类型的函数版本

怎么写

“泛型编程”的“泛型”(C++中叫模版参数)如何写?


插播……

模板参数
是什么

是没有具体类型的类型,后续才指定类型——其实就是“泛型”了。

它是“空心”的,当我们提供模版参数的实参列表,模版参数就可以推演出自己的参数列表了。

它很像函数的参数列表,

  • 函数参数列表定义了若干个特定类型的局部变量,但没有指出如何初始化他们,运行时通过实参列表初始化形参

  • 模版参数则是没有指出类型的参数,运行时通过实参列表的类型推演并绑定其类型到模版参数上
特性

作用于最近的 函数/类。

怎么写

通过关键字 tmplate(模版)来声明

tmplate <class TypeName>
tmplate <typename TypeName>

在此处,class 和 typename 等价。如此就声明了一个模版参数。

具体应用在函数模版和类模版中。

有了模版参数,我们怎么用模版参数定义一个函数模版?


回到函数模版的写法:

template <typename T1, T2, ..., Tn>
void 函数模版名(T1, T2, ..., Tn)
{
//...
}

以关键字template开始,后跟一个模版参数列表(不能为空),再接基于模版参数实现的函数体。

//template <class T>
template <typename T>
void Swap(T& e1, T& e2)
{
	T tmp = e1;
	e1 = e2;
	e2 = tmp;
}

int main()
{
	char c1 = 'a', c2 = 'b';
	Swap(c1, c2);
	cout << c1 << ' ' << c2 << endl;
	
	int i1 = 10, i2 = 20;
	Swap(i1, i2);
	cout << i1 << ' ' << i2 << endl;
	
	double d1 = 1.1, d2 = 2.2;
	Swap(d1, d2);
	cout << d1 << ' ' << d2 << endl;
	
	return 0;
}
:b a
20 10
2.2 1.1

模版参数可以成为任何你要的参数,这样就舒服多了。

过程

  1. 当我们调用有函数模版的函数,编译器通过实参列表,确定要绑定到模版参数上的类型

    cout << Swap(i1, i2) << endl;//编译器根据实参列表,推演出int并绑定到模版参数T上
    
  2. 接着,编译器实例化出对应函数(将函数中的模版参数全部替换成推演出来的类型)。

  3. 最终供你使用。

我们也能由此得知:调用的时候才实例化。

在这里插入图片描述

模版的实例化?和对象实例化有啥区别?

函数模版的实例化

讲实例化之前,得要摆出一个大前提:一个模版参数被推演出后不会再变,且实参必须匹配模版参数。

template <typename T>
T Add(T& e1, T& e2)
{
    return e1 + e2;
}

int main()
{
    int i = 10;
    double d = 10.10;
    
    
    cout << Add(i, d) << endl;//error
    
    return 0;
}

道理很好理解:“你就是你。”

模版参数T已经通过i推演出int类型(落定),又来个double,传double给int……如果模版参数不会变,实例化的时候实参向模版参数转换一下不就好了?这个问题先按下不表。

1. 隐式实例化

让编译器隐式推演模版类型来实例化。

template <typename T>
T Add(const T& e1, const T& e2)//强转产生临时变量,临时变量具有常性!无法用普通引用接收
{
    return e1 + e2;
}

int main()
{
    int i = 10;
    double d = 10.10;
    
    
    cout << Add(i, (int)d) << endl;
    
    return 0;
}
:20

这里的double d为什么要强转?“如果模版参数不会变,实例化的时候实参向模版参数转换一下不就好了?”。

【实参向模版参数转换?】

想想,类型转换发生在什么时候?

相近类型的赋值或传参的时候。

此处推演类型,并不涉及到赋值,更没有传参(函数都没实例化出来),所以

隐式实例化,参数不匹配时,实参不会向模版参数转换。

2. 显式实例化

给编译器显式指定模版类型来实例化。

显式实例化的写法:

Function<typename>(xxx, xxx);

具体试试:

template <typename T>
T Add(const T& e1, const T& e2)
{
    return e1 + e2;
}

int main()
{
    int i = 10;
    double d = 10.10;
    
    
    cout << Add<double>(i, d) << endl;
    
    return 0;
}
:20.1

“显示实例化,指定了Add的模版参数为double,我能理解,但是,i 可不是double类型,你怎么解释?”

【实参向模版参数转换?】

都说是显式实例化了:“我就是要这样用,能绑定你就这样给我绑定吧!”

显式实例化,类型不匹配时,允许实参向模版参数转换

特性

  • 调用函数时,对于同名的函数和函数模版,如果模版能产生更匹配的参数,会实例化出更合适的函数来调用
template <typename T1, typename T2>
void Print(const T1& e1, const T2& e2)
{
    cout << "void Print(const T1& e1, const T2& e2)" << endl;
    cout << e1 << ' ' << e2 << endl;
}

void Print(int& e1, int& e2)
{
    cout << "void Print(int& e1, int& e2)" << endl;
    cout << e1 << ' ' << e2 << endl;
}

int main()
{
    int i = 10;
    double d = 10.10;
    
    Print(i, d);
    
    return 0;
}
:void Print(const T1& e1, const T2& e2)
10 10.1

模版:“哎呀老弟你不行啊,闪开,让我来!”

#实例化引起的错误

  • 第一个阶段:编译模版本身,通常不会发现太多错误,只是检查语法之类。

  • 第二个阶段:编译器遇到模版使用时,对于函数模版调用,会检查实参列表和模版参数列表是否匹配。

  • 第三个阶段:模版实例化时,具体类型被带入函数体,如果代码实现不是对所有类型通用的,就可能出问题, 如下:此时如果e1/e2不支持“<”,这个代码就有问题了。

    template <class T>
    int compare(const T& e1, const T& e2)
    {
    	if(e1 < e2) return -1;
    	if(e2 < e1) return 1;
    }
    

    模版程序应该尽量减少对实参类型的要求。


三、类模版

是什么

类模版。

根据不同的模版参数,产生不同(互相独立)

为什么

“多个愿望,一次满足。”

怎么写

template <class T1, T2, ..., Tn>
class 类模版名
{
 //...
};

以template开头,表示是模版,后跟模版参数列表,接着是类模版的定义。

看一下基本使用。

template <class T>
class Stack
{
public:
 Stack(size_t capacity = 4)
 {
     _a = new T[capacity]{0};
 }
 void push(const T& data);
 void print();
private:
 T* _a;
 size_t _size = 0;
 size_t _capacity = 0;
};

int main()
{
 Stack<int> si;

 Stack<double> sd;

 return 0;
}

在这里插入图片描述

可以看到,si和sd类型都是独立的。

上面的演示没有提及类模版的成员函数,现在接着补充。

类模版的成员函数

对两种栈插入数据试试,实现push:

在这里插入图片描述

哎呀呀,一个个分析。

  1. 指定类域时,告诉我们Stack不是一个类:

    哦哦哦!我们确实没有声明一个类,而是声明了一个类模版而已,要实例化才有类。

  2. 使用模版类型的时候,告诉我们不认识它:

    由于类模版的每个版本都绑定了各自的模版参数,所以,在类模版中,成员函数绑定了和类相同的模版参数。也就代表,如果我们想把成员函数定义在模版之外(无法共享类模版声明的模版参数),就必须给函数单独声明模版参数。

修改一下……

template<class T>
void Stack<T>::push(const T& data)
{
    _a[_size++] = data;
}
  • 首先声明一个一样的模版参数给自己用。
  • 再看,类域的指定:类模版的成员函数具有和类模版相同的模版参数。编译器看到这里会这么理解:push是Stack<T>这个类内的代码(我们说过,指定函数类域会让编译器认为函数体位于类内)。
//因为类模版用不同实参实例化出的对象类型独立,而他们都有自己
template<class T>
void Stack<T>::push(const T& data)
{
    _a[_size++] = data;
}

template<class T>
void Stack<T>::print()
{
    for(int i = 0; i<_size; i++)
    {
        cout << _a[i] << ' ';
    } cout << endl;
}

int main()
{
    Stack<int> si;
    Stack<double> sd;

    int i = 1;
    double d = 1.1;

    si.push(i);
    sd.push(d);

    si.print();
    sd.print();
    return 0;
}

:1 
1.1 

类模版的实例化

Stack<int> si;
Stack<double> sd;

“你这不是闲得没事干嘛,让他自己推演类型,隐式实例化不就得了,还显式实例化?”

确实,我们不需要显示实例化,如果可以推演。

如果让编译器推演类模版参数,如何推?好像没合适的地方吧?构造函数?不行的,我们构造函数不一定会传我们需要的数据类型。其实,

类模版只能显示实例化。(没有推演的机会)

根据指定的类型,实例化出类。如果要用实例化出的类进一步实例化出对象:

  • 属性实例化的时机

    • 对象实例化的时候,属性一起实例化。
  • 成员函数实例化的时机

    • 类模版的成员函数在被调用的时候实例化——把调用方的模版参数也绑定给自己。

#类模版的成员函数:分离编译问题

//stack.h
template <class T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _a = new T[capacity]{0};
    }
    void push(const T& data);
    void print();
private:
    T* _a;
    size_t _size = 0;
    size_t _capacity = 0;
};

//stack.cpp
template<class T>
void Stack<T>::push(const T& data)
{
    _a[_size++] = data;
}

template<class T>
void Stack<T>::print()
{
    for(int i = 0; i<_size; i++)
    {
        cout << _a[i] << ' ';
    } cout << endl;
}

//main.c
int main()
{
    Stack<int> si;
    int i = 1;   
    si.push(i);
    si.print();
    return 0;
}

一运行居然找不到函数的定义?

在这里插入图片描述

肯定是链接出了问题

在这里插入图片描述

在这里,引用一篇把这个问题讲得很透彻的文章来分析,向更优秀的人学习:为什么C++编译器不能支持对模板的分离式编译

首先了解:
一个编译单元,是指一个.cpp文件以及它#include的所有.h文件。
.h文件会被拓展到包含它的.cpp文件里,而后,将现.cpp文件编译成一个.obj文件。后者拥有PE可执行文件格式(在win32下),里面是二进制码,但是不一定能够执行——其中不一定有main函数,还需要链接。
在编译器将工程内的所有.cpp文件逐个分离编译后,再由链接器(Linker)来链接,才成为最终的.exe可执行文件。

简略地理解:展开头文件 ==> 编译.cpp为.obj目标文件。而目标文件即使拥有可执行格式,也不一定能执行——必须要链接后从main函数开始执行。

我们用简单的例子理解这个过程:

//test.h
void f();//声明了函数f

//test.cpp
#include "test.h"
void f()
{
	//...
}//实现test.h中声明的函数f

//main.cpp
#include "test.h"
int main()
{
	f();//调用f
	return 0;
}

预处理后的main.cpp

//#include "test.h":test.h的内容扩展到此处
void f();//声明了函数f

int main()
{
	f();//调用f,但此处的f具有外部链接类型
	return 0;
}

test.cpp ==> test.obj & main.cpp ==> main.obj
main.cpp调用f,而f的定义不在main.cpp,所以f是外部符号。因而,main.obj内中用于调用f的指令 "jump 某地址"中,“某地址”没有实际含义,而是会等到链接的时候,找到了f,才会重定位(把f的地址替换进去)。

现在来看类模版的成员函数为什么不支持分离编译

//stack.h
template <class T>
class Stack
{
	//属性和方法的声明
};

//stack.cpp
template<class T>
void Stack<T>::push(const T& data)
{
	//...
}

//main.cpp
#include "stack.h"
int main()
{
    Stack<int> si;
    int i = 1;   
    si.push(i);
    return 0;
}

main.cpp ⇒ main.obj & stack.cpp ⇒ stack.obj
Stack模版展开后是可以被main.cpp用来实例化Stack<int>这个类,但类模版的成员函数是在调用时实例化,而main.cpp把Stack<int>的push看作外部符号(等着链接时替换地址)。但在别的地方也没人调用Stack<int>的push,这个类模版成员函数并没有实例化。自然地,链接时重定位push肯定失败。

说白了:在分离的文件中有信息差,类模版的成员函数是在调用时实例化,调用的地方没定义,只能把函数当作外部符号,其他地方也不调用 = 根本没有实例化成员函数,这个所谓的”外部符号“就不存在了。其实就是误会。所以,

不支持对模版的分离编译!

所以我们通常不分离。

不分离

不分离的写法:展开头文件的时候,会把函数模版也一起展开。

我们也常把存放类声明定义的文件叫做.hpp。

//stack.hpp
#include <iostream>
using namespace std;

template <class T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _a = new T[capacity]{0};
    }
    void push(const T& data);
    void print();
private:
    T* _a;
    size_t _size = 0;
    size_t _capacity = 0;
};

template<class T>
//被Stack<int>类型的对象调用,根据this指针知道它的类型给他定制
void Stack<T>::push(const T& data)
{
    _a[_size++] = data;
}

template<class T>
void Stack<T>::print()
{
    for(int i = 0; i<_size; i++)
    {
        cout << _a[i] << ' ';
    } cout << endl;
}

//main.cpp

#include "stack.hpp"
int main()
{
  //si的类型:Stack<int>
  //此时只实例化了属性,成员函数没有实例化
    Stack<int> si;
    int i = 1;
  	//Stack<int>类型的si调用了其成员函数,可以实例化了
    si.push(i);
    si.print();
    
    return 0;
}
:1
显式实例化(不推荐)

根据要使用的对象,对类的成员函数模版显示实例化。

//stack.h
template <class T>
class Stack
{
	//属性和方法的声明
};

//stack.cpp
template<class T>
void Stack<T>::push(const T& data)
{
	//...
}

//main.c
int main()
{
    Stack<int> si;
    int i = 1;   
    si.push(i);
    return 0;
}

我们这里要用栈存int,但现在调用push是错误调用,因为根本没push这个函数!(只有push模版)既然没有我们就搞一个出来:在push模版处显式实例化出真正的push(模版参数需要设计成int)。

//  stack.cpp
#include "stack.h"
template<class T>
void Stack<T>::push(const T& data)
{
    _a[_size++] = data;
}

template<class T>
void Stack<T>::print()
{
    for(int i = 0; i<_size; i++)
    {
        cout << _a[i] << ' ';
    } cout << endl;
}

template class Stack<int>;//在最下面实例化才能实例化到全部模版(顺序编译)

:1

解决是解决了,那么,代价是什么?

当我们还想用double类型的栈,就要在成员函数模版下面再实例化

template class Stack<double>;

太矬了,模版的优势都被磨灭。


今天的分享就到这里啦,感谢观看。

这里是培根的blog,期待与你共同进步!

下期见~

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值