上一篇,我们已经了解了关于vector的常见使用接口了,现在,为了对vector的认识更加深入,我们现在参考STL库里面来模拟实现vector。
ps:由于只是为了深入认识vector的知识,我们这里并不会完完全全按照STL的实现形式,而是根据我们作为新手初始学习的基础来模拟实现的。等到我们对C++的认识更加深入时,我们再去看看相关的书籍层层解析vector更加具体的实现方式。
首先,让我们来看看库里面是怎么定义它的成员变量的:
ps:侯捷老师的《STL源码剖析》
可能刚开始一看的时候,发现这些变量是什么意思啊?它为什么要用一个迭代器来作返回值类型呢?先别急,让我们一层一层看下去。
其实,对于刚开始看不懂时,我们可以再来借助它的其他成员函数的实现一起来帮助我们分析为何?
现在,我们只需看方框里面的就行,其他看不懂的先不管它,等到我们的功底变更厉害再去理解。其实,对应我画的方框,和结合我们之前(string的模拟实现的经历),它应该是一个指向开始,结尾的迭代器之类的变量。
它的基本框架应该是这么一个样子的:
现在我们展示出侯捷老师的书的一张图:
但是,我们会发出这样的一个疑问:
1.为啥要用迭代器呢?
2.为啥它不像我们之前写string模拟时,用T* a;int size,int _capacity?
解答:
首先,我们复习一下
迭代器作用:
ps:之前我们有讲过迭代器的实现是类似指针的一种东西。
1.迭代器按顺序访问向量中的每一个元素。这样不够你的类型是什么eg:list,vector,int以及以后学到的map。都是可以使用的。
2.迭代器提供了一种统一的访问方式。不同容器(如顺序容器和关联容器)的数据结构不同,但是迭代器提供了通用的接口来访问元素,使得算法能够以一种统一的方式操作不同类型的容器。比如,标准库中的 std::for_each 算法可以通过迭代器作用于各种容器,而不需要为每种容器单独写一个遍历函数。
3.迭代器还可以用来指示容器中元素的位置,方便在容器特定位置进行插入或删除操作。不过需要注意的是,对于某些容器(如 std::list )迭代器在插入或删除元素后可能会失效,所以在这些操作后需要谨慎使用。4.代码的复用,减少了代码的冗余。
5.封装性,不用去考虑底层数据存储的细节
这里有讲解我们迭代器失效的几种情况:
所以,我们的成员变量可以得出:
为什么要使用迭代器:
start 指向已使用内存空间的起始位置,用于标记数据存储的开头,通过它可以访问容器中存储的实际元素。 finish 指向已使用内存空间的末尾,它帮助我们确定容器中有效元素的范围。例如说,当你使用 push_back 添加元素时, finish 会相应更新,以始终指向最后一个有效元素之后的位置。可以利用 finish - start 来计算当前容器中有效元素的数量。 end_of_storage 指向整个分配内存空间的末尾。这用于管理内存分配。当向 vector 中添加元素导致 finish 等于 end_of_storage (即已使用的空间已满)时, vector 会触发重新分配内存的操作,以获取足够的空间来存储新元素。 作用 这三个迭代器协同工作,能够高效地管理内存和元素访问,使 vector 可以灵活地动态增长和收缩。 由此,也可以知道了为什么我们不用之前的size,capacity的形式了吧?
因为,我们要实现的vector并不只局限于满足只存储一种固定的元素,我们在实际当中会创建不同类型的元素的vector。eg:int,string,double,char类型等等。
因此,
定义成员变量:
//为了防止与库里面的命名冲突
namespace bai
{
//模板
template <class T>
class vector
{
typedef T* iterator;
public:
private:
iterator _start=nullptr;
iterator _finish=nullptr;
iterator _endofstorage=nullptr;
};
}
构造函数:
为了更好初始学习,我们并不会像库那样一模一样地实现(只是简单):
一:无参的构造函数
这里我们直接使用初始化列表,把所有的迭代器都指向nullptr
vector() :_start(nullptr) , _finish(nullptr) , _endofstorage(nullptr) {}
当然,也可以写成这样子(但是两个不能同时存在,因为都是无参的构造函数,编译器不能识别)
为什么呢?因为我们定义的变量的内置成员,这里我们默认不写的话编译器就会自动生成,内置函数不做处理(虽然有些编译器会处理,但也只是个性化行为,不是所有的都是),自定义函数会自动去调用默认函数。我们在成员变量已经初始化了,所以也可以这样子(看定义成员变量那里)
vector() {}
二:拷贝构造函数
ps:就是根据传入的参数vector来重新拷贝构造一个新的vector。
但是,要注意的是,我们要防止浅拷贝的问题(包括vector本身和vector中的对象),否则,造成浅拷贝会公用同一块空间,析构时释放两次,而造成程序崩溃。
vector(const vector<T>& v) { _start = new T[v.capacity()]; //memcpy(_start, v._start, sizeof(T)*v.size()); for (size_t i = 0; i < v.size(); i++) { _start[i] = v._start[i]; } _finish = _start + v.size(); _endofstorage = _start + v.capacity(); }
三: n个val的构造函数
ps: 解释为什么是要val=T() ?
原因:
在函数中,T()是一个默认参数。T是一个模板,代表的是vector的类型。那么T()实际就是在调用T类型的默认构造函数。但我们调用vector这个默认的构造函数时,如果只传进来一个参数n,没有传进val,那么val就会被初始化为T的默认值。
但看这个可能有点抽象,那我们举个例子:
我们在主函数调用了这么一个:
vector<int> v(6,6).
那么T就是int(内置类型),而val=6,这里的T(),就相当于int(6,6)了。所以就达到了创建一个6个6的vector。
如果 T 是一个自定义的类类型,比如 class MyClass ,并且 MyClass 有一个默认构造函数,那么 T() 就会调用 MyClass 的默认构造函数来生成一个默认对象,用于填充 vector
同样这里是因为在成员变量里写了初始值,所以可以不写初始化列表,反之要写
另外,这里的本质就是将一个空的vector,,扩充数据(初始化)到长度为n的过程。
这个过程就跟resize的接口功能一样了,所以我们可以直接复用resize函数(这个在下面又讲)
vector(int n, const T& val = T())
{
resize(n, val);
}
四:迭代器区间初始化:
优势:
1.灵活性:
2.一致性:(上面又讲)
3.封装性:只要提供对迭代器的区间即可,无论数据来自于哪里,都可以通过迭代器统一的逻辑进行元素的添加和初始化,提供代码的复用和可维护性。
讲解:
因为函数接收到的迭代器的类型是不确定的,所以我们在这里定义了一个函数模板,然后再将迭代器区间的数据一个个尾插到容器中。即可完成了初始化。
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
库中实现了多个构造函数:
不知道有没有人会有这么一个疑惑,为什么要写那么多个构造函数?我就有这个一个疑问:最后得出了结论:
主要有以下几个原因:
方便不同的初始化场景
-指定大小和初始值初始化 像 vector(size_t n, const T& val = T()) 和 vector(int n, const T& val = T()) 这种构造函数,允许用户方便地创建一个具有指定数量元素,且元素具有相同初始值的 vector 。例如,创建一个包含10个初始值为0的 int 类型 vector : vector<int> v(10, 0) 。 - 范围初始化: 模板构造函数 template<class InputIterator> vector(InputIterator first, InputIterator last) 可以使用一个范围(如其他容器中的部分元素范围)来初始化 vector 。假设已经有一个 list<int> myList ,可以用它的元素范围来初始化 vector : vector<int> v(myList.begin(), myList.end()) 。
灵活性和兼容性 - 类型兼容: 有 size_t 和 int 类型参数的构造函数( vector(size_t n, const T& val = T()) 和 vector(int n, const T& val = T()) )提供了灵活性。在一些场景下,用户可能会自然地使用 int 类型来表示元素数量,通过提供 int 参数的构造函数可以更好地兼容这种习惯。 - 接口通用性: 多种构造函数使得 vector 的接口更符合C++ 标准库中其他容器的接口风格,这样用户在使用不同容器时,能够以相似的方式来初始化它们,方便记忆和使用。
析构函数:
释放之前开辟的空间,再把成员变量置空。
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
operator=赋值函数:
对于赋值运算符重载,我们在模拟string的时候写过了,在vector中也是可以直接使用的。
我们来重新捋一下思路:
创建一个临时变量,把s的数据拷贝到一个局部变量temp中,再将temp内容与this内容交换,这样就做到了赋值。而它的优势是不用另外去释放,因为它是局部变量,局部变量出了函数作用域自动销毁。
同时,我们也有另外一种优化方案:
就是直接通过形参代替了创建临时拷贝的temp,(节省了一下拷贝构造)
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; }
迭代器的begin()函数
//可读可写
iterator begin()
{
return _start;
}
//只读
const_iterator begin()const
{
return _start;
}
end()函数
//可读可写
iterator end()
{
return _finish;
}
//只读
const_iterator end()const
{
return _finish;
}
size大小
size_t size() const
{
return _finish - _start;
}
capacity容量大小
size_t capacity() const
{
return _endofstorage - _start;
}
reserve()预留空间
步骤讲解:
1.判断n是否大于当前的容量,来决定是扩容还是缩容。只不过现在的电脑的内存空间基本完全支持你在这里多开出来的空间,可以忽略掉,所以我们一般只需要考虑扩容的情况。
再加上,vector扩容涉及到重新分配到一块更大的空间,再释放旧空间,那么,如果大量的调用扩容和缩容,会导致大量的元素复制和移动操作,这会有比较高的时间成本。还有就是,我们来想想vector的reserve的功能:主要是用来预先分配足够的空间,以免后续插入元素的时候频繁地进行内存重新分配。很多场景下,一旦空间分配的足够大,很少需要再缩容的(因为容易误用),(插入时不小心调用到缩容的话,后面再插入就又要重新扩容操作)。
过程:
1.动态开辟一段n的容量空间
2.原数据复制到新的空间
3.释放旧空间
4.更新数据(_start,_finish,_endofstorage)
两大易错点:!!!!!
1.使用memcpy,造成了浅拷贝。(同上面的拷贝构造不用的原因)
在 vector 的 reserve 操作中使用 memcpy 可能导致浅拷贝主要是因为:
memcpy 是按字节进行数据复制的低级操作。对于像包含指针成员的自定义类对象(或者是存的是像指针这种东西)(eg:string)的 vector , memcpy 只会复制指针的值(字节层面),而不是复制指针所指向的内容。例如,假设有一个 vector 存放自定义的 MyClass 对象, MyClass 有一个成员是指针指向动态分配的资源(如 char* buffer )。
当用 memcpy 复制 MyClass 对象时,只是简单地复制了 buffer 这个指针变量的值,而不是复制 buffer 所指向的实际字符数组。
这就导致两个不同的 MyClass 对象中的指针成员指向同一块内存区域,如果其中一个对象释放了这块内存,另一个对象再访问就会出现问题。
因此!不能使用memcpy,而要通过赋值运算符重载来调用它自己的深拷贝。
2.没有提前记录好size()的个数!
_start = tmp; _finish = _start + size(); _endofstorage = _start + n;
像上面:
1.若我们调用的是无参构造函数。
那么,此时我们的三个成员变量都为nullptr。
然后,到了_finish那一步,我们再来看看我们的size()是如何实现的?
那么,我们带进去,相当于_finish=_start+(_finish-_start)=_start+(nullptr-_start)=nullptr.
你看,这是不是相当于没有更新,那么我们后续使用到_finish的时候,这是不是就会出现错误而造成程序崩溃的呢?
因此,我们该怎么解决这种情况呢?我们先从问题出现的源头出发,问题就出现在size()的过程。因此,我们不妨在此之前先保存下来size()的个数,防止直接调用size()过程中出现的错误。
void reserve(size_t n)
{
if (n > capacity())
{
//先计算保存size的个数
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * sz);--->error
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
//_finish = _start + size(); --->error
_finish = _start + sz;
_endofstorage = _start + n;
}
}
resize()函数
这里跟我们写过的模拟string的resize的思路差不多的。
同样,vector中,我们的整体思路是不变的,唯一需要注意的是,我们由于使用的是迭代器,所以我们在遍历的时候也是用迭代器的方法,更改(初始化)为某个T类型的val
注意:迭代器!!!赋值时记得*
void resize(size_t n, const T& val = T()) { if (n < size()) { _finish = _start + n; } else { reserve(n); while (_finish != _start + n) { *_finish = val; ++_finish; } } }
Insert()插入函数
这里又写到insert和erase造成迭代器失效的原因分析!需要的自行去看噢~
所以我们该怎么解决这个问题?
我们采用:先提前记录下pos的位置,然后如果扩容了,在重新找pos的位置就很容易了。
举个形象的例子:
你在一个旧的教室里的位置是固定的5号位,那么,当有一天,由于上课人数超出了教室容量的的最大值,所以我们要重新找一间更多座位的教室,在此过程中,肯定是要基于原来的人数寻找教室的,(这就相当于拷贝到新的空间),那么,由于在旧的教室里你的座位号是5,你记下来了,那么到了新教室,你是不是就只要寻找新教室的5号位的座位就行了。这就很容易找到你的座位了。
注意:由于内部迭代器失效解决后,并不能也就此解决了外部,因此,我们在insert后,尽量不要再使用之前的形参迭代器,若非要使用,由于返回值是pos位置的迭代器,所以在调用时要注意接收这个返回值来更新pos值!
注意:这是迭代器!!!!!别忘了加*
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
// 解决pos迭代器失效问题
pos = _start + len;
}
//挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
//插入数据
*pos = x;
++_finish;
return pos;
}
ps:为什么要返回pos的一个迭代器?
当你插入元素后,这个返回的迭代器指向新插入的元素。这在一些连续操作场景下很有用。例如,如果你想在插入一个元素后,马上基于这个新插入的元素位置再进行其他操作,就可以直接使用这个返回的迭代器。
假设你有一个 vector<int> ,插入一个元素后,可以方便地用返回的位置来在新插入元素的后面再插入元素或者进行查找等操作
erase()删除函数
同样,这篇文章里有写关于erase造成迭代器失效的原因。
所以我们得在实际使用它的接口时,重新赋值,更新迭代器。
iterator erase(iterator pos) { assert(pos >= _start && pos < _finish); iterator it = pos + 1; while (it != _finish) { *(it - 1) = *it; ++it; } --_finish; return pos; }
erase为啥要返回pos迭代器?
当你使用 erase 删除元素后,它返回的迭代器指向被删除元素之后的元素。这在遍历容器并删除某些元素的场景下很有用。
例如,在循环中删除满足一定条件的元素时,就可以利用返回的迭代器来更新当前位置,确保遍历过程正确。
operator[]()函数
//可读可写 T& operator[](size_t pos) { assert(pos < size()); return _start[pos]; }
//只可读 const T& operator[](size_t pos) const { assert(pos < size()); return _start[pos]; }
push_back()尾插函数
void push_back(const T& x) { if (_finish == _endofstorage) { size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; reserve(newcapacity); } *_finish = x; ++_finish; }
同时,我们还有一种写法:复用insert()
void push_back(const T& x) { insert(end(), x); }
pop_back()尾删函数
//复用erase函数 void pop_back() { erase(--end()); }
好了,关于vector的模拟实现就分享到这里了,希望我们都共同进步!
全部代码展示:
ps:其实这里本意给出全部代码也没太大意义。但是,这里想要展示的是声明和定义不分离的认识
尽量不要分离声明和定义,否则会出现很多不必要的麻烦(具体的以后讲到模板的进阶再具体聊聊)
vector.h
#pragma once
#include<assert.h>
namespace bai
{
template <class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
const_iterator begin()const
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator end()const
{
return _finish;
}
vector(size_t n, const T& val = T())
{
resize(n, val);
}
vector(int n, const T& val = T())
{
resize(n, val);
}
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{ }
template<class InputIterator>
vector(InputIterator frist, InputIterator last)
{
while (frist != last)
{
push_back(*frist);
frist++;
}
}
vector(const vector<T>& v)
{
_start = new T[v._capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endofstorage = _start + v._capacity();
}
/*vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(v._capacity());
for (auto e : v)
{
push_back(e);
}
}*/
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage,v._endofstorage)
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
void reserve(size_t n)
{
if (n > _capacity())
{
int sz = _size();
T* temp = new T[n];
// memcpy(temp, _start, sizeof(T) * sz);
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
temp[i] = _start[i];
}
delete[] _start;
}
_start = temp;
_finish = _start + sz;
_endofstorage = _start+n;
}
}
void resize(size_t n, const T& val = T())
{
if (n < _size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
_finish++;
}
}
}
void push_back(const T& x)
{
/*if (_finish == _endofstorage)
{
size_t _newcapacity = _capacity() == 0 ? 4 : _capacity()*2;
reserve(_newcapacity);
}
*_finish = x;
_finish++;*/
insert(end(), x);
}
size_t _size()
{
return _finish-_start;
}
size_t _capacity()
{
return _endofstorage - _start;
}
T& operator[](size_t pos)
{
assert(pos<_size());
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos <_size());
return _start[pos];
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = _capacity() == 0 ? 4 : _capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
//挪动数据
iterator end = _finish - 1;
while (pos <= end)
{
*(end + 1) = *end;
end--;
}
//迭代器本质上是一个指针或者是类似指针的东西
*pos = x;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
it++;
}
_finish--;
return pos;
}
private:
iterator _start=nullptr;
iterator _finish=nullptr;
iterator _endofstorage=nullptr;
};
void vector_test1()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
void vector_test2()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
for (auto &e : v1)
{
e++;
cout << e << " ";
}
cout << endl;
for (size_t i = 0; i < v1._size(); i++)
{
v1[i]++;
cout << v1[i] << " ";
}
}
void vector_test3()
{
vector<string> s;
s.push_back("aaaa");
s.push_back("bbbb");
s.push_back("cccc");
s.push_back("dddd");
s.push_back("eeee");
s.push_back("ffff");
vector<string>::iterator it = s.begin();
s.insert(it, "300");
/*while (it != s.end())
{
cout << (*it) << " ";
it++;
}*/
//*it += 30;
for (auto e : s)
{
cout << e << " ";
}
}
void vector_test4()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(5);
v1.push_back(5);
v1.push_back(5);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
v1.insert(v1.begin(), 100);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
/*vector<int>::iterator p = v1.begin() + 3;
v1.insert(p, 300);*/
vector<int>::iterator p = v1.begin() + 3;
//v1.insert(p+3, 300);
// insert以后迭代器可能会失效(扩容)
// 记住,insert以后就不要使用这个形参迭代器了,因为他可能失效了
v1.insert(p, 300);
// 高危行为
// *p += 10;
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
void vector_test5()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator it = v.begin();
//v.erase(it);
v.resize(5, 8);
for (auto e : v)
{
cout << e << " ";
}
}
void vector_test6()
{
vector<string> s;
s.push_back("aaaa");
s.push_back("bbbb");
s.push_back("cccc");
s.push_back("dddd");
s.push_back("eeee");
s.push_back("ffff");
//vector<string>::iterator it = s.begin();
//s.insert(it, "300");
/*while (it != s.end())
{
cout << (*it) << " ";
it++;
}*/
//*it += 30;
s.resize(9, "555555");
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
vector<int> v(6, 0);
for (auto e : v)
{
cout << e << " ";
}
}
void vector_test7()
{
int a[]{ 1,2,3,4,5,6 };
vector<int> v(a, a + 3);
}
}
test.cpp
#include<iostream>
using namespace std;
#include "vector.h"
int main()
{
bai::vector_test6();
return 0;
}
最后,鸡汤环节:
坚持住!