多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C++:模板初阶详解

C++:模板初阶详解 前言为什么 C 要有模板我们可以从一个非常简单的Swap交换函数开始。如果没有模板为了交换不同类型的数据我们可能写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; }你会发现这几个函数除了int double char不同之外算法完全一样temp left left right right temp这就产生两个问题第一代码复用率低。每来一个新类型就要再写一遍第二可维护性差。如果交换逻辑需要修改就可能每一个重载版本都要改所以我们真正想要的是我只写一次“交换算法” ↓ 至于到底交换 int、double、char ↓ 让编译器自己生成对应版本这就是模板思想一 什么是泛型编程在具体讲解模板之前我们先了解一下泛型编程泛型编程是编写与类型无关的通用代码是代码复用的一种手段。模板就是泛型编程的基础。以前我们写int Add(int x, int y);这个函数只能处理int。而泛型编程想表达的是关心的是“怎么加”而不是“加的是哪种具体类型”于是我们把“类型”先抽象出来。你可以把模板想象成模具”同一个模具 ↓ 放入不同材料 ↓ 得到不同成品在 C 里就是同一个模板 ↓ 传入不同类型 ↓ 生成不同类型的代码二 函数模板到底是什么我们先了解最基本的函数模板templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; }函数模板代表一个函数家族它与具体类型无关使用时根据实参类型产生对应的具体函数版本注意这里T现在并不是某个真正的具体类型。它只是一个类型占位符。你可以理解成T “以后再告诉我是什么类型”于是T temp left;不是说存在一种真正叫T的数据类型而是说如果以后 T 是 int 这里就是 int temp 如果 T 是 double 这里就是 double temp 如果 T 是 char 这里就是 char temp三templatetypename T又是什么拆开来看templatetypename T意思可以理解成我接下来定义的是一个模板其中有一个类型参数名字叫T然后void Swap(T left, T right)表示left 和 right 的类型先不确定由以后调用时再决定。模板参数也可以有多个templatetypename T1, typename T2比如templatetypename T1, typename T2 T1 Add(T1 x, T2 y) { return x y; }此时T1 T2可以是不同类型。四typename和class有什么区别我们在之前学过了类与对象初步了解到了class的相关知识其实在这个地方我们也可以用class来定义模板类型参数的所以templatetypename T也可以写templateclass T所以templatetypename T和templateclass T在这个阶段可以看成一样五 模板本身并不是一个真正的函数函数模板是一个蓝图本身并不是函数而是编译器根据使用方式生成具体函数的模具例如你写templatetypename T void Swap(T a, T b) { T tmp a; a b; b tmp; }这时不要理解成已经生成了一个万能 Swap 函数更准确地说现在只有一张“生成函数的图纸”然后int a 1, b 2; Swap(a, b);编译器看到两个参数都是 int于是推导T int然后生成一个类似void Swap(int a, int b) { int tmp a; a b; b tmp; }如果又写double x 1.1, y 2.2; Swap(x, y);编译器又推导T double生成类似void Swap(double a, double b) { double tmp a; a b; b tmp; }所以真正过程是函数模板 ↓ 根据实参推导 T ↓ 生成具体函数 ↓ 调用具体函数六 模板实例化使用不同类型参数调用函数模板时产生具体版本的过程称为函数模板实例化例如Swap(a, b);如果a、b 是 int就产生Swapint这就是一次实例化。如果double x, y; Swap(x, y);又产生Swapdouble函数模板实例化又分两种隐式实例化 显式实例化1. 隐式实例化让编译器自己猜 T例如templateclass T T Add(const T left, const T right) { return left right; }然后int a1 10, a2 20; double d1 10.0, d2 20.0; Add(a1, a2); Add(d1, d2);第一句Add(a1, a2);编译器看到a1 → int a2 → int于是推导T int第二句Add(d1, d2);推导T double这就是隐式实例化你没有写Addint也没有写Adddouble而是编译器自己根据实参类型推导七 函数模板最容易出错的地方同一个 T 推导出了两个结果考虑templateclass T T Add(const T left, const T right) { return left right; }然后int a 10; double d 20.0; Add(a, d);这时候编译器开始推导。根据a得到T int但是根据d又得到T double问题来了T到底是谁模板只有T一个模板参数。它不可能同时T int T double所以编译器无法确定具体类型会产生错误八 解决混合类型问题1.方法一自己转换比如int a 10; double d 20.0;可以Add(a, (int)d);现在参数变成int int于是T int没有冲突2.方法二显式指定模板参数另一种方法Addint(a, d);这里int相当于直接告诉编译器不需要你推导了我明确指定T int。这就是显式实例化在函数名后面的中直接指定模板参数的实际类型所以Addint(a, d);相当于指定T int于是模板生成int Add(const int left, const int right);此时第二个参数原本是double d因为目标参数类型已经确定为int编译器会尝试进行类型转换。显式实例化之后如果实际参数与指定类型不完全相同编译器会尝试隐式转换无法转换才会报错3.方法三使用两个模板参数templateclass T1, class T2 T1 Add(T1 left, T2 right) { return left right; }这时候Add(1, 2.0);就不再要求两个参数必须对应同一个T。而是T1 int T2 double所以能够成功实例化九 普通函数和函数模板1.能不能同名答案是可以比如int Add(int left, int right) { return left right; } templateclass T T Add(T left, T right) { return left right; }这里同时存在普通函数 Add 函数模板 Add是允许的2.谁先被调用上方代码中同时存在普通函数 Add 函数模板 Add那么到底调用谁答案是普通函数和模板一样匹配时优先普通函数如果非模板函数和函数模板都能匹配并且匹配程度相同那么优先调用非模板函数。 06.模板初阶所以int Add(int left, int right); templateclass T T Add(T left, T right);执行Add(1, 2);两个都能处理。普通函数Add(int, int)直接完美匹配。模板也可以实例化Addint(int, int)也完全匹配。既然一样好优先选择普通函数所以不会没必要地再从模板生成一个版本3.那如果我们想强制调用模板该怎么做呢1.显示实例化我们可以写Addint(1, 2);只要你显式写了int就明确表示我要用模板版本了2.模板匹配更佳例如int Add(int left, int right) { return left right; }同时templateclass T1, class T2 T1 Add(T1 left, T2 right) { return left right; }调用Add(1, 2.0);普通函数需要把2.0 double转换成int才能调用而模板可以直接生成T1 int T2 double于是模板版本匹配得更准确所以如果模板能够生成一个匹配程度更好的函数就选择模板版本十 类模板1.类模板的定义基本格式templateclass T1, class T2, ..., class Tn class 类模板名 { // 类成员 };例如templatetypename T class Stack { public: Stack(size_t capacity 4) { _array new T[capacity]; _capacity capacity; _size 0; } private: T* _array; size_t _capacity; size_t _size; };这里最关键T* _array;以前int* _array;只能保存int。现在T* _array;具体存什么等实例化时决定2.StackT到底发生了什么例如Stackint st1;编译器看到T int那么可以帮助理解成生成了一个具体版本class Stackint { private: int* _array; size_t _capacity; size_t _size; };而Stackdouble st2;相当于T double于是内部double* _array;所以同一份类模板templatetypename T class Stack既能产生Stackint Stackdouble这就是类模板的意义3.类模板中Stack本身不是一个真正的类型Stack是类模板名Stackint才是类型所以Stack st;按照规则是不完整的因为编译器不知道T 是什么应该写Stackint st1;或者Stackdouble st2;4.函数模板和类模板在实例化上的区别函数模板templateclass T void Func(T x);调用Func(10);编译器通常可以根据10 是 int自己推导T int所以可以不写Funcint(10);但是类模板实例化时需要Stackint显式把类型写到里面5.类模板成员函数放到类外怎么写类templatetypename T class Stack { public: void Push(const T data); };如果想在类外定义push函数可以这么写templateclass T void StackT::Push(const T data) { ... }一定注意不是void Stack::Push(...)而是void StackT::Push(...)为什么因为Stack本身只是模板名字。我们现在定义的是某个 T 对应的 StackT 的成员函数所以必须写StackT::而且前面还要再次声明templateclass T
返回列表