目录
1.模板
在之前学习函数重载时,我们知道,形参的类型不同可以构成重载,但是如果我们对该函数有很多种输入的要求,例如整型,字符类型,浮点数类型的变量,写交换函数时就需要写三个函数,但是模板可以进行一个泛型编程的操作,把识别类型交给编译器来做
1.1函数模板
如下三个交换函数构成函数重载,很明显这样写很冗余
cpp
void swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
void swap(double& x, double& y) {
double tmp = x;
x = y;
y = tmp;
}
void swap(char& x, char& y) {
char tmp = x;
x = y;
y = tmp;
}
所以我们就可以使用模板来写函数,模板的写法如下,T1,T2等就是虚拟变量,在使用时由编译器自动匹配类型,class也可以用typename来写,注意模板中也可以存在真实的类型,不全是虚拟变量,例如可以在模板中写size_t n,int x
template<class T1,class T2.....>
代码如下,这就是一个简易的模板函数,当我们需要对某个类型的两个变量进行交换时,只需要将变量直接放入函数中即可,编译器会自动识别类型
cpp
template<class T>
void swap(T& x, T& y) {
T tmp = x;
x = y;
y = tmp;
}
注意在这个swap函数中,如果传入的x和y不是一个类型的,就会产生报错,因为只有一个T,无法匹配两个类型,编译器就不能确定T的类型导致编译报错
这就引出模板的隐式实例化和显式实例化 ,例如一个加法函数,只用了一个虚拟变量T,那么如果传入的参数类型不同就会报错,所以第二个输出add(x,z)会显示报错,但是第三个输出就不会,这就是显式实例化,直接规定T的类型,传入参数时会强行转换成对应类型进行操作,不加<int>就是隐式实例化,类型交给编译器自己识别
注意一个点,在形参部分要加上const,因为进行类型强转时,会产生一个临时对象,这个临时对象具有常性,如果不加const就造成了权限的放大
cpp
template<class T>
T add(const T& x, const T& y) {
return x + y;
}
int main(){
int x = 10;
int y = 20;
double z = 1.1;
cout << add(x, y) << endl;
cout << add(x, z) << endl;
cout << add<int>(x, z) << endl;
return 0;
}
如果既有匹配的普通函数,也可以用模板函数,会优先调用普通函数,因为现成的函数不需要编译器去识别类型,更加方便
cpp
template<class T>
T add(const T& x, const T& y) {
cout << "调用了模板函数" << endl;
return x + y;
}
int add(const int& x, const int& y) {
cout << "调用了普通函数" << endl;
return x + y + 10;
}
int main(){
int x = 10;
int y = 20;
cout << add(x, y) << endl;
//输出40
return 0;
}

1.2类模板
和函数模板的使用没有很大区别,例如一个栈,我们之前如果想改变内部存储的数据类型,直接在typedef的位置修改类型即可,但是如果要创建好几个存储不同数据类型的栈,就需要使用到模板,直接进行显式实例化即可,具体的使用可以根据下文讲解vector的时候继续理解
2.vector
vector和之前学的string一样,都是一种存储数据的容器 ,在使用上和string有相似之处,所以学过string之后继续学vector就觉得比较轻松,而vector其实就是顺序表,只不过它可以使用很多函数直接进行操作,比自己创建的数组顺序表要方便很多
2.1vector的使用
2.1.1vector的定义

vector在初始化时常见的有几种方式
1.无参构造
2.通过n个相同类型的val来构造,注意val不一定是内置类型,也可以是其他类或者嵌套vector
3.通过一段迭代器区间,将这一段的数据依次放入vector
4.拷贝构造,直接拷贝现成的vector
cpp
#include<iostream>
#include<vector>
using namespace std;
int main(){
vector<int> v1;//无参构造
vector<int> v2(5, 1);//通过n个val来构造,(size_t n,const T& val=T())
int arr[5] = { 1,2,3,4,5 };
vector<int> v3(arr, arr + 4);//通过一段迭代器区间
vector<int> v4(v3);//拷贝构造
for (auto e : v2) {
cout << e;
}
cout << endl;
for (auto e : v3) {
cout << e;
}
cout << endl;
for (auto e : v4) {
cout << e;
}
return 0;
}
2.1.2vector迭代器的使用

和string一样,有begin和end函数用于指示开头和末尾

例如用两个迭代器分别正向逆向遍历vector,注意反向迭代器名字要加上reverse,并且++之后是朝着开头位置走的,而不是往末尾
cpp
#include<iostream>
#include<vector>
using namespace std;
int main(){
vector<int> v1 = { 1,2,3,4,5,6,7 };
vector<int>::iterator it = v1.begin();
vector<int>::reverse_iterator it1 = v1.rbegin();
while (it != v1.end()) {
cout << *it;
it++;
}
cout << endl;
while (it1 != v1.rend()) {
cout << *it1;
it1++;
}
cout << endl;
return 0;
}
2.1.3vector的空间管理

可以使用size函数获取元素个数 ,capacity函数获取空间大小
resize可以改变元素的个数 ,如果传入的参数小于原来的元素个数,那么就会直接删除多余的元素,如果大于原来元素个数,在比空间还大的情况下会进行扩容的操作,大于元素个数小于空间个数,那么只会填充vector,使其元素个数达到目标
reverse函数可以为vector预留空间,指定vector的空间大小
cpp
#include<iostream>
#include<vector>
using namespace std;
int main(){
vector<int> v1 = { 1,2,3,4,5,6,7 };
cout << v1.size() << endl;
cout << v1.capacity() << endl;
v1.resize(5);
v1.reserve(10);
cout << v1.size() << endl;
cout << v1.capacity() << endl;
return 0;
}
2.1.4vector的增删查改操作

push_back尾插元素,pop_back尾删元素
insert和erase用于指定位置插入和删除元素 ,注意传入的参数要是迭代器类型,使用迭代器表示位置而不是下标
swap函数可以直接交换两个vector容器中的数据
find函数不是vector的成员接口 ,但是用于查找对应元素下标也可以使用,但是要注意加上头文件**<algorithm>**才能调用算法库的find,返回值是一个迭代器,减去开头的迭代器就可以获取下标
cpp
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
vector<int> v1 = { 1,2,3,4,5,6,7 };
vector<int> v2 = { 10,9,8,7,6,5,4 };
cout << "交换前的v1:";
for (auto e : v1) {
cout << e << " ";
}
cout << endl;
cout << "交换前的v2:";
for (auto e : v2) {
cout << e << " ";
}
cout << endl;
swap(v1, v2);
cout << "交换后的v1:";
for (auto e : v1) {
cout << e << " ";
}
cout << endl;
cout << "交换后的v2:";
for (auto e : v2) {
cout << e << " ";
}
cout << endl;
cout << v1[1] << " " << v1[2] << endl;
v1.push_back(9);
v1.push_back(10);
v1.insert(v1.begin() + 1, 20);
for (auto e : v1) {
cout << e << " ";
}
cout << endl;
v1.pop_back();
v1.pop_back();
v1.erase(v1.begin() + 4);
for (auto e : v1) {
cout << e << " ";
}
cout << endl;
auto f = find(v1.begin(), v1.end(), 7);
cout << f - v1.begin() << endl;
return 0;
}
}

2.2vector迭代器失效
对于改变底层空间的操作可能会引起迭代器失效
1.插入元素时,如果空间不够,那么此时会进行扩容,扩容完的空间和原来不是一个空间,而迭代器还留在旧空间的位置,所以此时迭代器失效
2.删除元素时,删除某个位置的元素后,后面所有的元素均往前移动,此时没有造成空间的改变,但是,如果此时删除的是最后一个元素,此时就会获取到一个空的位置,再进行解引用就会产生报错,而编译器就认为这样的操作是不安全的,为了防止对空指针进行解引用,所以此时的迭代器就失效了,判定它不可继续使用
所以对于下面的代码,当vector进行删除操作时,该位置及其之后的迭代器就会失效
当进行insert插入操作时,如果没有进行扩容迭代器不失效,但是扩容的时候就会失效
cpp
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
vector<int> v1 = { 1,2,3,4,5,6,7 };
vector<int>::iterator it = v1.begin();
while (it != v1.end()) {
v1.erase(it);
it++;
}
vector<int>::iterator it1 = v1.begin();
while (it1 != v1.end()) {
if (*it1 == 5) {
v1.insert(it1, 8);
}
it1++;
}
return 0;
}
为了避免迭代器失效,我们只需要对迭代器重新赋值即可,因为insert和erase函数都会返回下一个位置的迭代器,所以重新获取一个有效的迭代器就行

3.vector的模拟实现
先构造一个命名空间,防止和系统库的vector发生冲突
写出vector的基本结构,因为传入的数据类型比较复杂,所以用迭代器去封装指针,然后让编译器自动识别传入数据的类型即可
所以成员变量有三个,开始位置的迭代器,元素末尾的迭代器以及空间迭代器
cpp
#pragma once
#include<iostream>
#include<assert.h>
#include<stdbool.h>
namespace Vec {
template<class T>
class vector {
typedef T* iterator;
typedef const T* const_iterator;
public:
//成员函数
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
//给上缺省值,避免初始化的问题
};
}
3.1构造函数和析构函数
无参构造默认给三个迭代器赋值为nullptr,如果不写可以用vector()=default的方式让编译器声明默认构造
对于拷贝构造 ,我们先使用reverse函数为容器开辟相同大小的空间 ,然后使用范围for遍历需要拷贝的vector,依次尾插元素即可,但是之前模拟实现string的时候使用memcpy进行拷贝,为什么这里缺不使用,因为memcpy如果拷贝字符串是浅拷贝,如果内部数据是string类型,那么拷贝的时候只是指向了同一块空间而不是额外开空间,所以这里使用遍历尾插的方式拷贝
第三种方法,新创建一个模板用于兼容任意容器的迭代器,只要传入对应的迭代器,然后让迭代器一边往后走一边将遍历到的元素尾插进vector
析构函数比较简单,如果_start不为空指针,那么就删除该空间,并将三个迭代器置为空
cpp
vector()//构造函数
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
vector() = default;
//强制编译器生成默认构造
vector(const vector<T>& v) {
reverse(v.capacity());
for (auto& e : v) {
push_back(e);
}
}
template<class InputIterator>
//兼容任意容器的迭代器
vector(InputIterator start, InputIterator finish) {
while (start != finish) {
push_back(*start);
start++;
}
}
~vector() {
if (_start) {
delete[] _start;
_start = _finish = _endstorage = nullptr;
}
//指针不为空才需要释放空间
}
3.2头尾迭代器
写法很简单,只要返回成员变量中的_start和_finish即可,分为普通迭代器和const迭代器,const迭代器指向的内容不可更改
cpp
iterator begin() {
return _start;
}
iterator end() {
return _finish;
}
const_iterator begin() const{
return _start;
}
const_iterator end() const {
return _finish;
}
3.3空间管理函数
因为迭代器底层使用了指针,所以可以通过指针的相减返回空间大小和元素个数
reserve函数 ,当传入的n大于当前空间的时候,会进行扩容的操作,此处使用2倍扩容,如果_start不为空那么要先进行赋值,然后释放原空间,注意原空间的元素个数要提前存储,不然后续使用size函数给_finish赋值就会使用新的_start而不是原来的,会产生报错
resize函数 需要用元素填充vector,这里使用T()是为了通用性,如果是内置类型不做特殊处理,如果是其他类,那么会调用其默认构造函数进行初始化,如果需要开空间,可以复用reserve函数,如果不需要开空间,那么直接移动末尾指针_finish即可
empty函数用于判断vector是否为空,那么只要判断size是否为0
cpp
size_t capacity() {
return _endofstorage - _start;
}
size_t size() {
return _finish - _start;
}
void reserve(size_t n) {
//对空间大小进行调整
//一般只进行扩容不进行缩容
//所以传入的n如果比较小是不会进行操作的
if (n > capacity()) {
T* tmp = new T[n];
size_t old_size = size();//先存储旧空间的元素个数
if(_start){
for (size_t i = 0; i < old_size; i++) {
tmp[i] = _start[i];//依次赋值
}
delete[] _start;
//如果原来有内存空间
//先进行释放
}
_start = tmp;
_finish = _start + old_size;
//如果此处不使用old_size而是size()
//size()中的_start用的是更新后的,_finish还是之前的
_endofstorage = _start + n;
}
}
void resize(size_t n, T val = T()) {
//对元素个数进行调整
//如果小于原来元素个数,直接把后续元素删除
//如果大于原来元素个数,先使用reserve保证有足够空间
//然后从原末尾开始依次插入val,直到扩充到新的元素个数
if (n > size()) {
reserve(n);
while (_finish != _start + n) {
*_finish = val;
_finish++;
}
}
else {
_finish = _start + n;
}
}
bool empty() {
return size() == 0;
}
3.4增删查改函数
push_back尾插函数,如果空间不足那么需要进行扩容,然后在末尾处加上新元素并移动_finish
pop_back尾删函数,要先判断是否存在元素可以删除,然后移动_finish
swap函数,直接调用库里的swap函数,对三个成员变量进行交换
clear函数 ,清除元素,直接令_finish=_start即可,注意并不是把空间一起清除了
insert插入函数,使用迭代器指示位置,同样先判断是否需要扩容,然后从末尾依次移动元素,给指定位置腾出一个空间,然后插入指定元素,并移动_finish
erase函数 ,判断迭代器位置是否在_start和_finish之间,否则视为非法操作,从指定位置开始依次往前赋值,覆盖指定位置的元素,然后将_finish--
cpp
void push_back(T& x) {
if (_finish == _endofstorage) {
size_t newcapacity = (capacity() == 0) ? 4 : 2 * capacity();
reserve(newcapacity);
}
*finish = x;
++finish;
}
void pop_back() {
assert(size() > 0);
--_finish;
}
void swap(vector<T>& v) {
//引用减少拷贝
//但是为了不修改被拷贝的vector
//调用该函数时,先将被拷贝的vector拷贝一份
//再进行使用,就可以达到赋值的效果
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofdstorage);
}
void clear() {
_finish = _start;
}
iterator insert(iterator pos, const T& x) {
assert(pos < _finish);
assert(pos >= _start);
if (_finish == _endofstorage) {
size_t newcapacity = (capacity() == 0) ? 4 : 2 * capacity();
reserve(newcapacity);
}
iterator end = _finish + 1;
while (end != pos) {
*end = *(end - 1);
end--;
}
*pos = x;
_finish += 1;
return pos;
}
iterator erase(iterator pos) {
assert(pos < _finish);
assert(pos >= _start);
while (pos != _finish) {
*pos = *(pos + 1);
pos++;
}
--_finish;
return pos;
}
3.5补充
写完swap函数后,对于赋值重载就可以写的很简单,使用传值调用传入的vector,所以形参不会改变实参,所以直接将形参传入swap中,然后交换当前类和v的成员变量
还有对于[ ]的两种重载,因为底层使用指针,所以直接用_start[pos]即可访问对应数据
cpp
vector<T>& operator=(vector<T> v) {
//因为形参v的成员要发生改变,不加const
swap(v);
return *this;
}
T& operator[](size_t pos) {
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const {
assert(pos < size());
return _start[pos];
}

