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

目录
  • 模板初阶
    • 泛型编程
    • 函数模板
      • 函数模板概念
      • 函数模板格式
      • 函数模板的原理
      • 函数模板的实例化
      • 模板参数的匹配原则
  • 类模板
    • 类模板的定义格式
    • 类模板的实例化
  • 总结

模板初阶

泛型编程

在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术。什么是泛型呢?实质上就是不使用具体数据类型(例如 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;
}

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用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;
}

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

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

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

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

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,这种声明和定义分开实现的方式,会出现链接错误

总结

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

(0)

相关推荐

  • C++中模板(Template)详解及其作用介绍

    目录 概述 函数模板 类模板 模板类外定义成员函数 类库模板 抽象和实例 概述 模板可以帮助我们提高代码的可用性, 可以帮助我们减少开发的代码量和工作量. 函数模板 函数模板 (Function Template) 是一个对函数功能框架的描述. 在具体执行时, 我们可以根据传递的实际参数决定其功能. 例如: int max(int a, int b, int c){ a = a > b ? a:b; a = a > c ? a:c; return a; } long max(long a, l

  • C++中模板和STL介绍详解

    目录 一.模板 1.1.函数模板 1.1.1.两种函数模板的实例化 1.1.2.模板参数的匹配原则 1.2.类模板 二.STL 总结 一.模板 对于一个交换函数,虽然C++支持函数重载,我们可以对多个交换函数起相同的名字: void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } void Swap(double& left, double& right) { doub

  • C++ 标准模板类详解

    目录 1 标准模板库 2.泛型编程 总结 1 标准模板库 STL提供了表示容器.迭代器.函数对象和算法的模板. 容器:类似数组存储若干值,切实同质的: 迭代器:遍历容器的对象,类似遍历数组的指针,广义指针: 算法:完成特定的任务: 函数对象:类对象或函数指针. 模板类 vector erase() 删除矢量中给定区间元素.接受两个迭代器参数(该参数定义了要删除的区间),迭代器1指向区间起始处,迭代器2指向区间终止处的后一个位置. // delete first and second elemen

  • 详解C++ 模板编程

    类型模板 类型模板包括函数模板和类模板,基本上是C++开发人员接触模板编程的起点. 下面代码演示了函数模板和类模板的使用方法: // 函数模板 template<typename T> T add(const T& a, const T& b) { return a + b; } // 类模板 template<typename T> class Point { private: T x[3]; ... }; 类型模板以template开始声明,尖括号内的typen

  • 详解c++中的trait与policy模板技术

    目录 概述 trait模板技术 用模板参数来传递多种trait policy模板技术 模板化的policy trait模板与policy模板技术的比较 概述 我们知道,类有属性(即数据)和操作两个方面.同样模板也有自己的属性(特别是模板参数类型的一些具体特征,即trait)和算法策略(policy,即模板内部的操作逻辑).模板是对有共性的各种类型进行参数化后的一种通用代码,但不同的具体类型又可能会有一些差异,比如不同的类型可能会有自己的不同特征和算法实现策略. trait模板技术 当在模板代码中

  • C++11新特性之变长参数模板详解

    目录 C++11 变长参数模板 变长函数参数包 如何解参数包 sizeof()获得函数参数个数 递归模板函数 变参模板展开 结论 C++11 变长参数模板 在C++11之前,无论是类模板 还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数: 这已经大大提升了代码的复用! 在C++11之后,加入了新的表示方 法,允许任意个数.任意类别的模板参数,同时也不需要在定义时将参数的个数固定.更加像"黑魔法"了. template<typename... Ts> class

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

    目录 模板初阶 泛型编程 函数模板 函数模板概念 函数模板格式 函数模板的原理 函数模板的实例化 模板参数的匹配原则 类模板 类模板的定义格式 类模板的实例化 总结 模板初阶 泛型编程 在计算机程序设计领域,为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,人们发展的泛型及泛型编程技术.什么是泛型呢?实质上就是不使用具体数据类型(例如 int.double.float 等),而是使用一种通用类型来进行程序设计的方法,该方法可以大规模的减少程序代码的编写量,让程序员可以集中精力用于业

  • 一篇文章带你了解C++智能指针详解

    目录 为什么要有智能指针? 智能指针的使用及原理 RALL shared_ptr的使用注意事项 创建 多个 shared_ptr 不能拥有同一个对象 shared_ptr 的销毁 shared_ptr 的线程安全问题 shared_ptr 的循环引用 unique_ptr的使用 unique_ptr 总结 为什么要有智能指针? 因为普通的指针存在以下几个问题: 资源泄露 野指针 未初始化 多个指针指向同一块内存,某个指针将内存释放,别的指针不知道 异常安全问题 如果在 malloc和free 或

  • 一篇文章带你入门Java之编程规范

    目录 引言 命名规则 代码排版 1.代码缩进对齐 2.遇到分号换行 3.大括号.括号等成对出现 4.加上注释 Java注释 注释的作用 注释的3种类型 给代码加上单行注释 给代码加上多行注释 给代码加上文档注释 总结 引言 ♀ 小AD:小明哥,之前你教我的Helloworld实例我写完了,超级简单呢,我一下子就搞定了 ♂ 明世隐:是吗,那不错哦. ♀ 小AD:我还举一反三.自由发挥了一波 ♂ 明世隐:这波怎么说? ♀ 小AD:怎么说?我说我在第5层,你信不 ♂ 明世隐:我信,你举的什么.反的什么

  • 一篇文章带你入门java模板模式

    目录 Java设计模式-模板模式 什么是模板模式? 总结: Java设计模式-模板模式 什么是模板模式? 模板模式,顾名思义,就是通过模板拓印的方式. 定义模板,就是定义框架.结构.原型.定义一个我们共同遵守的约定. 定义模板,我们的剩余工作就是对其进行充实.丰润,完善它的不足之处. 定义模板采用抽象类来定义,公共的结构化逻辑需要在抽象类中完成,只将非公共的部分逻辑抽象成抽象方法,留待子类充实实现. 下面首先通过一个简单的程序来分析一下,例如:现在有三种类型:猪.机器人.人: 猪具备三种功能:吃

  • 一篇文章带你了解C++面向对象编程--继承

    目录 C++ 面向对象编程 -- 继承 总结 C++ 面向对象编程 -- 继承 "Shape" 基类 class Shape { public: Shape() { // 构造函数 cout << "Shape -> Constructor" << endl; } ~Shape() { // 析构函数 cout << "Shape -> Destructor" << endl; } vo

  • 一篇文章带你入门java网络编程

    目录 基于TCP/IP协议的通信 基于UDP协议的通信 InetAddress 案例演示 (简易聊天室) UDP 演示 总结 基于TCP/IP协议的通信 TCP/IP协议 TCP/IP协议.会在通信两端建立连接(虚拟连接),用于发送和接收数据 TCP/IP协议是一种可靠的网络协议,它通过重发机制来保证这种可靠性 通信的实现 ServerSocket用来监听来自客户端的连接,当没有连接时,它处于阻塞状态 客户端使用Socket连接到指定的服务器 基于UDP协议的通信 UDP协议 UDP协议不会在通

  • 一篇文章带你顺利通过Python OpenCV入门阶段

    目录 1. OpenCV 初识与安装 2. OpenCV 模块简介 3. OpenCV 图像读取,显示,保存 4. 摄像头和视频读取,保存 5. OpenCV 常用数据结构和颜色空间 6. OpenCV 常用绘图函数 7. OpenCV 界面事件操作之鼠标与滑动条 8. 图像像素.通道分离与合并 9. 图像逻辑运算 10. 图像 ROI 与 mask 掩膜 11. 图像几何变换 12. 图像滤波 13. 图像固定阈值与自适应阈值 14. 图像膨胀腐蚀 15. 边缘检测 16. 霍夫变换 17.

  • 一篇文章带你了解Java Spring基础与IOC

    目录 About Spring About IOC Hello Spring Hello.java Beans.xml Test.java IOC创建对象的几种方式 Spring import settings Dependency Injection 1.构造器注入 2.set注入 3.拓展注入 P-namespcae&C-namespace Bean scopes singleton prototype Bean的自动装配 byName autowire byType autowire 小结

  • 一篇文章带你吃透Vue生命周期(结合案例通俗易懂)

    目录 1.vue生命周期 1.0_人的-生命周期 1.1_钩子函数 1.2_初始化阶段 1.3_挂载阶段 1.4_更新阶段 1.5_销毁阶段 2.axios 2.0_axios基本使用 2.1_axios基本使用-获取数据 2.2_axios基本使用-传参 2.3_axios基本使用-发布书籍 2.4_axios基本使用-全局配置 3.nextTick和refs知识 3.0$refs-获取DOM 3.1$refs-获取组件对象 3.2$nextTick使用 3.3$nextTick使用场景 3.

  • 一篇文章带你彻底搞懂VUE响应式原理

    目录 响应式原理图 编译 创建compile类 操作fragment 获取元素节点上的信息 获取文本节点信息 操作fragment 响应式 数据劫持 收集依赖 响应式代码完善 Dep类 全局watcher用完清空 依赖的update方法 需要注意的一个地方 双剑合璧 总结 首先上图,下面这张图,即为MVVM响应式原理的整个过程图,我们本篇都是围绕着这张图进行分析,所以这张图是重中之重. 响应式原理图 一脸懵逼?没关系,接下来我们将通过创建一个简单的MVVM响应系统来一步步了解这个上图中的全过程.

随机推荐