目录
2.3、使用举例 比如说我们需要建立一个可以装不同数据的栈,那么我们也可以用到函数模版:
初阶 前言
随着C++的学习,讲了C++的发展过程、流插入、流提取、函数缺省值、类与构造等等。接下来学习C++很方便的玩意,函数模版。函数模版就像是模具一样,C++会自动用模版编译出合适的函数供程序员使用。以前不同类型相同操作的函数都能通过函数模版,只写一个来解决。这么说来,怪不得祖师爷会写出函数重载这样的优化。因为如此,函数模版大有作用。
一、new和delete
1、简介
new和delete是C++中新增的关键字,new用来申请堆上的空间,delete用来释放堆上的空间。在C语言中我们学过了3个函数,分别是“malloc”“ralloc”“free”,前两个用来申请空间,free用来释放空间。
既然C语言中已经有了申请空间的函数,为什么C++中又单独开发出来一套系统来申请空间呢?接下来就来聊聊他们之间的区别吧。
2、malloc/free与new/delete区别:
(1)malloc/free是函数,new/delete是操作符。
(2)malloc申请空间就只是申请空间不会初始化,new会申请空间并初始化空间。
(3)malloc需要手动计算申请空间的大小,new能够通过类型自动计算需要申请大小。
(4)malloc申请空间失败会返回NULL,new申请空间失败不会返回NULL但是需要捕获异常(这里需要用到try/catch)。
(5)malloc只是开空间,free只会释放空间、new会调用构造函数初始化空间,delete会调用析构函数释放空间。
3、举例
如果正常的使用new和delete,也就是说配套使用,方法如下:
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)->" << _a <<endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
int main()
{
A* exp = new A;
delete exp;
exp = new A[4]{1,2,3,4};
delete[] exp;
exp = nullptr;
return 0;
}
如举例所示,在new后面接上“类型”就能够直接开辟一个类的空间,释放空间接delete和它的指针。如果需要开辟多个空间就像数组一样在new后面接上“类型[]”,释放空间就需要接“delete[]”和它的指针。同时如果想修改初始化中的成员可以增加“{}”,里面写变量。那么调用构造的时候会直接使用。
但是为什么不直接用“delete”而是需要增加“delete[]”?这是因为用new申请多个空间的时候会在所开空间前额外开4个字节的空间用来记录调用构造和析构函数的次数,方便处理。这个时候使用delete会出错,程序会直接结束。因为多开了4字节。到了现在C++的进步,如果不写析构函数,那么优化做的比较多的编译器就不会多生成这4个字节“delete”也能够使用。
如果将类“A”的析构函数去掉,那么使用“delete”释放空间也是可行的。例如:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)->" << _a <<endl;
}
private:
int _a;
};
另外,malloc/free也可以和new/delete混用,但是不会调用构造和析构函数,如果类里面有申请空间,就不能使用例如:
class B
{
public:
B()
{
_b = new int(0);
}
~B()
{
delete _b;
}
private:
int* _b;
};
这是因为malloc/free不会调用析构和构造去释放申请的空间,造成内存泄漏,所以最好将new和delete配套使用。
二、模版
1、函数模版
1.1、简介
函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。
它的出现虽然对于编译器来说增加了开销,但是却节省了程序员的开销,让程序员写一个功能就能一劳永逸。
1.2、举例
在讲到C语言函数的时候,我们学到的有加法函数,就是将两个数加起来,最后变成一个数返回。那么这里我们也用这个函数举例。
template<class T>
T Add(T& n1, T& n2)
{
return n1 + n2;
}
这里的“tempelate”是C++中新增的关键字,这个关键字能够用来定义模版变量的类型。在上述函数中,“n1”和“n2”是同一种类型,他们都属于类型“T”。这里的“T”可以根据需要取名字,就和类一样。这个类型“class”(切记:不能使用struct代替class)也可以使用“typename”,所以这个函数用以下写法也是一样的:
template<typename T1>
T1 Add(T1& n1, T1& n2)
{
return n1 + n2;
}
1.3、函数模板的实例化
在函数的使用方面分为两种方法,第一种是推导实例化、另一种是显示实例化。接下来会在例子中主要说明两种实例化的不同操作方式。模版函数就采用上面写的“Add”函数:
#include <iostream>
using namespace std;
template<class T>
T Add(T n1, T n2)
{
return n1 + n2;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
// 推导实例化
cout << Add(a1, a2) << endl;
cout << Add(a1, (int)d1) << endl;
cout << Add((double)a1, d1) << endl << endl;
// 显示实例化
cout << Add<int>(a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
return 0;
}
推导实例化 就是模版的参数程序员不给出,由传入函数的参数以及编译器的推导决定。相反显示实例化就是程序员固定了模版的类型,计算机直接将这个类型交给模版就行了。注意返回值无法影响编译器的推导,只有输入的形参才行。
请注意,如果使用推导实例化就不能让编译器搞不清楚变量实例化成什么类型。如下传法都是错误的:
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
// 推导实例化
cout << Add(a2, d2) << endl;
cout << Add(a1, d1) << endl;
cout << Add(a1, d2) << endl << endl;
return 0;
}
编译器会觉得,到底是“int”还是“double”呢?就直接报错退出了。所以这里建议使用显示实例化,编译器也不含糊,不容易出错。
1.4、模板参数的匹配原则
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这 个非模板函数// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(1, 2); // 与非模板函数匹配,编译器不需要特化 Add<int>(1, 2); // 调用编译器特化的Add版本 }
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模 板// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T1, class T2> T1 Add(T1 left, T2 right) { return left + right; } void Test() { Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数 }
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
2、类模版
除了函数能够使用模版之外,类也能使用模版。他们的作用都是相同的,减少程序员的代码量。使用方法也类似。所以这里就直接举例了。
2.1、类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
2.2、类模板的实例化
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double
2.3、使用举例
比如说我们需要建立一个可以装不同数据的栈,那么我们也可以用到函数模版:
namespace zhc
{
template<typename T>
class Stack
{
public:
Stack(int n = 4)
:_array(new T[n])
,_capacity(n)
,_size(0)
{}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
void push(const T& x)
{
if(_size == _capacity)
{
T* tmp = new T[_capacity * 2];
memcpy(tmp, _array, sizeof(T) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
void pop()
{
if(_size != 0)
{
--_size;
}
}
T top()
{
if(_size != 0)
{
return _array[_size - 1];
}
else{
return T(0);
}
}
bool empety()
{
return !_size;
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
}
int main()
{
zhc::Stack<int> st_int;
zhc::Stack<double> st_double;
st_int.push(1);
st_int.push(2);
st_int.push(3);
st_int.push(4);
cout << st_int.top() << endl;
st_int.pop();
st_int.pop();
st_int.pop();
cout << st_int.top() << endl;
st_double.push(1.123);
cout << st_double.top() << endl;
return 0;
}
这样我们的栈也能够使用了,和函数不同的是类模板都需要显示实例化,请注意。
这里我也用到了命名空间,这样就不会和库里的函数重叠。
创建了两种栈“int”和“double”的,同时输入输出一些栈内元素,输出上和代码预期相同。首先入4个数到“int”栈,然后取栈顶元素输出。然后从“int”栈中删除3个再次输出栈顶元素。随后输入一个元素到“double”栈,然后输出它。
在模版之中的类型也可以是自定义类型,但相应的就需要写更多的代码来确保运行。
学习本节我觉得最大的作用就是为接下来学习stl标准库做准备
.
进阶 补充
模版还可以更加深化的去说。这设计到一些底层的实现,不过大致的逻辑是相通的,同时也是给继承和多态做一个铺垫。
三、非类型模板参数
四、模版特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些 错误的结果 ,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板// 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } int main() { cout << Less(1, 2) << endl; // 可以比较,结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; // 可以比较,结果正确 Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }
可以看到,Less 绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1 指向的 d1 显然小于 p2 指向的 d2 对象,但是 Less 内部并没有比较 p1 和 p2 指向的对象内容,而比较的是 p1 和 p2 指针的地址,这就无法达到预期而错误。此时,就 需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式 。模板特化中分为 函数模板特化 与 类模板特化 。
C++中的模版特化,是基于已有模版的基础上,对模版假定内容进行固定。就像我们使用模版的时候能够进行显示实例化一样,模版除了计算机自己写,也可以经由程序员自己编写。
自己编写的情况出现为,在特定的情况下走特定的程序,与普通模版做出区分。例如我们之前所做的日期类比较,不是单独比较年或者月和日。而是优先的比较年,随后是月,最后是日。这种情况也发生在传入内容是指针的时候,由于地址是不确定的,如果直接比较那么大小就会出错,我们需要比较指针指向的内容。这样我们就可以用模版的特化,让程序走指针特化的模版,从而达到解引用后比较目的。
1、函数模板特化
函数模板的特化步骤:1. 必须要先有一个基础的函数模板2. 关键字 template 后面接一对空的尖括号 <>3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型4. 函数形参表 : 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。// 函数模板 -- 参数匹配 template<class T> bool Less(T left, T right) { return left < right; } // 对Less函数模板进行特化 template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; } int main() { cout << Less(1, 2) << endl; Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了 return 0; }
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该 函数直接给出。bool Less(Date* left, Date* right) { return *left < *right; }
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
2、类模板特化
2.1、全特化
全特化即是将模板参数列表中所有的参数都确定化。
template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; template<> class Data<int, char> { public: Data() { cout << "Data<int, char>" << endl; } private: int _d1; char _d2; }; void TestVector() { Data<int, int> d1; Data<int, char> d2; }
2.2、偏特化/半特化
偏特化或者叫半特化,这样的函数存在于函数模版超过两个的函数中,将一部分函数模版的类型进行固定。除了向int、char或者自定义类型之外。也可以对模版进行这个模版是指针或者是引用的声明。例如对于如下类模板Date来说:
template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; };
半特化包括,将其中部分模版参数类型固定:
// 将T2固定为int template <class T1> class Data<T1, int> { public: Data() { cout << "Data<T1, int>" << endl; } private: T1 _d1; int _d2; };
将模板的内容声明为指针:
// 模板的内容声明为指针 template <typename T1, typename T2> class Data <T1*,T2*> { public: ta() { cout << "Data<T1*, T2*>" << endl; } private: T1 _d1; T2 _d2; };
将模版的类型声明为引用:
// 模板的内容声明为引用 template <typename T1, typename T2> class Data <T1&, T2&> { public: Data(const T1& d1, const T2& d2) : _d1(d1) , _d2(d2) { cout << "Data<T1&, T2&>" << endl; } private: const T1 & _d1; const T2 & _d2; };
对于指针特化和引用特化,T1/T2如果载入int*不表示int*,而是表示int。这是因为系统希望T1能够表示更多的类型。如果T1表示int*,那么就没法表示int,但是是int能够表示int*。
根据全特化和半特化来说,全特化的优先级更高。半特化就相当于是家里洗好的菜,还要料理之后才能吃,但是全特化作为凉饭是能够直接吃的。
#include <iostream> using namespace std; template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 将T2固定为int template <class T1> class Data<T1, int> { public: Data() { cout << "Data<T1, int>" << endl; } private: T1 _d1; int _d2; }; // 模板的内容声明为指针 template <typename T1, typename T2> class Data <T1*,T2*> { public: ta() { cout << "Data<T1*, T2*>" << endl; } private: T1 _d1; T2 _d2; }; // 模板的内容声明为引用 template <typename T1, typename T2> class Data <T1&, T2&> { public: Data(const T1& d1, const T2& d2) : _d1(d1) , _d2(d2) { cout << "Data<T1&, T2&>" << endl; } private: const T1 & _d1; const T2 & _d2; }; int main() { Data<double, int> d1; Data<int, double> d2; Data<int *, int*> d3; Data<int&, int&> d4(1, 2); return 0; }
说到这里,我们来看看模版特化时,计算机执行的特点吧。
五、模版选择优先级
1、普通函数 模版函数 模版特化函数
对于计算机来说,由模版到具体的函数是需要消耗资源的。就像做饭一样,模版就好比是菜场新鲜的菜,需要买回家烹饪才能吃。而对于模版特化来说,就像是闲饭,热热就可以吃。普通函数就是新鲜美味的做好的饭,可以直接吃。
计算机也是聪明的很,如果对得上号,就表示能够在这些函数中做出选择。这个时候有新鲜的就会优先吃新鲜的,没有新鲜的就吃旧的,没有旧的就只好去菜场买自己做了吃。编译器在选择同名函数的时候优先级是:“普通函数”>“模版特化函数”>“模版函数。
根据以下代码:
#include <iostream>
using namespace std;
template<typename Type>
Type Max(const Type &a, const Type &b)
{
cout << "This is Max<Type>" << endl;
return a > b ? a : b;
}
template<>
int Max<int>(const int &a, const int &b)
{
cout << "This is Max<int>" << endl;
return a > b ? a : b;
}
template<>
char Max<char>(const char &a, const char &b)
{
cout << "This is Max<char>" << endl;
return a > b ? a : b;
}
int Max(const int &a, const int &b)
{
cout<< "This is Max" <<endl;
return a > b ? a : b;
}
int main()
{
Max(10, 20);
Max(12.34, 23.45);
Max('A', 'B');
Max<int>(20, 30);
return 0;
}
上述顺序:
// Max(10, 20);->int Max(const int &a, const int &b);
// 能够调用合适的普通函数就直接调用
// Max(12.34, 23.45);->Type Max(const Type &a, const Type &b);
// 没有浮点数的特化,就只能自己做。调用原始模版
// Max('A', 'B');->char Max<char>(const char &a, const char &b);
// 有特殊模版能够支持char类型,识别后调用它
// Max<int>(20, 30);->int Max<int>(const int &a, const int &b);
// 声明调用模版int,并且优int特化的模版,调用模版int特化
2、有关指针特化的注意事项
指针的特化有一个非常奇怪的现象。当使用指针特化,如果对指针指向的内容增加const的时候,计算机反而不会走这个const指针特化。
template<typename T>
T Add(T x, T y)
{
cout << "T Add(T x, T y)" << endl;
return x + y;
}
template<typename T>
T Add(T* x, T* y)
{
cout << "T Add(T* x, T* y)" << endl;
return *x + *y;
}
template<typename T>
T Add(const T*& x, const T*& y)
{
cout << "T Add(const T*& x, const T*& y)" << endl;
return *x + *y;
}
int main()
{
int a = 10, b = 20;
int* x = &a;
int* y = &b;
Add(x, y);
return 0;
}
对于以上代码,我们一般会认为,调用指针内容的时候回去调用:
T Add(const T*& x, const T*& y)
但是实际上计算机认为这不是一个意思,虽然这个是个指针,但是const修饰的不是指针本身而是指针指向的内容。从结果上来说,编译器不会走它,如果没有第二个模版会调用前面第一个模版。这样就会出错,因为两个指针不能相加。增加了第二个模版才不会报错,计算机也会走第二个模版:.
所以,调用特化函数的时候,const必须修饰传入的参数类型。比如这里修饰的是指针,const就应该修饰指针,而不是指针指向的内容。例如下面的第四个模版:
// 1
template<typename T>
T Add(T x, T y)
{
cout << "T Add(T x, T y)" << endl;
return x + y;
}
// 2
// template<typename T>
// T Add(T* x, T* y)
// {
// cout << "T Add(T* x, T* y)" << endl;
// return *x + *y;
// }
// 3
template<typename T>
T Add(const T*& x, const T*& y)
{
cout << "T Add(const T*& x, const T*& y)" << endl;
return *x + *y;
}
// 4
template<typename T>
T Add(T* const & x, T* const & y)
{
cout << "T Add(T* const & x, T* const & y)" << endl;
return *x + *y;
}
int main()
{
int a = 10, b = 20;
int* x = &a;
int* y = &b;
Add(x, y);
return 0;
}
六、模板分离编译
1、什么是分离编译
2、模板的分离编译
模版最好不要声明和定义分离。这是由于编译器的4个过程决定的。
编译器编译程序分为4个过程:
(1)预处理.i
头文件展开、预处理、条件编译、去注释等
(2)编译.s
检查语法、生成汇编代码
(3)汇编.o
汇编码转化为二进制
(4)链接.exe
目标文件合并在一起生成可执行程序,并把需要的函数地址等连接上。
而模版如果分离编译在不同文件、那么在编译的阶段他不会去向声明的部分去要有哪些需要生成函数,每个文件单独编译,所以cpp文件没有实例化出来。因为编译阶段有声明所以能够通过该阶段,但是在链接阶段就会出错,因为找不到对应的实例化函数。如果不分开,那么编译阶段才知道。