前言
在string的学习过程中,我们不难发现由于String比STL更早实现,而后再重新对String进行编写后,String的很多接口存在笼余问题,但是我们还是凭此对STL有了一个系统的认识,接下来我们将开始第二个标准库------Vector的学习。
目录
[2.1 vector的定义](#2.1 vector的定义)
[2.2 迭代器的使用](#2.2 迭代器的使用)
[2.3 空间增长变化问题](#2.3 空间增长变化问题)
[2.4 增删改查操作](#2.4 增删改查操作)
[2.5 迭代器失效问题](#2.5 迭代器失效问题)
[2.6 二维数组的实现](#2.6 二维数组的实现)
[4.1 短小简单函数实现](#4.1 短小简单函数实现)
[[] 运算符重载](#[] 运算符重载)
[4.2 reserve、push_back以及pop_back的实现](#4.2 reserve、push_back以及pop_back的实现)
[4.3 resize的实现:](#4.3 resize的实现:)
[4.4 erase、insert的实现](#4.4 erase、insert的实现)
[4.5 构造函数与析构函数](#4.5 构造函数与析构函数)
一、Vector的介绍
Vector翻译成中文为向量,那我们学习过数据结构后可以把底层看成顺序表,实际的源代码有不同但是其实底层看成顺序表是没问题的。
cpp
private:
int* _arr;
int _size;
int _capacity;
vector中存储的不一定就是int类型的,也可以是其他类型。这里要说明与string类不同,string类是类模板实例化后的结果,而vector还只是类模板,需要进行显示实例化成具体的类才能使用。

那我们依旧会根据文档进行功能探索向量 - C++ 参考
二、Vector的使用
我们来对一些常用的接口进行探索。
2.1 vector的定义


代码展示:
cpp
void test_vector1()
{
vector<int> v1;
vector<int> v2(10, 1);
vector<int> v3(v2.begin()+2, v2.end()-1);
vector<int> v4(v3);
}
结果展示:

2.2 迭代器的使用

所有标准库的迭代器用法是相同的,我们可以利用迭代器进行遍历vector实例化后的对象。
cpp
void test_vector2()
{
vector<int> v1(10,1);
vector<int>::iterator it = v1.begin();
int i = 0;
while (it != v1.end())
{
*it = i++;
cout << *it << ' ';
it++;
}
}

我们前面提过范围for的概念,其底层是迭代器,所以在这里同样适用。


2.3 空间增长变化问题

size()可以得到对象的有效元素的额个数,capacity()可以得到对象的空间大小。
**reserve()**的功能与string中的类似,都是当n大于实际空间时进行扩容,n小于实际空间时不会缩容,意思reserve只会进行扩容的操作。
我们设计一个代码来观察一下在vs编译器下,reserve的扩容机制:
cpp
void test_vector3()
{
vector<int> v1;
cout << v1.capacity() << endl;
for (int i = 0;i < 100;i++)
{
v1.push_back(i);
if (v1.size() == v1.capacity())
{
cout << v1.capacity() << endl;
}
}
}

我们发现是1.5倍的扩容,向上取整,而在linux中g++是二倍扩容。
resize

我们看文档可以发现,在vector中如果n<size(),那么会缩小size,如果大于size(),小于capacity(),那么就会增加size()为n,补充的元素内容为val;如果n>capacity(),那么就会进行扩容并补充对应的元素。
cpp
void test_vector4()
{
//在vs编译器中reserve以及resize的逻辑都是大的扩容
vector<int> v1(10,1);
cout << v1.capacity() << endl;
v1.reserve(100);//扩容
cout << v1.capacity() << endl;
v1.shrink_to_fit();
//根据size对capacity进行调整
cout << v1.size() << ' ' << v1.capacity() << endl;
vector<int> v2(10, 1);
cout << v2.capacity() << endl;
v2.reserve(5);//不扩容
cout << v2.capacity() << endl;
vector<int> v3(10, 1);
cout << v3.capacity() << endl;
v3.resize(100);//扩容
cout << v3.capacity() << endl;
vector<int> v4(10, 1);
cout << v4.capacity() << endl;
v4.resize(5);//不扩容,但是改变size
cout << v4.capacity() << endl;
cout << v4.size() << endl;
}

capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5****倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
resize在开空间的同时还会进行初始化,影响size。
2.4 增删改查操作

push_back以及pop_back我们应该已经很熟悉了,尾插尾删。
cpp
void test_vector6()
{
vector<int> v;
for (int i = 0;i < 5;i++)
{
v.push_back(i);
}
for (auto it : v)
{
cout << it << ' ';
}
cout << endl;
v.pop_back();
for (auto it : v)
{
cout << it << ' ';
}
}

insert在指定位置之前插入数据,我们看看相关参数:

我们发现函数参数中使用迭代器来进行元素的的定位,当然也很方便,我们只需要对begin()以及end()进行加减就可以改变位置,我们调用时很方便,但是但给我们尝试进行模拟实现时,可能会引发迭代器失效的问题,针对这个问题我们后面再进行解释。
代码展示:
cpp
void test_vector7()
{
vector<int> v;
for (int i = 0;i < 5;i++)
{
v.push_back(i);
}
auto it = v.begin();
v.insert(it + 2, 5);
for (auto it : v)
{
cout << it << ' ';
}
cout << endl;
v.insert(it + 2, 4, 8);
for (auto it : v)
{
cout << it << ' ';
}
cout << endl;
}

erase的介绍
一样的,由于移动多个数据的位置,所以会导致效率比较低,函数中的参数同样也是迭代器
代码功能展示:
cpp
void test_vector8()
{
vector<int> v;
for (int i = 0;i < 10;i++)
{
v.push_back(i);
}
auto it = v.begin();
v.erase(it + 9);
for (auto it : v)
{
cout << it << ' ';
}
cout << endl;
v.erase(it + 2, it + 6);
for (auto it : v)
{
cout << it << ' ';
}
v.clear();
}

访问操作

功能介绍其实我们学习过strin后就知道了,我们直接给出功能展示:
cpp
void test_vector5()
{
vector<int> v;
for (int i = 0;i < 10;i++)
{
v.push_back(i);
}
cout << v[0] << ' ' << v.at(9) << endl;
cout << v.front() << ' ' << v.back() << endl;
int* start = v.data();//返回向量内部用于存储其自有元素的内存数组的直接指针
cout << *start+5;
}

2.5 迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对 指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器 底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即 如果继续使用已经失效的迭代器,程序可能会崩溃)。 对于vector可能会导致其迭代器失效的操作有: 会引起其底层空间改变的操作,都有可能是迭代器失效......后面很多情况我们在底层实现时会提到。
2.6 二维数组的实现
cpp
//展示一下二维数组
void test_vector9()
{
/*vector<int> v1(5, 1);
vector<vector<int>> v2(4, v1);
for (int i = 0;i < 4;i++)
{
for (int j = 0;j < 5;j++)
{
cout << v2[i][j] << ' ';
}
cout << endl;
}*/
vector<int> v(5, 7);
vector <vector<int>> vv(7, v);
for (int i = 0;i < vv.size();i++)
{
for (int j = 0;j < vv[i].size();j++)//可能每行的列数不同
{
cout << vv[i][j] << ' ';
}
cout << endl;
}
}

因为vector内的类型不一定是内置类型,也可以是一维数组,我们都知道二维数组其实是元素为一维数组的一维数组,抽象地看就是:

那么到这里我们就基本将vector的几个接口功能看完了,下面我们就开始进行底层的实现。
三、vector源码中的成员变量
在上面我们说,可以将vector看成顺序表,我们知道顺序表的成员变量是下面这样的:
cpp
private:
T* _val;
int _size;
int _capacity;
但是我们查看vector的源代码发现好像不是这样的,源代码中的成员变量是:
cpp
private:
//我们观察vector的源码发现
//其底层是由三个迭代器实现的,但其实我们还是可以将其看成顺序表
iterator _start;//指向第一个
iterator _finish;//指向最后一个有效元素的下一位
iterator _end_of_storage;//指向容器中的最后一个有效空间
//T* _arr;
//int _size;
//int _capacity;
};
但是其实我们对比发现,只是将顺序表中的size以及capacity换了一种形式进行表示。
我们给出基本的框架,再对框架进行完善:
cpp
#include<iostream>
namespace mine
{
template<class T>//vector本质上是类模板
class vector
{
public:
typedef iterator T*;
typedef const_iterator const T*;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
我们在介绍的过程中说过,vecotor为类模板,类模板不支持声明与定义分离在不同的文件中,否则会发生链接错误。而后我们将在此基础上进行常用功能的补充。
四、函数成员的补充
4.1 短小简单函数实现
我们说过,类中的函数时默认内联的,那么我们先实现短小简单的函数
不带参的构造函数
cpp
vector()
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{}
begin、end
cpp
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
capacity、size、empty
cpp
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
bool empty() const
{
return size() == 0;
}
[] 运算符重载
cpp
T& operator[](size_t n)
{
assert(n < size());
return _start[n];
}
const T& operator[](size_t n) const
{
assert(n < size());
return _start[n];
}
4.2 reserve、push_back以及pop_back的实现
我们先实现reserve,这在我们后面的代码中很常用。
我们再明确一下reserve的功能:当其中的参数n大于我们已有的空间时,才会进行扩容其他场景不予操作,好,那么我们开始写:
cpp
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
memcpy(tmp, _start, size()*sizeof(T));
delete[] _start;
_start = tmp;
_finish = tmp + size();
_end_of_storage = _start + n;
}
}
嗯,好像没什么问题,那我们再写一个push_back然后测试看看:
cpp
void push_back(const T& val)//防止当T为非内置类型时,进行多次拷贝所以采用引用
{
if (size() == capacity())
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish++ = val;
}
我们经过调试发现,_finish有问题,初始化列表没问题那么就是reserve出现了问题。

cpp
_finish = tmp + size();
reserve中这个代码,当我们调用其中的size时,发现_start已经发生了改变了,指向的是tmp,那么这里肯定就会发生问题,指向的都不是同一个对象,那么_finish的结果就未知了,我们要解决这个问题就需要将原本的大小保存起来:
cpp
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t old_size = size();
memcpy(tmp, _start, size()*sizeof(T));
delete[] _start;
_start = tmp;
_finish = tmp + old_size;
_end_of_storage = _start + n;
}
}
调试后,我们发现可以正常运行,但是我们我们测试的是int型的,如果为string类型时,会引发问题。
问题分析:
-
memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。
-
如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅 拷贝。
所以我们需要设计为:
cpp
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t old_size = size();
for (int i = 0;i < old_size;i++)
{
tmp[i] = _start[i];//这里可以调用对应类型的赋值重载
}
delete[] _start;
_start = tmp;
_finish = tmp + old_size;
_end_of_storage = _start + n;
}
}
pop_back代码:
cpp
void pop_back()
{
assert(size());
_finish--;
}
4.3 resize的实现:
cpp
void resize(size_t n, T val = T())
{
if (n <size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _end_of_storage)
{
*(_finish++) = val;
}
}
}
4.4 erase、insert的实现
cpp
iterator insert(iterator pos, const T& val)
//这里的pos指向的是原对象,如果进行了扩容,那么就会产生错误
//所以我们需要存储pos位置在顺序表中的相对位置
{
assert(pos >= _start&& pos <= _finish);
if (size() == capacity())
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;
}
iterator end = _finish-1;//_finish指向的最后一个元素的最后一个字节
while (end >= pos)
{
*(end+1) = *end;
end--;
}
*pos = val;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos <= _finish && pos >= _start);
iterator it = pos;
while (it != _finish-1)
{
*it = *(it + 1);
it++;
}
_finish--;
return pos;
}
4.5 构造函数与析构函数
cpp
vector(const vector<T>& v)
{
reserve(v.size());
for (auto& it : v)//使用引用是为了防止多次拷贝
{
push_back(it);
}
}
//可以使用其他类型的迭代器,将数据放入vector中
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
InputIterator in = first;
while (in != last)
{
push_back(*in);
in++;
}
}
vector(size_t n, const T& val = T())
{
for (size_t i = 0;i < n;i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
for (size_t i = 0;i < n;i++)
{
push_back(val);
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
//v1=v2
vector<T>& operator=(const vector<T>& v)
{
if (this != &v)
{
vector<T> tmp(v); // 拷贝构造
swap(tmp); // 交换内部指针
}
return *this;
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
其实很多代码都是很容易实=实现的,测试功能时有问题自己就多进行调试。