一篇文章带你了解C++模板编程详解

 更新时间:2021年11月2日 16:00  点击:1414 作者:小赵小赵福星高照~

模板初阶

泛型编程

在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术。什么是泛型呢?实质上就是不使用具体数据类型(例如 int、double、float 等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业务逻辑的实现。泛型也是一种数据类型,只不过它是一种用来代替所有类型的“通用类型”

我们通常如何实现一个通用的交换函数呢?

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}
void Swap(char& left, char& right)
{
    char temp = left;
    left = right;
    right = temp;
}
......

Swap函数能实现各种类型的变量交换,但是只要类型不同就需要重新写一个

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  • 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
  • 代码的可维护性比较低,一个出错可能所有的重载均出错,那能否告诉编译器一个模版,让编译器根据不同的类型利用该模版来生成代码呢?

可以的,C++语法中有了模板:

函数模板

函数模板概念

所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。 这个通用函数就称为 函数模板(Function Template) 。函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

函数模板格式

template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}

template<typename T>
//或者 template<class T>
void Swap(T& x1, T& x2)
{
    T temp = left;
    left = right;
    right = temp;
}

T1,T2等等是什么类型现在也不确定,一会用的时候才能确定

注意:

typename是用来定义模板参数关键字,也可以使用class

函数模板的原理

函数模板本身并不是函数,是编译器根据调用的参数类型产生特定具体类型函数的模具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器,我们看下面的例子:

template<class T>
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	char A = 'a';
	char B = 'b';
	Swap(A,B);
	return 0;
}

image-20211024163521112

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然
后产生一份专门处理int类型的代码,对于字符类型也是如此。

然而当我们在写了函数时,不会进入模板函数里,没有写具体的函数时,就会进入模板函数里,我们看下面的例子:

void Swap(int& x, int& y)
{
	int temp = x;
	x = y;
	y = temp;
}
template<class T>
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	char A = 'a';
	char B = 'b';
	Swap(A,B);
	return 0;
}

我们进行调式:

模板

我们可以看到int类型的交换函数我们写了,调用时调用的是我们写的,而char类型的我们没写,就用了模板。

那么这里调用的是模板函数吗?

不是的,实际上这里会有两个过程

1、模板推演,推演T的具体类型是什么

2、推演出T的具体类型后实例化生成具体的函数

上面的代码实例化生成了下面的函数:

void Swap(char& x, char& y)
{
	char temp = x;
	x = y;
	y = temp;
}

真正调用的还是两个函数,但是其中的一个函数不是我们自己写的,而是我们给了编译器一个模板,然后编译器进行推演在编译之前实例化生成三个对应的函数,模板是给编译器用的,编译器充当了写函数的工具:

image-20211024161726234

可以看到这里是调用了Swap<char>函数

在C++当中,其实内置类型也可以像自定义类型那样这样初始化:

int a(1);
int(2);//匿名

image-20211024162224522

void Swap(T& x1, T& x2)
{
    T temp(x1);
    x1 = x2;
    x2 = x1;
}

所以模板还可以这样写,可以使内置类型和自定义类型兼容:

void Swap(T& x1, T& x2)
{
    T temp(x1);
    x1 = x2;
    x2 = x1;
}

我们来具体看一看函数模板的实例化:

函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

隐式实例化:让编译器根据实参推演模板参数的实际类型

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.0, d2 = 20.0;
    Add(a1, a2);
    Add(d1, d2);
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
    Add(a1, d2);
    return 0;
}

该语句是不能够通过编译的,因为在编译期间,当编译器看到该实例化时,用a1去推T是int,而用d2去推是double,但是模板参数列表里只有一个T,编译器不能明确该T是int还是double,T是不明确的,所以编译器会报错

那么怎么处理呢?

解决方式:

1、调用者自己强制转换

//实参去推演形参的类型
Add(a1, (int)d2);
Add((double)a1,d2);

这里可以将d2先强制类型转换,然后再进行推演;或者将a1先强制类型转换再进行推演

2、使用显式实例化

//实参不需要去推演形参的类型,显式实例化指定T的类型
Add<int>(a1, d2);
Add<double>(a1,d2);

这种方式是显式实例化指定T的类型

显式实例化在哪种场景可用呢?看下面的这种场景:

class A
{
    A(int a=0):_a(a)
    {}
private:
    int _a;
};
template<class T>
T func(int x)
{
    T a(x);
    return a;
}
int main()
{
    func<A>(1);
    func<int>(2);
    return 0;
}

有些函数模板里面参数中没用模板参数,函数体内才有用到模板参数,此时就无法通过参数去推演T的类型,这时只能显示实例化

上面我们提了一点模板参数的匹配原则,下面我们具体看看模板参数的匹配原则:

模板参数的匹配原则

 一个非模板函数可以和一个同名的函数模板同时存在,此时如果调用地方参数与非模板函数完全匹配,则会调用非模板函数

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
    Add(1,2);//调用自己的函数
    return 0;
}

Add(1,2)参数是int类型,而我们有现成的int参数的Add函数,所以有现成的就用现成的,编译器也会偷懒

那么如果我们想让这里调用必须用模板呢?显式实例化:

Add<int>(1,2);

这样编译器就强制会用模板去实例化函数

一个非模板函数可以和一个同名的函数模板同时存在,此时如果调用地方参数与非模板函数不完全匹配,则会优先使用模板实例化函数

int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
    Add(1.1,2.2);//使用模板实例化函数
    return 0;
}

模板匹配原则总结:

有现成完全匹配的,那就直接调用,没有现成调用的,实例化模板生成,如果有需要转换类型才能匹配的函数(也就是不完全匹配),那么它会优先选择去实例化模板生成。

优先级:

完全匹配>模板>转换类型匹配

类模板

类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
	//类内成员定义
};

我们来看一个类模板的使用场景:

typedef int STDateType;
class Stack
{
private:
    STDateType* _a;
    int _top;
    int _capacity;
};
int main()
{
    Stack st1;
    Stack st2;
    return 0;
}

这是我们定义的栈数据结构,我们创建了两个栈对象,但是现在st1和st2的存储数据的类型都是int,要是想转换数据类型呢?

typedef double STDateType;

我们这样就转换了,但是我们要是想st1为int,st2为double呢:

Stack st1;//int
Stack st2;//double

此时需要写多个类,名字还得不一样,如下:

typedef int STDateType1;
typedef double STDateType2;
class IntStack
{
private:
    STDateType1* _a;
    int _top;
    int _capacity;
};
class DoubleStack
{
private:
    STDateType2* _a;
    int _top;
    int _capacity;
};

这样太麻烦了,那么什么办法可以解决呢?类模板可以解决:

//类模板
template<class T>
class Stack
{
private:
    T* _a;
    int _top;
    int _capaticy;
};
int main()
{
    //类模板的使用都是显式实例化
    Stack<double> st1;
    Stack<int> st2;
    return 0;
}

注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具

类模板的实例化

//类模板
template<class T>
class Stack
{
public:
    Stack(int capacity = 4)
        :_a(new T(capacity))
         ,_top(0)
         ,_capacity(capacity)
        {}
    ~Stack()
    {
        delete[] _a;
        _a = nullptr;
        _top = _capacity = 0;
    }
    void Push(const T& x)
    {
        //...
    }
private:
    T* _a;
    int _top;
    int _capaticy;
};
int main()
{
    //类模板的使用都是显式实例化
    Stack<double> st1;
    Stack<int> st2;
    return 0;
}

注意:类模板的使用都是显式实例化

假设我们想类里面声明和类外面定义成员函数呢?

//类模板
template<class T>
class Stack
{
public:
    Stack(int capacity = 4)
        :_a(new T(capacity))
         ,_top(0)
         ,_capacity(capacity)
        {}
    ~Stack()
    {
        delete[] _a;
        _a = nullptr;
        _top = _capacity = 0;
    }
    //假设我们想类里面声明和定义分离呢?
    void Push(const T& x);
private:
    T* _a;
    int _top;
    int _capaticy;
};
//在类外面定义
template<class T>
void Stack<T>::Push(const T& x);
{
    //...
}
int main()
{
    //类模板的使用都是显式实例化
    Stack<TreeNode*> st1;
    Stack<int> st2;
    return 0;
}

//在类外面定义
template<class T>
void Stack<T>::Push(const T& x);
{
    //...
}

在类外面定义我们必须要加模板的关键字,以及需要在实现的函数前面表明域Stack<T>。普通类,类名就是类型,对于类模板,类名不是类型,类型是Stack<T>,需要写指定

注意:

模板不支持把声明写到.h,定义写到.cpp,这种声明和定义分开实现的方式,会出现链接错误

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注猪先飞的更多内容!

原文出处:https://blog.csdn.net/attemptendeavor/article/details/121017

[!--infotagslink--]

相关文章

  • C++ STL标准库std::vector的使用详解

    vector是表示可以改变大小的数组的序列容器,本文主要介绍了C++STL标准库std::vector的使用详解,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2022-03-06
  • C++中取余运算的实现

    这篇文章主要介绍了C++中取余运算的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-23
  • 详解C++ string常用截取字符串方法

    这篇文章主要介绍了C++ string常用截取字符串方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • C++中四种加密算法之AES源代码

    本篇文章主要介绍了C++中四种加密算法之AES源代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。...2020-04-25
  • Json格式详解

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。JSON采用完全独立于语言的文本格式,这些特性使JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成...2021-11-05
  • C++ 整数拆分方法详解

    整数拆分,指把一个整数分解成若干个整数的和。本文重点给大家介绍C++ 整数拆分方法详解,非常不错,感兴趣的朋友一起学习吧...2020-04-25
  • C++中 Sort函数详细解析

    这篇文章主要介绍了C++中Sort函数详细解析,sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变...2022-08-18
  • C++万能库头文件在vs中的安装步骤(图文)

    这篇文章主要介绍了C++万能库头文件在vs中的安装步骤(图文),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-23
  • 深入解析WordPress中加载模板的get_template_part函数

    这篇文章主要介绍了WordPress中加载模板的get_template_part函数,其中重点讲解了其函数钩子的使用,需要的朋友可以参考下...2016-01-14
  • vue中的插槽详解

    这篇文章主要介绍了Vue中的插槽,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-10-19
  • 详解C++ bitset用法

    这篇文章主要介绍了C++ bitset用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-04-25
  • 浅谈C++中的string 类型占几个字节

    本篇文章小编并不是为大家讲解string类型的用法,而是讲解我个人比较好奇的问题,就是string 类型占几个字节...2020-04-25
  • mysql配置模板(my-*.cnf)参数详细说明

    mysql安装成功后有几个默认的配置模板,列表如下: my-huge.cnf : 用于高端产品服务器,包括1到2GB RAM,主要运行mysql my-innodb-heavy-4G.ini : 用于只有innodb的安装,最多有4GB RAM,支持大的查询和低流量 my-large.cnf : 用于...2015-03-15
  • Vue之计算属性详解

    这篇文章主要为大家介绍了Vue的计算属性,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助...2021-11-16
  • C++ Eigen库计算矩阵特征值及特征向量

    这篇文章主要为大家详细介绍了C++ Eigen库计算矩阵特征值及特征向量,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-04-25
  • C++ pair的用法实例详解

    这篇文章主要介绍了C++ pair的用法实例详解的相关资料,需要的朋友可以参考下...2020-04-25
  • VSCode C++多文件编译的简单使用方法

    这篇文章主要介绍了VSCode C++多文件编译的简单使用方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-03-29
  • C语言的基本语法详解

    本篇文章主要讲解C语言 基本语法,这里提供简单的示例和代码来详细讲解C语言的基本语法,开始学习C语言的朋友可以看一下,希望能够给你带来帮助...2021-09-18
  • C++中的循环引用

    虽然C++11引入了智能指针的,但是开发人员在与内存的斗争问题上并没有解放,如果我门实用不当仍然有内存泄漏问题,其中智能指针的循环引用缺陷是最大的问题。下面通过实例代码给大家介绍c++中的循环引用,一起看看吧...2020-04-25