🔥个人主页:Quitecoder
🔥专栏:c++笔记仓
vector涉及到许多细节问题,比如双层深拷贝,迭代器失效等,本篇文章我们通过模拟实现来深度理解这块的内容
目录
- `1.基本框架`
- `2.构造和销毁`
- `3.元素访问`
- `4.获取迭代器与容量操作`
- `reserve开空间`
- `5.对内容的修改`
- `迭代器失效`
1.基本框架
namespace own
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;// 指向数据块的开始
iterator _finish;// 指向有效数据的尾
iterator _endOfstorage; // 指向存储容量的尾
};
}
我们首先定义了一个模版类,这里的vector三个成员均为迭代器,而Vector的迭代器是一个原生指针,我们这里为其定义别名iterator
私有成员:
iterator _start; // 指向数据块的开始
iterator _finish; // 指向有效数据的尾
iterator _endOfstorage; // 指向存储容量的尾
这些成员变量用于管理vector
内部的动态数组
_start
: 这是一个指针,指向分配给vector
的内存区域的开始。这是数组的第一个元素_finish
: 这个指针指向数组中最后一个实际存在的元素的下一个位置。这意味着它指向结束后的第一个元素,它用来表示存储在vector
中的实际元素的结束_endOfstorage
: 这个指针指向分配给vector
的内存块的末尾。这不是最后一个有效元素的位置,而是整个内存块的结束位置,在这之后可能会有额外的未初始化空间,预留以实现当vector
增长时无需重新分配整个数组
2.构造和销毁
🔥vector()
空值初始化:
vector()
: _start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{}
我们也可以直接利用缺省值来完成:
vector()
{}
private:
iterator _start=nullptr;
iterator _finish=nullptr;
iterator _endOfStorage=nullptr;
};
🔥vector(size_t n, const T& value = T())
这个函数的功能是用n个value元素来构造一个vector
实现如下:
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
const T& value = T()
是使用了一个默认参数和引用的函数参数声明。
= T()
这部分声明了默认值,如果在调用函数时没有提供这个参数,就会使用它。T()
创建了T
类型的一个临时对象,这是通过类型的默认构造函数完成的。这意味着如果没有提供具体的value
值时,构造函数将使用T
类型默认构造出的一个新对象作为默认值。
例如,如果 T
是 int
,那么 T()
就是 0
。如果 T
是某个类类型,并且该类有一个无参数的构造函数,那么 T()
就会调用这个默认构造函数来创建一个新对象。
因此,这个参数声明使得构造函数可以具有灵活性:你既可以用特定的初始值来构造 vector
,也可以不提供初始值,让 vector
用类型 T
的默认值来填充
🔥vector(InputIterator first, InputIterator last)
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
这个函数是vector
类的一个范围构造函数(range constructor),它允许你根据一对迭代器 first
和 last
来构造一个新的 vector
对象。这个构造函数遍历从 first
开始一直到 last
结束的序列,并将每个元素添加到新构造的 vector
中
下面是详细的说明:
-
template<class InputIterator>
这一行表述了模板参数InputIterator
,它是一种迭代器类型,用于表示输入序列中的位置。它可以是指针或支持++
(前置递增)和*
(解引用)操作的任何类型的迭代器。 -
vector(InputIterator first, InputIterator last)
这是构造函数的声明,它接受两个参数,first
和last
,代表输入序列的开始和结束迭代器。序列不包括迭代器last
指向的元素。序列由[first, last)
间的元素组成,是一个左闭右开的区间
函数体内的代码逻辑如下:
while (first != last)
循环,将一直执行,直到first
迭代器等于last
迭代器,这表示已经到达了输入序列的末尾。push_back(*first)
在循环体内部调用,这个函数应该是vector
类中的成员函数,它会添加解引用迭代器first
指向的当前元素到vector
的末尾。++first
然后迭代器first
递增以便在下一次迭代中指向序列中的下一个元素。
这个构造函数可以用来构造一个 vector
,使其包含现存容器(如另一个 vector
、list
或 array
)中某个子序列的元素,或者任何由迭代器定义的元素序列。例如:
注意,除了这两个函数,我们模拟实现时需要手动增加一个函数:
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
理论上将,提供了
vector(size_t n, const T& value = T())
之后vector(int n, const T& value = T())
就不需要提供了,但是对于:vector<int> v(10, 5);
编译器在编译时,认为T已经被实例化为int,而10和5编译器会默认其为int类型就不会走vector(size_t n, const T& value = T())
这个构造方法,最终选择的是:vector(InputIterator first, InputIterator last)
因为编译器觉得区间构造两个参数类型一致,因此编译器就会将InputIterator实例化为int但是10和5根本不是一个区间,编译时就报错了故需要增加该构造方法
🔥vector(const vector& v)
拷贝构造函数实现,只需要分配好空间对元素依次尾插即可
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
上述有关reserve和push_back函数的模拟实现,我们后文会讲到
🔥~vector()
对于析构函数,我们需要释放空间并置指针指向空:
~vector()
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
3.元素访问
🔥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];
}
🔥获取首尾元素
T& front()
{
return *_start;
}
const T& front()const
{
return *_start;
}
T& back()
{
return *(_finish - 1);
}
const T& back()const
{
return *(_finish - 1);
}
4.获取迭代器与容量操作
🔥iterator begin(),iterator end()
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
🔥size(),capacity()
获取容量大小与有效元素个数,只需要进行指针相减即可
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
reserve开空间
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t old_size = size();
memcpy(tmp, _start, size() * sizeof(T));
delete[] _start;
_start = tmp;
_finish = tmp + old_size;
_endofstorage = tmp + n;
}
}
这里我们开空间完成的是一个深拷贝的过程,用 memcpy 将旧数组中的元素复制到新数组,memcpy 在这里用于基于字节的拷贝,memcpy是一个浅拷贝,那么,如果我们vector实例化为string类,这里string类进行浅拷贝会涉及到二次释放等问题
虽然我的_start指向了新空间完成深拷贝,但是string类完成的是浅拷贝,仍指向原来的空间,这里为了解决上述问题,我们不能使用memcpy来进行拷贝,我们需要进行赋值操作来进行二次深拷贝
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t old_size = size();
//memcpy(tmp, _start, size() * sizeof(T));
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = tmp + old_size;
_endofstorage = tmp + n;
}
}
通过一个循环,使用拷贝赋值操作符逐个拷贝旧数组中的元素到新数组
🔥resize()
void resize(size_t n, const T& val = T())
{
if (n > size())
{
reserve(n);
// 插入
while (_finish < _start + n)
{
*_finish = val;
++_finish;
}
}
else
{
// 删除
_finish = _start + n;
}
}
- 若resize传入的n大于capacity,进行扩容并用val来填满新位置
- 若n大于有效元素个数并小于capacity,不进行扩容,用val填满空位置
- 若n小于有效元素个数,进行删除操作
5.对内容的修改
🔥insert()
void insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
--it;
}
*pos = val;
++_finish;
}
首先是否判断需要扩容,接着进行挪动数据,由于这里是指针,挪动数据我们就不用考虑越界问题,指针不会指向零
迭代器失效
注意,上述代码我们忽略了pos的位置
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
这里就会有迭代器失效的问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针
T*
。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃,即如果继续使用已经失效的迭代器,程序可能会崩溃
扩容后,我原先pos指向的位置被释放,这里pos变的不可用
所以这里我们需要更新pos位置
if (_finish == _endofstorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
// 如果扩容了要更新pos
pos = _start + len;
}
首先,记录pos到起始位置的大小,更新后新的start加上距离即可
在C++标准模板库(STL)中,迭代器失效(Iterator invalidation)是指当底层容器(例如vector
,list
或map
等)发生改变时,其迭代器可能不再指向正确的元素,或者变得完全不可用。迭代器失效通常会发生在执行插入、删除或重新分配操作后
对于不同类型的容器,迭代器失效的条件会有所不同。对于vector
:
-
增加容器中的元素(例如通过
push_back
、insert
等)可能会导致存储空间重新分配,从而使所有指向容器元素的迭代器、指针和引用失效。如果容器在插入新元素前还有足够的capacity
(未使用的预留空间),一般来说,除了指向插入点之后元素的迭代器之外,其他的迭代器、指针和引用会保持有效。 -
删除容器中的元素(例如通过
erase
、pop_back
等)会使所有指向被删除元素以及之后元素的迭代器、指针和引用失效。 -
调整容器的大小(例如通过
resize
)至大于当前size
可能会导致重新分配,这也将导致所有迭代器、指针和引用失效。
当涉及vector
类的成员函数时,需要确保任何可能导致迭代器失效的操作之后都不使用旧的迭代器。例如,在调用insert
的例子中,如果进行了扩容操作,之前的pos
迭代器就将失效,因为reserve
可能会导致动态数组的重新分配。所以代码中重新计算了pos
的值来防止迭代器失效
要安全地使用迭代器,最好的实践是避免在迭代过程中修改容器的大小和结构,或者如果确实需要修改,则应在每次修改后重新获取迭代器
🔥erase()
注意!erase返回的值是迭代器
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator it = pos + 1;
while (it < _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
erase
函数的返回值是一个迭代器,它指向被删除元素的原位置。由于元素已经被移动了,这个位置现在包含了前一个被删除元素位置之后的元素。
🔥push_back和pop_back
我们这两个函数直接复用上面的函数即可:
void push_back(const T& val)
{
insert(end(), val);
}
void pop_back()
{
erase(end() - 1);
}
本节内容到此结束!!感谢大家阅读,文章完整代码如下:
namespace own
{
template<class T>
class vector
{
public:
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;
}
vector()
{}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
bool empty()
{
return _start == _finish;
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
~vector()
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t old_size = size();
//memcpy(tmp, _start, size() * sizeof(T));
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = tmp + old_size;
_endofstorage = tmp + n;
}
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
T& front()
{
return *_start;
}
const T& front()const
{
return *_start;
}
T& back()
{
return *(_finish - 1);
}
const T& back()const
{
return *(_finish - 1);
}
void resize(size_t n, const T& val = T())
{
if (n > size())
{
reserve(n);
// 插入
while (_finish < _start + n)
{
*_finish = val;
++_finish;
}
}
else
{
// 删除
_finish = _start + n;
}
}
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
pos = _start + len;
}
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
--it;
}
*pos = val;
++_finish;
}
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator it = pos + 1;
while (it < _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
void push_back(const T& val)
{
insert(end(), val);
}
void pop_back()
{
/*assert(!empty());
--_finish;*/
erase(end() - 1);
}
private:
iterator _start=nullptr;
iterator _finish=nullptr;
iterator _endOfstorage=nullptr;
};
template<class T>
void print_vector(const vector<T>& v)
{
for (size_t i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
//typename vector<T>::const_iterator it = v.begin();
/*auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)
{
cout << e << " ";
}
cout << endl;*/
}
}