目录
一、vector容器是什么?
在C++的STL(Standard Template Library)中,vector是一种动态数组容器。它提供了一种存储和访问元素的方式,类似于固定大小的数组,但具有动态调整大小的能力。
vector容器可以在运行时根据需要自动调整大小,可以在末尾快速添加或移除元素。它提供了一组成员函数和迭代器,用于访问和操作容器中的元素,例如插入、删除、查找元素等。
下面是一些vector容器的特点和用法:
vector中的元素在内存中是连续存储的,可以通过下标访问和修改元素。
容器的大小可以根据需要自动增长或缩小,使用push_back和pop_back等函数可以方便地在末尾添加或移除元素。
可以使用迭代器进行遍历和访问容器中的元素。
vector还提供了一些成员函数,如size()获取容器中元素的个数,empty()检查容器是否为空等。
vector容器在C++中非常常用,用于存储和操作可变大小的数据集合,可以适用于各种场景和需求。
二、vector的模拟实现
2.1 vector的成员变量
vector的成员变量就是三个指针变量:_start,_finish,_endofstorage。_start指向顺序表中的第一个元素的位置,_finish指向最后一个有效元素的下一个位置,_endofstorage指向容量大小的下一个位置。
2.2 构造函数
构造函数是对成员变量做初始化。
2.2.1 无参构造函数
无参构造函数就是把三个指针置空就行了,可以在构造函数的时候开辟一定的空间,也可以在插入元素的时候再开辟空间。
//构造函数
//这里的初始化列表的值可以在这里给,也可以在成员变量直接给默认值nullptr
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
2.2.2 有参构造函数
//有参构造函数
//
//T()是T类型的匿名对象做缺省值,因为vector能存放任何类型的值,这里不能给0作为
// 缺省值,因为整形int的缺省值是0,但是假如这里的T是string,那么给0做缺省值就
// 不对了,所以这里是需要注意的,同样,如果成员变量处已经给了默认值,那么这里的
//初始化列表初始化就可以不写了
vector(size_t n, const T& value = T())
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
//这里复用resize就能完成需求
resize(n, value);
}
//有参构造
//跟上面的构造函数构成函数重载,避免在调用时和下面的迭代器区间
//构造函数产生歧义
vector(int n, const T& value = T())
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
resize(n, value);
}
//利用迭代器区间初始化的构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
//把这段迭代器区间的值一个一个地尾插即可
while (first != last)
{
push_back(*first);
++first;
}
}
这个图中的文字有点小,把它放大如下图:
2.3 拷贝构造函数
因为这里的成员变量中有指针变量,所以一定要注意深浅拷贝问题!!!!
拷贝构造(传统写法),注意一定要深拷贝,不然是会出现同一块空间被析构两次的情况的
//vector(const vector<T>& v)
// :_start(nullptr)
// ,_finish(nullptr)
// ,_endofstorage(nullptr)
//{
// //开辟跟v一样大小的空间
// T* tmp = new T[v.capacity()];
// if (tmp)
// {
// //当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
// // 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
// // 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
// // 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
// // 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
// // memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
// // 解决了string的浅拷贝问题
// //
// //不能用memcpy(tmp, v._start, sizeof(T) * v.size());
// for (size_t i = 0; i < v.size(); i++)
// {
// tmp[i] = v._start[i];
// }
// _start = tmp;
// _finish = _start + v.size();
// _endofstorage = _start + v.capacity();
// }
//}
//拷贝构造(现代写法)
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
Swap(tmp);
}
2.4 赋值重载函数
成员变量有指针的赋值重载函数同样需要注意深浅拷贝的问题。
void Swap(vector<T>& tmp)
{
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
}
赋值重载函数(现代写法)
利用传参调用拷贝构造得到想要的临时vector,再利用Swap函数
把这个拷贝构造好了的临时对象的成员变量交换给自己的成员变量即可完成赋值
//vector<T>& operator=(vector<T> v)
//{
// Swap(v);
// return *this;
//}
//赋值重载(传统写法)
vector<T>& operator=(const vector<T>& v)
{
if (this != &v)
{
T* tmp = new T[v.capacity()];
if (tmp)
{
int i = 0;
for (int i = 0; i < v.size(); i++)
{
tmp[i] = v._start[i];
}
if (_start)
{
delete _start;
}
_start = tmp;
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
}
return *this;
}
2.5 析构函数
析构函数的作用是释放清理动态申请的资源。
//析构函数
~vector()
{
if (_start)
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endofstorage = nullptr;
}
}
2.6 reserve函数
reserve函数是调整空间大小的函数,一般用于扩容。
void reserve(size_t n)
{
if (n > capacity())
{
//需要先把size()记录下来,不然后面更新_finish的时候会出错
//因为存在异地扩容,而size()=_finish-_start,更新_finish
//的逻辑是_finish=_start+size(),size()=_finish-_start;
//即_finish=_start+_finish-_start,所以_finish永远都是
//nullptr的,所以要想后续正确地更新_finish,需要把原来的size()
//用sz记录下来,然后_finish=_start+sz才是正确的
size_t sz = size();
T* tmp = new T[n];
if (tmp && _start)
{
//当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
// 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
// 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
// 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
// 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
// memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
// 解决了string的浅拷贝问题
//
//不能用memcpy(tmp, _start, sizeof(T) * size());
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
2.7 resize函数
resize也是用于调整空间大小的函数,但是resize功能更多一些,还可以带上默认值。
void resize(size_t n, const T& value = T())
{
if (n <= size())
{
//如果n<=size(),直接更新_finish指针为最后一个元素的下一个位置即可
_finish = _start + n;
}
else
{
//n>capacity()时,需要先扩容,reserve函数里面会判断是否需要
//扩容,所以这里可以判断,也可以不判断都行
reserve(n);
//根据resize的性质,给_finish到_start+n这段区间填充value即可
while (_finish != _start + n)
{
*_finish = value;
++_finish;
}
}
}
2.8 insert函数
insert函数是用于在某个位置插入数据的函数。
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
//满了就扩容
if (_finish == _endofstorage)
{
//存在迭代器失效问题,需要记录pos相对于原来空间的位置,由于扩容后指向了
//新的空间,造成原来的迭代器失效,需要更新pos找到pos在新的空间的相对位置
size_t len = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
//更新pos在新的空间的相对位置
pos = _start + len;
}
//这里的insert就是顺序表的插入,就是把从pos位置开始往后的所有元素
// 都往后移动一位,给pos腾出一个位置即可插入目标元素
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = x;
++_finish;
//根据源码的要求需要返回插入的元素的迭代器
return pos;
}
2.9 erase函数
erase函数是用于在某个位置删除元素的函数。
iterator erase(iterator pos)
{
//assert(pos >= begin() && pos < end());
//iterator ret = pos;
//iterator start = pos + 1;
//while (start < end())
//{
// *pos = *start;
// ++pos;
// ++start;
//}
//--_finish;
//return ret;
assert(pos >= begin() && pos < end());
//顺序表的删除也非常简单,把pos后面的所有元素都往前挪动一位即可
iterator start = pos + 1;
while (start < end())
{
*(start - 1) = *start;
++start;
}
--_finish;
return pos;
}
2.10 push_back和pop_back函数
push_back和pop_back函数是用于尾插尾删的函数。可以直接复用insert和erase函数。
void push_back(const T& x)
{
//复用insert尾部插入x即可
//end()是最后一个元素的下一个位置,刚好就是尾插的位置
insert(end(), x);
}
void pop_back()
{
//end()是最后一个元素的下一个位置,所以尾删需要删除
//end()的前一个位置,前置--是先--,再使用
erase(--end());
}
2.11 capacity函数
size_t capacity() const
{
return _endofstorage - _start;
}
2.12 size函数
size_t size() const
{
return _finish - _start;
}
2.13 运算符重载operator[]函数
//operator[]返回的是pos位置的引用
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
2.14 迭代器
//对于vector,vector里面存放的数据类型的指针就是iterator
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
迭代器失效问题
三、STL容器vector模拟实现代码汇总
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
#include <vector>
namespace kb
{
template<class T>
class vector
{
//对于vector,vector里面存放的数据类型的指针就是iterator
typedef T* iterator;
typedef const T* const_iterator;
public:
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
//构造函数
//这里的初始化列表的值可以在这里给,也可以在成员变量直接给默认值nullptr
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
//有参构造函数
//
//T()是T类型的匿名对象做缺省值,因为vector能存放任何类型的值,这里不能给0作为
// 缺省值,因为整形int的缺省值是0,但是假如这里的T是string,那么给0做缺省值就
// 不对了,所以这里是需要注意的,同样,如果成员变量处已经给了默认值,那么这里的
//初始化列表初始化就可以不写了
vector(size_t n, const T& value = T())
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
//这里复用resize就能完成需求
resize(n, value);
}
//有参构造
//跟上面的构造函数构成函数重载,避免在调用时和下面的迭代器区间
//构造函数产生歧义
vector(int n, const T& value = T())
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
resize(n, value);
}
//利用迭代器区间初始化的构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
//把这段迭代器区间的值一个一个地尾插即可
while (first != last)
{
push_back(*first);
++first;
}
}
拷贝构造(传统写法),注意一定要深拷贝,不然是会出现同一块空间被析构两次的情况的
//vector(const vector<T>& v)
// :_start(nullptr)
// ,_finish(nullptr)
// ,_endofstorage(nullptr)
//{
// //开辟跟v一样大小的空间
// T* tmp = new T[v.capacity()];
// if (tmp)
// {
// //当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
// // 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
// // 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
// // 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
// // 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
// // memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
// // 解决了string的浅拷贝问题
// //
// //不能用memcpy(tmp, v._start, sizeof(T) * v.size());
// for (size_t i = 0; i < v.size(); i++)
// {
// tmp[i] = v._start[i];
// }
// _start = tmp;
// _finish = _start + v.size();
// _endofstorage = _start + v.capacity();
// }
//}
//拷贝构造(现代写法)
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
Swap(tmp);
}
void Swap(vector<T>& tmp)
{
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
}
赋值重载函数(现代写法)
利用传参调用拷贝构造得到想要的临时vector,再利用Swap函数
把这个拷贝构造好了的临时对象的成员变量交换给自己的成员变量即可完成赋值
//vector<T>& operator=(vector<T> v)
//{
// Swap(v);
// return *this;
//}
//赋值重载(传统写法)
vector<T>& operator=(const vector<T>& v)
{
if (this != &v)
{
T* tmp = new T[v.capacity()];
if (tmp)
{
int i = 0;
for (int i = 0; i < v.size(); i++)
{
tmp[i] = v._start[i];
}
if (_start)
{
delete _start;
}
_start = tmp;
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
}
return *this;
}
//析构函数
~vector()
{
if (_start)
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endofstorage = nullptr;
}
}
void reserve(size_t n)
{
if (n > capacity())
{
//需要先把size()记录下来,不然后面更新_finish的时候会出错
//因为存在异地扩容,而size()=_finish-_start,更新_finish
//的逻辑是_finish=_start+size(),size()=_finish-_start;
//即_finish=_start+_finish-_start,所以_finish永远都是
//nullptr的,所以要想后续正确地更新_finish,需要把原来的size()
//用sz记录下来,然后_finish=_start+sz才是正确的
size_t sz = size();
T* tmp = new T[n];
if (tmp && _start)
{
//当T是自定义类型string的时候,vector拷贝构造是深拷贝,但是如果用memcpy对vector
// 里面的内容 进行拷贝,因为memcpy是按字节拷贝的,vector里面存放的是string,string的
// 成员变量是_str,_size,_capacity三个指针,所以memcpy按字节拷贝是把每一个string的这
// 三个指针 拷贝给了新的vector,所以新的vector中存放的string的指针和v中存放的string的
// 指针是一样的,也就是浅拷贝问题,里面的string会被析构两次,造成程序崩溃,所以这里不能用
// memcpy,而是要把v中的string一个个地赋值给新的vector,本质是调用了string的赋值重载
// 解决了string的浅拷贝问题
//
//不能用memcpy(tmp, _start, sizeof(T) * size());
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
void resize(size_t n, const T& value = T())
{
if (n <= size())
{
//如果n<=size(),直接更新_finish指针为最后一个元素的下一个位置即可
_finish = _start + n;
}
else
{
//n>capacity()时,需要先扩容,reserve函数里面会判断是否需要
//扩容,所以这里可以判断,也可以不判断都行
reserve(n);
//根据resize的性质,给_finish到_start+n这段区间填充value即可
while (_finish != _start + n)
{
*_finish = value;
++_finish;
}
}
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
//满了就扩容
if (_finish == _endofstorage)
{
//存在迭代器失效问题,需要记录pos相对于原来空间的位置,由于扩容后指向了
//新的空间,造成原来的迭代器失效,需要更新pos找到pos在新的空间的相对位置
size_t len = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
//更新pos在新的空间的相对位置
pos = _start + len;
}
//这里的insert就是顺序表的插入,就是把从pos位置开始往后的所有元素
// 都往后移动一位,给pos腾出一个位置即可插入目标元素
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = x;
++_finish;
//根据源码的要求需要返回插入的元素的迭代器
return pos;
}
iterator erase(iterator pos)
{
//assert(pos >= begin() && pos < end());
//iterator ret = pos;
//iterator start = pos + 1;
//while (start < end())
//{
// *pos = *start;
// ++pos;
// ++start;
//}
//--_finish;
//return ret;
assert(pos >= begin() && pos < end());
//顺序表的删除也非常简单,把pos后面的所有元素都往前挪动一位即可
iterator start = pos + 1;
while (start < end())
{
*(start - 1) = *start;
++start;
}
--_finish;
return pos;
}
void push_back(const T& x)
{
//复用insert尾部插入x即可
//end()是最后一个元素的下一个位置,刚好就是尾插的位置
insert(end(), x);
}
void pop_back()
{
//end()是最后一个元素的下一个位置,所以尾删需要删除
//end()的前一个位置,前置--是先--,再使用
erase(--end());
}
size_t capacity() const
{
return _endofstorage - _start;
}
size_t size() const
{
return _finish - _start;
}
//operator[]返回的是pos位置的引用
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
void test1(void)
{
vector<int> v;
//v.push_back(1);
//v.push_back(2);
//v.push_back(3);
//v.push_back(4);
//v.push_back(5);
v.insert(v.begin(), 1);
v.insert(v.begin(), 2);
v.insert(v.begin(), 3);
v.insert(v.begin(), 4);
v.insert(v.begin(), 5);
//v.insert(v.begin() + 3, 100);
//for (const auto& e : v)
//{
// cout << e << " ";
//}
//cout << endl;
//kb::vector<int>::iterator it = v.erase(v.begin());
//cout << *it << endl;
size_t sz = v.size();
for (size_t i = 0; i < sz; i++)
{
v.erase(v.begin());
}
for (size_t i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
}
void test2(void)
{
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
std::vector<int>::iterator ret = v.erase(v.begin());
cout << *ret << endl;
//for (const auto& e : v)
//{
// cout << e << " ";
//}
//cout << endl;
}
void test3(void)
{
vector<int> v(5, 1);
v.push_back(2);
v.push_back(2);
v.push_back(2);
v.insert(v.begin(), 9);
vector<int> v1(v);
for (const auto& e : v1)
{
cout << e << " ";
}
cout << endl;
for (const auto& e : v1)
{
cout << e << " ";
}
cout << endl;
}
void test4(void)
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
vector<int> v2(v1.begin(), v1.end());
//v1.resize(6, 1);
for (auto& e : v2)
{
cout << e << " ";
}
cout << endl;
}
void test5(void)
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
vector<int> v2(v1);
//v1.resize(6, 1);
for (auto& e : v2)
{
cout << e << " ";
}
cout << endl;
}
void test6(void)
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
vector<int> v2;
v2.push_back(9);
v2.push_back(10);
v2 = v1;
for (auto& e : v2)
{
cout << e << " ";
}
cout << endl;
}
}
以上就是STL容器vector常用接口的模拟实现的全部内容了,其实vector还有很多接口,但是这些接口是常用的,其它的不常用的接口就不实现了。以上就是今天想要跟大家分享的内容,你学会了吗?如果这篇文章对你有所帮助,那么点点小心心,点点关注呗,后续还会持续更新C++的相关知识哦,我们下期见!!!!!