1.vector的简介
1.
vector 是表示可变大小数组的序列容器,就像数组一样,vector 也采用的连续存储空间来存储元素。也就是意味着可以采用下标对 vector 的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
2.
本质讲, vector 使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector 并不会每次都重新分配大小。
3.
vector 分配空间策略: vector 会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
2.vector的框架
在了解了vector的大致功能后,就要开始实现vector的功能,首先去看了一下库中vector的源代码,可以发现,成员变量是由三个指针组成。
分别命名为:
start:整个数组起始位置的指针
finish:最后一个数据的下一个位置的指针
endofstorage:指向内存最后面的下一个位置。
3.成员函数的实现
1.基本函数的实现
首先实现几个最基础的功能,好让我们的vector可以正常的运行。
1.构造函数
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr) {
}
构造函数需要将三个成员变量初始化一下,防止可能出现的bug,因为编译器不一定会默认将内置类型初始化,这是标准未定义的。
2.析构函数
~vector() {
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
析构函数直接用起始的指针进行释放就可以了,之后将三个指针置空(可以不置空,这里为了规整就置空了)。
3.capacity和size
size_t capacity()const {
return _endofstorage - _start;
}
size_t size() const{
return _finish - _start;
}
要求出两个指针中的数据个数,如果是一闭一开的区间的话(类似10和1,一闭一开的区间),两个指针相减就可以直接得到。endofstorage和finish都是开区间,start是闭区间,所以直接返回他们的插值,就是数据个数和开辟的空间大小了。
4.reserve
void reserve(size_t cp) {
if (cp > capacity()) {
size_t sz = size();
T* tmp = new T[cp];
if (_start) {
for (int i = 0; i < sz; i++) {
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + cp;
}
}
reserve能够提前申请空间,在之后的成员函数会大量复用到,这里就先实现了
思路:首先比较需要的空间是否大于原来的空间,如果不大于,就不扩容,直接返回。
需要保存size的值,扩容之后访问size会因为地址的变化出现bug,申请新的动态内存空间,并进行判断,start不为空说明原本空间不为空(start为空,说明没有需要拷贝的地址),需要将数据拷贝到新的地址去,需要注意的是这里的拷贝必须要调用赋值,因为vector里面可能保存其他的自定义类型,赋值运算符会调用自定义类型的赋值运算,不会出现浅拷贝的问题。
最后,将start,finish,endofstorage修改为新的地址。
5.push_back
void push_back(const T& val) {
if (_finish == _endofstorage) {
reserve((capacity() == 0) ? 4 : capacity() * 2) ;
}
*_finish = val;
_finish++;
}
思路:
要插入新的数据最先需要检查的就是空间是否足够,不足就进行扩容。这边用finish和endodstorage进行比较,相等,说明空间已经满了,需要进行扩容。
这边每次的扩容都是在原来的基础乘2倍,这是比较合理的大小。
之前已经实现了reserve函数,所以直接复用就可以,传过去的数需要进行三目运算符的比较,因为原本数据大小可能为0,0乘以2还是0,所以当内存大小为0时,传一个4进行扩容,不为0时才传原本内存大小的2倍。
将finish所在位置进行赋值,++finish。
6.operator[]
const T& operator[](int pos)const {
assert(pos < size());
return _start[pos];
}
思路:
要先和数据个数进行比较,如果不小于就说明越界了,直接报一个警告就行。
直接用start进行下标访问返回。
测试
void test1() {
vector<string> v;
v.push_back("abc");
v.push_back("123");
v.push_back("456");
v.push_back("789");
v.push_back("end");
for (int i = 0; i < v.size(); i++) {
cout << v[i] << ' ';
}
cout << endl;
}
运行
这里用了string来作为vector的类型,如果用mencpy之类的直接拷贝,会有析构两次的风险。
2.迭代器的实现
1.重定义迭代器
typedef T* iterator;
const typedef T* const_iterator;
迭代器一般都分为可修改和不可修改,所以这里重定义也重定义了两个。
2.end和begin
iterator end() {
return _finish;
}
iterator begin() {
return _start;
}
const_iterator end()const {
return _finish;
}
const_iterator begin()const {
return _start;
}
begin返回start,end返回finish。也是分为可修改和不可修改。
测试
void test2() {
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
for (auto e:v) {
cout << e << ' ';
}
cout << endl;
}
auto的底层实现就是靠迭代器的,使用auto能够通过就说明了迭代器已经完成了。
运行
3.常用函数的实现
1.swap
void swap(vector<T>& v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
思路:
直接使用库里的函数swap交换三个成员变量。
2.resize
void resize(size_t sz,T val = T()) {
if (sz <= size()) {
_finish = _start + sz;
}
else {
reserve(sz);
while (_finish != _endofstorage) {
*_finish = val;
_finish++;
}
}
}
resize函数中,val的缺省值是默认构造,因为vector支持内置类型和自定义类型,所以缺省值不能给0这样固定的数,需要去调用这种类型的构造参数,为了支持这个功能,在c++中内置类型也是有构造函数的。
也就是说可以这样赋值
int i = int(1);
思路:
第一步比较所需的数据个数和目前的数据个数。
若不大于,则直接修改finish即可。
若大于,无需判断内存是否足够,直接复用reserve即可,reserve会进行检查,之后填充数据。
3.insert
void insert(iterator pos,const T& val) {
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage) {
size_t len = pos - _start;
reserve((capacity() == 0) ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish-1;
while (pos <= end) {
*(end + 1) = *end;
end--;
}
*pos = val;
++_finish;
}
insert的实现是传一个迭代器的位置pos和一个需要插入的数据val过来,在pos之前插入val。
思路:
第一步检查pos是否在可插入的范围内,即在start到finish这部分地址之中,不存在即插入违规,直接报错。
第二步检查内存是否足够,如果finish和endofstorage相同,说明,需要扩容。扩容会产生一个问题,原本的pos位置也会被释放,所以需要先保存pos和start的差值,才能在扩容后找到应该插入数据的位置。
第三步,开始向后移动数据,直到pos位置的数据向后移位结束。在pos位置插入val,++finish。
4.erase
iterator erase(iterator pos) {
assert(pos >= _start);
assert(pos < _finish);
iterator end = pos+1;
while (end < _finish) {
*(end-1) = *end;
end++;
}
--_finish;
return pos;
}
erase的实现是传一个迭代器的位置过来,删除pos位置的数据。
思路:
第一步检查pos是否在可删除的范围内,即在start到(finish-1)这部分地址之中,不存在即删除违规,直接报错。
第二步,开始向前移动数据,直到pos的后一位数据移到pos位置向前移位结束,--finish。
5.拷贝构造函数
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr){
reserve(v.capacity());
for (auto& e : v) {
push_back(e);
}
}
思路:
先在初始化列表那边,全部置空,因为编译器对内置类型不一定处理,尾插的时候就会出问题,也会有其他的bug。
之后先提前开好空间,然后从被拷贝的对象中去数据进行尾插。
6.operator=
vector<T>& operator=(vector<T> tmp) {
swap(tmp);
return *this;
}
思路:
直接使用swap和拷贝过来的tmp进行交换。
测试
void test3() {
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector<int> v1;
v1 = v;
for (auto e : v) {
cout << e << ' ';
}
cout << endl;
for (auto e : v1) {
cout << e << ' ';
}
cout << endl;
v.insert(v.begin(), 0);
v.insert(v.end(), 6);
for (auto e : v) {
cout << e << ' ';
}
cout << endl;
v.erase(v.begin());
for (auto e : v) {
cout << e << ' ';
}
cout << endl;
v1.resize(9, 1);
for (auto e : v1) {
cout << e << ' ';
}
cout << endl;
}
将新实现的函数都用一下
没有问题。
但是insert和erase使用的时候可能会有迭代器丢失的问题,所以一般来说insert和erase之后这个迭代器就能使用了。