STL——vector的底层实现C++
文章目录
- 1.前言
- 2.vector的实现
- 2.1vector的基本结构
- 2.2 capacity() 和 size()
- 2.3 迭代器和下标访问
- 2.4 构造,拷贝构造,析构函数,赋值重载
- 2.5增删查改(重点)
1.前言
首先什么是vector?
在C++中,vector 是一种非常常用的容器,属于标准模板库(STL)的一部分。它是一个封装了动态大小数组的序列容器,提供了方便的接口来存储、访问和操作数据。
vector 是一种动态数组,它的大小可以随着元素的添加或删除而自动调整。与普通数组不同,普通数组的大小在定义时是固定的,而 vector 的大小可以在运行时动态改变。
2.vector的实现
首先我们来实现vector的基础结构(成员变量,迭代器等),再介绍一些常用的构造和拷贝构造,最后实现一些增删查改。
2.1vector的基本结构
//首先为了区分库里的vector我们用命名空间将我们要写的vector类包起来
namespace dhb
{template<class T>class vector{public://重新命名一下方便书写迭代器等typedef T* iterator;typedef const T* const_iterator;//各种成员函数private://成员变量iterator _start=nullptr;//这里我们提前给定缺省值iterator _finish=nullptr;iterator _endofstorage=nullptr;
};
vector里为了能存储各类数据用到了模板template<class T>
由由于模板不能声明定义分离
我们的成员函数就写在头文件中。
2.2 capacity() 和 size()
size_t capacity() const;
- 返回对象所开空间大小(以元素数量表示)
size_t size() const;
- 返回数组中的元素数量
size_t:返回无符号整型
const:不改变成员变量
size_t capacity()const
{return _endofstorage - _start;
}
size_t size()const
{return _finish - _start;
}
2.3 迭代器和下标访问
迭代器:
这里介绍普通迭代器和const迭代器,反向迭代器先不做解释。
容器里常用的范围for就是通过调用迭代器实现
iterator begin()
const_iterator begin() const
- 返回一个指向该向量(vector)中第一个元素的迭代器
iterator end()
const_iterator end() const
- 返回一个指向vector容器中“末端之后”元素的迭代器。
这里的“末端之后”元素是一个理论上的元素,它位于vector中最后一个元素的后面。它并不指向任何实际存在的元素,因此不能被解引用
由此可以将begin()和end()看成一个左闭右开的区间 [begin(),end())
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;
}
下标访问:
不管是在做题还是在项目中下标访问都是非常实用的。
T& operator[](size_t i)
const T& operator[](size_t i)const
- 返回向量容器中位置为n的元素的引用
- 原理和C语言数组类似不过多解释
- 值得注意的是const版本,第一个const表示返回值不能改变(不能++或- -),第二个是成员变量不能改变
T& operator[](size_t i)
{assert(i < size());return _start[i];
}
const T& operator[](size_t i)const
{assert(i < size());return _start[i];
}
2.4 构造,拷贝构造,析构函数,赋值重载
由于有些构造要用到迭代器所以就放在这里讲解了。
首先最简单的无参构造:
vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr)
{}
有参构造:
vector(size_t n, T val = T())
{resize(n, val);//这里我提前复用了后面函数resize()//大家可以结合后面的resize()理解
}
以上两种构造较简单不做重点讲解。
拷贝构造:
vector(const vector<T>& v)
- 用一个对象去初始化一个新的对象
使用场景:vector<int> v2(v1)
;(假定v1在上文已初始化)
vector(const vector<T>& v)
{//假如用v1构造v2//v就是v1//不要忘记拷贝数据reserve(capacity());for (auto& e : v){//把数据逐一插入push_back(e);}
}
用花括号构造:
想要像下方一样用花括号构造,要有initializer_list构造函数
dhb::vector<int> v3 = { 1,2,3,4,5,5 };
dhb::vector<int> v4 = { 1,2,3,4,5,51,1,1,1,1,1,1,1 };
其实现和拷贝构造类似
vector(initializer_list<T> li)
{//空间略有不同,我们这里只知道花括号中的元素数量//把空间开成size()大小reserve(li.size());for (auto& e : li){push_back(e);}
}
迭代器区间构造:
template<class InputIterator>
vector(InputIterator first,InputIterator last)
{//只拷贝一部分迭代器就不考虑reserve了while(first!=last){push_back(*first);++first;}
}
注意:在用迭代器区间构造时要用模板(为了适配所有的容器)可以用vector构造。
析构函数:
将数组释放再置空。
~vector()
{if (_start){delete[] _start;_start = _finish = _endofstorage = nullptr;}
}
赋值重载:
void swap(vector<T>& v)
{std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endofstorage, v._endofstorage);
}// 可以写v1 = v2了
vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}
2.5增删查改(重点)
这里我们重点讲解扩容函数reserve()中的一些坑,insert()和erase()中的迭代器失效。
T
是上面基本结构中介绍的模板参数
void push_back(const T& x)
void pop_back()
在_finish尾插处尾插一个数据
在写尾插函数时还有个前提条件:扩容函数reserve()
- 我先写个大家容易写成的错误版本:
//先写个扩容函数
void reaerve(size_t n)
{if(n>capacity()){//申请空间T* tmp=new T[n];//如果_str不为空拷贝数据if(_str){memcpy(tmp,_str,size()*sizeof(T));//注意这里的第三个参数是字节//释放旧空间delete [] _str;}//重新给成员函数值_str=tmp;_finish=_str+size();_endofstorage=_str+n;}
}
这段代码主要有两个错误:
第一个是_finish=_str+size();
由于size()返回的是_finish - _start
但是_finish是老的vector上的指针,而_start已经完成了_str=tmp
的操作,_start和_finish不在同一数组上由此我们的size()就失效了。我们可以通过提前记录size()的值的方法来解决。
第二个错误是memcpy
的使用,memcpy
是按字节拷贝,不适用于拷贝 std::string
这样的复杂对象,因为它们内部有指针和动态内存管理逻辑,所以我们要手动逐一拷贝数据。
由此我们可以修改我们的代码:
void reserve(size_t n)
{if (n > capacity()){T* tmp = new T[n];size_t oldsize = size();//拷贝数据//只有顺序表内不为空的时候才需要拷贝数据if (_start){/*memcpy(tmp, _start, oldsize*sizeof(T));*///这里第三个参数易错是长度乘每个参数大小//memcpy是一个一个字节拷贝,会导致string类型的问题//逐个拷贝for (size_t i = 0; i < oldsize; i++){tmp[i] = _start[i];}delete[]_start;}_start = tmp;_finish = _start + oldsize;_endofstorage = _start + n;}
}
写完了扩容函数我们的尾插尾删就容易多了简单带过一下:
void push_back(const vector<T>& x)
{if(_finish==_endofstorage){//空间满了就扩容reserve(capacity()==0?4:2*capacity());}//插入数据*_finish=x;_finish++;
}//尾删
//判空
bool empty() const
{return _finish == _start;
}
void pop_back()
{assert(!empty());//再写个判空--_finish;
}
iterator insert(iterator pos, T x)
iterator erase(iterator pos)
//任意位置插入iterator insert(iterator pos, T x){assert(pos >= _start);assert(pos <= _finish);if (_finish == _endofstorage){size_t len = _finish - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());//扩容以后我们的pos就被判定为失效状态(虽然实际上不一定失效)//重新将pos定位一下pos = _start + len;}//挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}//插入数据*pos = x;_finish++;return pos;//?}iterator erase(iterator pos){assert(pos >= _start);assert(pos <= _finish);//挪动数据iterator end = pos + 1;while (end != _finish){*(end - 1) = *end;end++;}_finish--;return pos;}
接下来由以上两个接口来介绍一下迭代器失效:
在 C++ 中,迭代器失效(Iterator Invalidation)是一个常见的问题,尤其是在对容器进行修改操作时。当迭代器失效时,继续使用该迭代器可能会导致未定义行为(如访问非法内存、程序崩溃等)。
结合上文的reserve函数:
这里的迭代器失效虽然只是在空间扩容后才发生的,但是我们在写代码的过程中只要insert数据了我们酒吧迭代器认定为失效状态。
erase也会造成迭代器失效问题但是和insert有些不同,我们结合实例理解:
vector<int> v{1,2,3,4,4,5}
auto it = v.begin();
while (it != v.end())
{if (*it % 2 == 0)v.erase(it);else++it;
}
那么我们该如何解决迭代器失效问题呢?
这是库里的函数,我们发现都会返回一个迭代器position(也就是我上面写pos),insert的pos就是插入数据的位置,erase的pos位置是那个被删除后的数据后面的一个数据(因为后面数据前移了)。有了返回值我们就可以将其赋值给迭代器进行更新。
如下:
iterator insert(iterator pos, T x)
{assert(pos >= _start);assert(pos <= _finish);if (_finish == _endofstorage){size_t len = _finish - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());//扩容以后我们的pos就被判定为失效状态(虽然实际上不一定失效)//重新将pos定位一下pos = _start + len;}//挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}//插入数据*pos = x;_finish++;return pos;//?
}
iterator erase(iterator pos)
{assert(pos >= _start);assert(pos <= _finish);//挪动数据iterator end = pos + 1;while (end != _finish){*(end - 1) = *end;end++;}_finish--;return pos;
}