🥁作者: 华丞臧
📕专栏:【C++】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。推荐一款刷题网站 👉LeetCode
文章目录
- 模板初阶
- 泛型编程
- 函数模板
- 函数模板概念
- 函数模板格式
- 函数模板原理
- 函数模板实例化
- 多个参数
- 模板参数的匹配原则
- 类模板
- 类模板定义格式
- 类模板的实例化
- 模板的声明和定义分离
- []的重载
- STL初识
- 什么是STL
- STL版本
- STL的六大组件
- STL重要性
- STL的缺陷
模板初阶
泛型编程
首先来看下面一段代码:
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;
}
两个类型的交换是常见一段代码,如上使用函数重载虽然可以实现,但是一段代码只能实现一种类型的数据交换,其缺点也很明显:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数;
- 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
为了解决上述问题,C++中提供了一个模具,通过给这个模具中填充不同的类型,来获得不同类型的代码。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板
函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
函数模板格式
C++中提供了一个新的关键字template,用来定义函数模板,template后面跟一个尖括号用来定义类型,这个类型不是具体的类型而是一个模板类型(虚拟类型)。
template<typename T1, typename T2...,typename Tn>
返回值类型 函数名(参数列表)
{}
//也可以使用class
template<class T1, class T2..., class Tn>
返回值类型 函数名(参数列表)
{}
//例子
template<class T>
void swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
//使用
int main()
{
int a = 1, b = 2;
swap(a, b);
double c = 1.1, d = 2.2;
swap(c, d);
char e = 'a', f = 'b';
swap(e, f);
return 0;
}
注意:typename是用来定义模板参数关键字,也可以使用class,但是切记不能使用struct代替class。
函数模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
函数模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板实例化分为:隐式实例化和显式实例化。
- 隐式实例化:让编译器根据实参推演模板参数的实际类型;
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);
//该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型;
//通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
//编译器无法确定此处到底该将T确定为int 或者 double类型而报错。
//注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
// Add(a1, d1);
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a, (int)d);
return 0;
}
- 显式实例化:在函数名后的<>中指定模板参数的实际类型。
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
多个参数
template<class T1, class T2>
T Add(const T1& left, const T2& right)
{
return left + right;
}
模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数;
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生一个实例,如果模板可以产生一个具有更好匹配的函数,那么将选择模板;
- 模板函数不允许自动类型转换,但普通函数可以自动进行类型转换。
类模板
我们知道
typedef
可以给类型重命名,typedef
很好的解决了代码的可维护性,但不是所谓的泛型编程,它不能满足泛型编程的要求;假设有一个栈,如果我们要在栈中存放不同类型的数据就需要用到模板。
类模板定义格式
类模板的成员函数都是模板函数。
//例子
template<typename T>
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" <<capacity<<endl;
_a = (T*)malloc(sizeof(T)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(const T& x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
// 类模板一般没有推演时机,函数模板实参传递形参,推演模板参数
// 类模板一般显式实例化
// 他们是同一个模板实例化出来的
// 但是模板参数不同,他们就是不同的类型
Stack<int> st1;
Stack<double> st2;
return 0;
}
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
模板的声明和定义分离
注意模板不支持分离编译;这涉及到编译的过程,一个程序编译需要经过预编译、编译、汇编、链接四个过程,在链接之前的三个过程(预编译、编译、汇编)都是单线进行的。如果分离编译,那么用的地方在实例化但是只有声明,没有定义;而定义的地方无法完成实例化,只有定义,定义的地方只有模板而没有实例化的对象,因此链接时定义文件.cpp
的符号表中并没有对应函数的地址。
代码地址👉git仓库地址
解决方案:
- 显式实例化:缺点很明显换个类型还得显式实例化。
程序运行成功:
- 声明和定义不分离
文件后缀为.hpp
是模板文件,.hpp
表示是声明和定义放在一起。
//Stack.h
#pragma once
#include <iostream>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int capacity = 4);
~Stack();
void Push(const T& x);
private:
T* _a;
int _top;
int _capacity;
};
template<class T>
Stack<T>::Stack(int capacity)
{
cout << "Stack(int capacity = )" << capacity << endl;
_a = (T*)malloc(sizeof(T) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
template<class T>
Stack<T>::~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
template<class T>
void Stack<T>::Push(const T& x)
{
// ....
//
_a[_top++] = x;
}
//test.cpp
#include "Stack.h"
int main()
{
Stack<int> st1;
st1.Push(1);
return 0;
}
程序运行成功。
[]的重载
#define N 10
using namespace std;
namespace hcz
{
template<class T>
class array
{
public:
T& operator[](size_t i)
{
assert(i < N); //断言检查是否越界
return _a[i];
}
private:
T _a[N];
};
}
int main()
{
//int a2[10];
//a2[20] = 0; //编译器检查不出越界访问
//a2[10]; //对数组边界抽查
hcz::array<int> a1;
for (size_t i = 0; i < N; ++i)
{
// a1.operator[](i)= i;
a1[i] = i;
}
for (size_t i = 0; i < N; ++i)
{
// a1.operator[](i)
cout << a1[i] << " ";
}
cout << endl;
for (size_t i = 0; i < N; ++i)
{
a1[i]++;
}
for (size_t i = 0; i < N; ++i)
{
cout << a1[i] << " ";
}
cout << endl;
//a1[20];
//a1[10];
return 0;
}
重载运算符[]可以让我们像使用数组一样使用某些自定义类型,如上述代码。
编译器对于数组越界的检查是不严格的,编译器只会对数组的边界进行抽查并且只有修改了数据才会报错,这种检查是很 不安全;而自定义类型中我们无疑可以可以对数组越界进行更加严格的检查,比如加上断言。
从上述图片可以看到,vs对于越界访问检查不出,并且对于越界访问并且修改了数据也可能检查不出,这是因为编译器对于数组的越界检查是抽查;编译器只会对于数组边界进行抽查,只有修改数组边界上的数据编译器才可能会报错。
可以看到在VS2019中,越界访问程序能正常运行并结束,vs编译器检查不出数组越界访问的问题。
STL初识
什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且
是一个包罗数据结构与算法的软件框架。
STL版本
- 原始版本
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本–所有STL实现版本的始祖。 - P.J.版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。 - RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。 - SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。后面学习STL要阅读部分源代码,主要参考的就是这个版本。
STL的六大组件
STL重要性
STL是C++学习中非常重要的一部分,不管在笔试中还是面试中都是非重要的;网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。
STL的缺陷
- STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
- STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
- STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
- STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。