std是一个容器和算法相关的库,顺序表作为一个常见的容器也在标准库中有相应的实现--vector。今天我们就来简单的认识一下vector的使用,并且简单的模拟实现一个我们的vector
具体vector类的描述可以参考vector - C++ Reference (cplusplus.com)
在不同的编译器下string类的实现各有差异,这里我们使用的是Microsoft Visual Studio Community 2022 (64 位) - Current 版本 17.8.5
了解一个类,首先我们要先了解它的构造函数
声明一下,allocator_type是std六大组件之一的空间配置器,目前我们可以直接将其忽略。
首先vector提供了一个默认的构造函数--default(1),我们简单的写一段程序来观察一下
此时我们首先可以得到一个结论,那就是默认的构造函数会将属性赋值为0(这里泛指各种0,例如数字0,0.0亦或是指针nullptr)。其次我们发现标准库使用的是三个指针来维护这个顺序表的。
我们合理猜测_Myfirst标识这块空间的开始位置,_Mylast标识这块空间最后一个有效元素的下一个位置,_Myend标识这块空间结束的下一个位置。
这样_Mylast-_Myfirst就能得到size(有效元素个数),_Myend_Myfirst就能得到capacity(有效元素个数)
第二个构造函数可以一次性初始化n个元素,这个元素可以指定,不指定则使用默认值(这里会调用元素类型的默认构造)
第三个构造函数可以使用迭代器区间(左开右闭)来初始化,这可以和其他容器适配(因为容器是std六大组件之一,std的使用容器都支持迭代器)
第四个就是一个拷贝构造,这里还是要注意深浅拷贝的问题
首先vector应该可以存储各种各样的数据类型,我们也实现一个类模板来适配各种数据类型,我们也学习标准库里用三个指针维护顺序表的方式实现
namespace zzzyh {
template
class vector
{
public:
typedef T* iterator;
private:
iterator _start = nullptr;//开始位置
iterator _finish = nullptr;//有效元素的下一个位置
iterator _end_of_storage = nullptr;//结束位置的下一个位置
};
}
这里T类型就是要存储的数据类型,我们也发现顺序表这种结构下的指针就是天然的迭代器,我们定义T*为迭代器(当然标准库的实现要比我们这复杂很多,包括但不限于各种检查,我们这里就实现它基础的功能)
有了类的框架,我们就可以来实现他的构造函数
vector()
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{}
默认构造很简单,直接全部赋为空值即可
vector(int size,const T& x = T())
{
T* tmp = new T[size];
_start = _finish = tmp;
_end_of_storage = tmp + size;
for (size_t i = 0; i < size; i++)
{
push_back(x);
}
}
这里我们可以初始化n个元素,并且指定初始化的值。这里我们复用了push_back方法,它的作用是尾插一个指定元素,但它的实现还是略微有点复杂,还牵扯到扩容(reserve)的逻辑,这两块内容我们在后面展开,这里先都直接使用了
我们再来实现一个拷贝构造
vector(const vector& tmp)
{
size_t size = tmp.size();
reserve(size);
for (auto& x : tmp) {
push_back(x);
}
}
这里还是要注意深浅拷贝的问题,不能简答的理解为赋值操作,而是要开辟一块独立的空间出来
最后我们要来实现一个迭代器区间的构造函数
template
vector(InputIterator first, InputIterator last)
{
while (first != last) {
push_back(*first);
first++;
}
}
在类模板中我们依然可以使用函数模板,此时使用函数模板就可以实现接收各种各样的迭代器
注意,while条件里最好还是使用!=号,因为某些容器在内存上是不连续的(例如链表)
显然,这个类是有资源需要释放的,那么使用我们默认生成的析构函数是有内存泄露的风险的,需要我们再自己实现一份
~vector() {
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
这里如果_start是nullptr不会做任何处理,我们就不检查了
operator=也是默认生成的函数,但我们在拷贝构造的部分就讲了,vector是存在深浅拷贝的问题就的,所以我们同样还需要手动实现operator=
//template 类的内部不需要
vector& operator=(const vector& tmp) {
if (this == &tmp) {
return *this;
}
vector v(tmp);
swap(v);
return *this;
}
这里的迭代器用法和功能上和我们再string里讲的完全相同,我们就挑前四个简单实现一下
iterator begin() {
return _start;
}
iterator end() {
return _finish;
}
typedef const T* const_iterator;
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
下面两组关于逆遍历的迭代器我们就先不展开了
对于一个容器,容量相关的操作尤为重要,下面我们边介绍边实现一些常用的容量操作函数
得到数据的个数
size_t size() const
{
return _finish - _start;
}
得到最大容量
size_t capacity() const
{
return _end_of_storage - _start;
}
判断容器是否为空,即有没有有效元素,空返回true,否则返回false
bool empty() {
return _finish == _start;
}
reserve可以将容量增加到不小于指定大小。如果指定大小小于等于当前容量不做任何处理,大于当前容量会将容量增加到不小于指定大小,具体多大取决于编译器的实现,只保证不会影响数据
void reserve(size_t n) {
size_t sz = size();
size_t oldCapacity = capacity();
if (n <= oldCapacity) {
return;
}
int newCapacity = n > oldCapacity*2 ? n : oldCapacity * 2;
T* rem = new T[newCapacity];
if (sz != 0) {
//memmove(rem, _start, sizeof(T) * sz);
for (size_t i = 0; i < sz; i++)
{
rem[i] = _start[i];
}
//delete[] _start;
}
delete[] _start;
_start = rem;
_finish = rem + sz;
_end_of_storage = rem + newCapacity;
}
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代
价缺陷问题
resize则会影响数据。如果指定大小<原大小,会将数据减少至指定大小;如果指定大小==原大小数据层面可以理解为不做处理,如果指定大小>原大小则会扩容至指定大小,可以指定扩容使用的值,不指定依然使用默认值。resize在开空间的同时还会进行初始化,影响size
void resize(size_t n,const T& x = T()) {
if (n == size()) {
}
else if (n < size()) {
_finish = _start + n;
}
else {
reserve(n);
int i = n-size() ;
while (i > 0)
{
this->push_back(x);
i--;
}
}
return;
}
这可以尾插一个指定的元素,前面我们多次使用它,现在我们就来简单的实现一下
void push_back(const T& data) {
if (_finish == _end_of_storage) {
reserve(capacity() == 0 ? 1 : capacity() * 2);
}
new(_finish)T(data);//这里和下面注释的代码起到相同的效果,不过这里是拷贝构造
//*_finish = data; 这里是赋值重载
_finish++;
}
尾删一个元素,并且弹出元素的值(标准库中未弹出,没有返回值,我们这里实现一个弹出版本的)
T pop_back() {
if (!empty()) {
T ret = *(_finish - 1);
_finish--;
return ret;
}
}
查询第一个符合指定的元素,如果查询到返回此元素的迭代器,否则返回有效元素的下一个位置
注意,这是std算法模块提供的函数,所以我们在vector中不提供具体实现,只简单介绍其用法
在pos位置之前插入一个指定的元素
iterator insert(iterator pos, const T& data) {
assert(pos >= _start && pos <= _finish);
if (pos == _finish) {
push_back(data);
return pos+1;
}
if (_finish == _end_of_storage) {
int i = pos - _start;
reserve(capacity() == 0 ? 1 : capacity() * 2);
pos = _start + i;
}
iterator end = this->end()-1;
while (end >= pos) {
//memmove(end + 1, end, sizeof(T));
*(end + 1) = *(end);
end--;
}
new(pos)T(data);
_finish++;
return pos+1;
}
清除所有元素
void clear() {
_finish = _start;
}
得到指定下标的元素
T& operator[](int i) {
return *(_start + i);
}
const T& operator[](int i) const
{
return *(_start + i);
}
交换两个容器的内容
void swap(vector& tmp) {
std::swap(this->_start, tmp._start);
std::swap(this->_finish, tmp._finish);
std::swap(this->_end_of_storage, tmp._end_of_storage);
}
删除指定位置的元素
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
if (pos == _finish - 1) {
pop_back();
return pos-1;
}
//delete (this[pos - _start]);
//int i = pos - _start;
//memmove(pos, pos + 1, sizeof(T) * (size() - 1 - (pos - _start)));
iterator left = pos;
while (left!=_finish-1) {
*left = *(left + 1);
left++;
}
//pos = _start+i;
_finish--;
return pos;
}
迭代器在vector中的本质就是指针,在我们进行容量相关的操作的时候,不可避免的会对元素存储位置进行修改(例如扩容时会释放旧空间,开辟新空间),如果我们在使用未更新的迭代器会产生类似于野指针的问题,这就是迭代器失效
迭代器失效的原因:
等……
注意,vs对迭代器的检查是较为严格的,使用失效的迭代器程序就直接崩溃,但在Linux中gcc编译下检查的不严格不一定会崩溃。但是失效的迭代器我们认为使用是有风险的,建议更新迭代器后再使用,这也是为什么前面我们在实现容量相关函数的时候,返回值是一个迭代器,就是为了解决迭代器失效的问题
string的迭代器也同样存在这样的问题,不过string的迭代器与下标相比并不常用,所以没在string中强调迭代器失效的问题
迭代器失效的解决办法就是及时更新迭代器
memmove/memcpy只是单纯的复制拷贝,并不会开辟额外的空间
如果我们在vector中存储类,例如string,在扩容等容量操作时进行单纯的复制拷贝,此时进行的是浅拷贝,会导致新旧两块空间指向同一块内存,此时我们再释放旧空间会导致新空间跟着一起被释放
所以我们在实现这些容量相关的操作时建议使用深拷贝
以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。
因为这对我很重要。
编程世界的小比特,希望与大家一起无限进步。
感谢阅读!