C++语言原理与实践(十):vector类的底层实现

本篇目标:

掌握vector部分接口的简单实现

一.vector类的底层实现

1.三个指针

首先通过对前面vector的使用,我们就已经知道了vector可以实例化出int,vector<int>,string等各种类型,无论是内置类型,还是自定义类型,都可以实例化,那么我们自然就想到了vector的底层实现必然是有模板的参与,但是有了模板以后,我们就无法将类中接口的声明与定义放在两个不同的文件里了 ,而通过对string的底层的探索,我们知道了string底层是通过指针来管理一片连续的空间的,那么vector 如何管理一块连续空间?

首先,vector 需要记录三个信息:

  1. 空间从哪里开始:才能找到存储的元素。
  2. 已经存了多少个元素 :对应 size()。
  3. 总共能存多少个元素 :对应 capacity(),用于判断是否需要扩容。

这三个信息,可以通过三个边界位置来表示:

指针 含义
_start 存储空间的起始位置
_finish 最后一个有效元素的后一个位置
_end_of_storage 整块存储空间的结束位置,即容量边界

简化后,成员可以这样写:

cpp 复制代码
T* _start;
T* _finish;
T* _end_of_storage;

由于元素连续存储,指针相减就能得到元素个数:

cpp 复制代码
size     = _finish - _start;
capacity = _end_of_storage - _start;

例如,已经存了 3 个元素 ,但分配了 5 个元素的空间:

  • _start 位于起点。
  • _finish 位于起点后第 3 个元素的位置。
  • _endofstorage 位于起点后第 5 个元素的位置。

当 _finish == _end_of_storage 时,说明空间已经用满,再插入就需要扩容。

所以vector的大致框架 为:

在vector.h中,

cpp 复制代码
#pragma once
#include <iostream>
#include <assert.h>

using namespace std;
namespace kong
{
	template<class T>
	class vector
	{
	public:
        vector()
            :_start(nullptr),
             _finish(nullptr),
             _end_of_storage(nullptr)
        {}
	private:
		T* _start;
		T* _finish;
		T* _end_of_storage;
	};
}

注意:其实底层也是通过三个指针来实现的,但是目前vector的底层过于复杂,就不带大家来验证了。

2.迭代器

前面我们已经知道,vector 通过起始指针和有效元素的结束指针,确定了元素所在的范围。那么,当我们想在容器外部遍历这些元素时,应该如何访问它们呢?

这些指针属于 vector 的内部成员,我们可以通过 begin() 和 end() 向外提供有效元素范围的起点和终点。遍历时,我们需要一种对象:能够通过 * 访问当前元素 ,通过 ++ 移动到下一个元素,并通过比较判断是否到达终点。这样的对象,就是迭代器。

由于 vector 的元素连续存储,普通指针本身就支持这些操作,因此在模拟实现 vector 时,可以直接使用 T* 作为迭代器类型 。begin() 返回指向第一个元素的迭代器,end() 返回指向最后一个元素后一个位置的迭代器。于是,从 begin() 开始不断向后移动,直到到达 end(),就能遍历所有元素。注意,end() 表示遍历的终点,不能对它进行解引用。

示意图:

迭代器让我们通过统一的方式访问容器中的元素。对于其他存储结构的容器,也可以提供支持相应操作的迭代器,而不必让使用者直接处理它们的内部结构。

所以为了与库保持一致,我们就需要将T*typedef一下,修改后的框架:

cpp 复制代码
namespace kong
{
	template<class T>
	class vector
	{
	public:
        typedef T* iterator;
        typedef const T* const_iterator;
        vector()
            :_start(nullptr),
             _finish(nullptr),
             _end_of_storage(nullptr)
        {}
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

知道了上面的意思后,迭代器的实现就简单了,如代码所示:

cpp 复制代码
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator cbegin()const { return _start; }
const_iterator cend()const { return _finish; }

顺便将size(),capacity()和empty()实现一下:

cpp 复制代码
size_t size() const { return _finish - _start; }
size_t capacity() const { return _end_of_storage - _start; }
bool empty() const { return _start == _finish; }

3.reserve接口

我们发现,当 vector 的容量不足以容纳新元素时,需要先进行扩容。而后续的插入等操作,也可能遇到容量不足的问题。因此,我们可以将扩容逻辑单独封装成一个接口,方便其他操作复用,这就引出了 reserve。

reserve(n) 用于保证 vector 的容量至少为 n。如果 n 不大于当前容量,就不需要进行任何操作;如果 n 大于当前容量,就申请新的空间,将原有元素迁移过去,释放旧空间,并更新三个指针。

注意:reserve 只增加容量,不增加有效元素的个数。

代码块:

cpp 复制代码
template<class T>
void vector<T>::reserve(size_t n) 
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * size());
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + size();
		_end_of_storage = _start + n;
	}
}

注意:这个是将reserve的实现放到了类外的,但是我们还要指明这是那一个类里面的,以及添加模板,后续对应一些较长的代码我都是写在类外的,当然大家也可以写在类里面。

4.push_back接口

前面我们已经了解了 vector 如何通过三个指针管理连续空间,以及如何通过迭代器访问其中的元素。那么,当我们需要向 vector 中添加一个新元素时,又该如何操作呢? 这里先考虑最常用的尾部插入操作 ,也就是 push_back 接口。

由于 _finish 指向最后一个有效元素的后一个位置,如果还有剩余容量,就可以将新元素放到这个位置,然后让 _finish 向后移动一个位置。这样,有效元素的个数就增加了一个。

不过,插入之前还需要判断空间是否已经用满 。当 _finish == _endofstorage 时,说明当前容量不足,不能直接插入,需要先扩容:申请更大的空间,将原有元素迁移过去,释放旧空间,并更新三个指针,然后再完成尾部插入。因此,实现 push_back 时,需要先检查容量,再根据情况完成扩容和插入。

代码块:

cpp 复制代码
template<class T>
void vector<T>::push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		//我们需要先找到旧空间的空间大小
		size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newCapacity);
	}

	*_finish = x;
	_finish++;
}

测试代码:

cpp 复制代码
void test_vector1()
{
	kong::vector<size_t> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	v.push_back(7);
	v.push_back(8);
	for (auto& x : v)
	{
		cout << x << " ";
	}
	cout << endl;
}

可是测试结果:

这个居然显示错误了,那么就让我们调试一下吧,如图:

错误如图:

为啥这个_finish的地址是nullptr啊?我不是已经在reserve里面更新了_finish吗?

答案 :我们确实是在reserve里面更新了_finish,但是我们是先更新_start为tmp ,此时**_start指向的是新的地址** ,而**_finish仍然指向旧空间地址** ,那么我们调用size()即_finish-_start时不就是用旧地址减去新地址,然后再加上_start,那么_finish=_start+_finish-_start,那么_finish不还是旧地址吗?而我们构造时_finish不是nullptr吗? 所以我们***nullptr不就报错了吗?** 那么该咋解决?我们可以先更新_finish,但是这样写不太雅观,但是我们可以先保存一下旧的size,后面更新时用old_size+_start即可,更新后的reserve如代码所示:

cpp 复制代码
template<class T>
void vector<T>::reserve(size_t n) 
{
	if (n > capacity())
	{
		//我们需要先找到旧空间的空间大小
		size_t old_size = size();
		T* tmp = new T[n];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * old_size);
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + old_size;
		_end_of_storage = _start + n;
	}
}

此时我们再进行测试,就可以得出正确的结果了。

5.pop_back()接口

尾部删除的接口实现就非常简单了,如代码:

cpp 复制代码
template<class T>
void vector<T>::pop_back()
{
	assert(!empty());
	_finish--;
}

6.insert接口

前面我们已经实现了 push_back,可以在 vector 的尾部插入数据,那么如果我们想在开头或者中间插入数据,又该怎么办呢?这就需要引入 insert 接口了。我们可以通过迭代器指定插入位置,将新的数据插入到这个位置。

那么该如何实现呢?由于 vector 管理的是一块连续空间,我们不能直接将数据放到指定位置,否则就会覆盖原来的数据。因此,我们需要先将插入位置及其后面的元素,从后往前依次向后移动一位,腾出位置后再放入新的数据,最后让 _finish 向后移动一位 。当然,如果空间已经满了,就需要先通过 reserve 扩容,再进行插入。

然后我们这个insert的移动与string中的insert移动也一样,就不再演示了。

代码块:

cpp 复制代码
template<class T>
void vector<T>::insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);

	if (_finish == _end_of_storage)
	{
		size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newCapacity);
	}

	// 将插入位置及其后面的元素整体向后移动一位
	std::memmove(pos + 1, pos, sizeof(T) * (_finish - pos));

	*pos = x;
	_finish++;
}

测试代码:

cpp 复制代码
void test_vector2()
{
	kong::vector<int> v;
	v.insert(v.begin(), 1);
	v.insert(v.begin()+1, 2);
	v.insert(v.begin()+2, 3);
	v.insert(v.begin()+1, 4);
	v.insert(v.begin()+2, 5);
	v.insert(v.begin()+5, 6);
	for (auto& x : v)
	{
		std::cout << x << " ";
	}
	std::cout << std::endl;
}

运行结果:

为啥又出错了呢? 让我们再调试检查一下吧!

如图:

我们仔细一看,嗯?为啥又读到 nullptr 了?

此时,_start 和 _finish 都指向同一个地址,说明还没有插入任何数据;而 _end_of_storage 与 _start 的地址相差十六进制的 0x10,也就是 16 字节,对于 int 类型来说,正好可以存放 4 个元素。这说明空间已经开辟成功了啊 ,可是为什么 pos 仍然是 nullptr 呢?

那么我们再仔细想一下:这个 pos 保存的不还是扩容之前的地址吗?扩容后,_start、_finish 和 _end_of_storage 都已经更新 为新空间中的对应位置,但是 pos 并不会跟着它们自动更新,它仍然保留着原来的值。这里原来的值就是 nullptr,那我们继续使用这个旧的 pos,不就会出错了吗?

所以,我们需要在扩容之前,先保存 pos 相对于 _start 的偏移量 ;等扩容完成后,再根据新的 _start 和之前保存的偏移量,重新确定 pos 的位置,这样它才能正确指向新空间中的插入位置。

所以我们还需要更新一下pos,因此我们需要记录一下len,len=pos-_start;

如代码所示:

cpp 复制代码
template<class T>
void vector<T>::insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);

	if (_finish == _end_of_storage)
	{
        size_t len=pos-_start;
		size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newCapacity);
        pos=_start+len;
	}

	// 将插入位置及其后面的元素整体向后移动一位
	std::memmove(pos + 1, pos, sizeof(T) * (_finish - pos));

	*pos = x;
	_finish++;
}

7.erase接口

前面我们已经实现了 insert 接口,可以在指定位置插入数据,那么如果我们想删除指定位置的数据,又该怎么办呢? 这就需要引入 erase 接口了,同样通过迭代器来指定要删除的位置。

那么该如何实现呢?由于 vector 管理的是一块连续空间,删除一个元素后,我们需要将它后面的元素依次向前移动一位,覆盖掉要删除的数据,最后让 _finish 向前移动一位,这样有效元素的个数就减少了一个。这里减少的是 size,而 capacity 不变,因为我们并没有释放原来开辟的空间。

需要注意的是,end() 指向最后一个有效元素的下一个位置,并没有可供删除的元素,所以传入的位置必须满足 pos >= _start && pos < _finish。

代码块:

cpp 复制代码
template<class T>
voud vector<T>::erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);

	for (iterator it = pos + 1; it < _finish; it++)
	{
		*(it - 1) = *it;
	}

	_finish--;
}

测试代码:

cpp 复制代码
void test_vector2()
{
	kong::vector<int> v;
	v.insert(v.begin(), 1);
	v.insert(v.begin()+1, 2);
	v.insert(v.begin()+2, 3);
	v.insert(v.begin()+1, 4);
	v.insert(v.begin()+2, 5);
	v.insert(v.begin()+5, 6);
    v.insert(v.begin()+5, 6);
	for (auto& x : v)
	{
		std::cout << x << " ";
	}
	std::cout << std::endl;
    for (auto it = v.begin(); it < v.end(); )
    {
	    if (*(it) % 2 == 0)
	    {
		    v.erase(it);
	    }
		it++;
    }
    std::cout << std::endl;
    for (auto& x : v)
    {
	    std::cout << x << " ";
    }
    std::cout << std::endl;
}

运行结果:

看这个运行结果是不是非常的奇怪?为啥6没有被删除呢?那么要讲清楚这个就要提到迭代器失效问题了。

8.迭代器失效

通过上面的报错,让我来解释一下吧。

你插入数据后,vector 中的元素为:

cpp 复制代码
1 4 5 2 3 6 6

当 it 指向 4 时,执行 erase(it),后面的元素依次向前移动,得到:

cpp 复制代码
1 5 2 3 6 6

在你当前使用指针作为迭代器的实现中,it 保存的地址没有改变,但是这个位置上的数据已经由 4 变成了 5。 如果接着执行 it++,就会走到 2,跳过刚刚移动过来的 5。

跳过奇数暂时看不出问题,但是最后两个连续的 6 就能体现出来:

操作 vector 中的元素 it 所在位置
删除前一个 6 之前 1 5 3 6 6 前一个 6
删除后,后一个 6 向前移动 1 5 3 6 移动过来的 6
再执行 it++ 1 5 3 6 新的 end()

此时循环结束,移动过来的 6 没有被检查,所以最终输出的是:

cpp 复制代码
1 5 3 6

对于标准 std::vector,erase 会使删除位置及其后面的迭代器失效 ,应该使用它返回的有效迭代器继续遍历 。因此,我们模拟实现的 erase 也可以返回删除位置 :删除后,下一个元素会移动到这里;如果删除的是最后一个元素,这里就是新的 end()。

代码:

cpp 复制代码
template<class T>
typename vector<T>::iterator vector<T>::erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);

	for (iterator it = pos + 1; it < _finish; it++)
	{
		*(it - 1) = *it;
	}
	_finish--;

	return pos;
}

然后我来解释一下这个typename的作用:

在这段代码中:

cpp 复制代码
template<class T>
typename vector<T>::iterator vector<T>::erase(iterator pos)

typename 的作用是告诉编译器:后面的 vector<T>::iterator 是一个类型名。

我们在类中定义过:

cpp 复制代码
typedef T* iterator;

所以知道 iterator 是类型 。但是 vector<T> 依赖模板参数 T,这种依赖模板参数的名字称为依赖名 。编译器解析模板时,不能仅凭 vector<T>::iterator 就确定它是类型,还是静态成员变量等其他成员。

因此,写上:

cpp 复制代码
typename vector<T>::iterator

就是明确告诉编译器:"把它当作类型来解析,它是这个函数的返回类型。"

那么insert其实也会犯同样的问题:如果 insert 触发扩容,it 仍然指向旧空间。虽然 insert 内部修正了 pos,但外面的 it 不会自动更新,所以后面的 *it 和 it++ 都会出问题, 所以我们也应该返回插入后的位置,代码:

cpp 复制代码
template<class T>
typename vector<T>::iterator vector<T>::insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);
	if (_finish == _end_of_storage)
	{
		//我们需要先找到旧空间的空间大小
		size_t len = pos - _start;
		size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newCapacity);
		pos = _start + len;
	}
	memmove(pos + 1, pos, sizeof(T) * (_finish - pos));

	*pos = x;
	_finish++;

	return pos;
}

9.resize接口

前面我们已经实现了 reserve 接口,可以调整 vector 的容量,那么如果我们想直接调整有效元素的个数,又该怎么办呢?这就需要引入 resize 接口了。

假设我们要将有效元素的个数调整为 n,那么就需要分情况考虑 :如果 n 小于当前的 size(),就将有效元素的个数缩减到 n;如果 n 大于当前的 size(),就需要补充新的元素,并用指定的值进行初始化。如果没有指定这个值,对于 int 类型,新增元素就初始化为 0。

当然,增加元素时,也要考虑空间是否足够。如果 n 大于当前的 capacity(),就需要先通过 reserve 扩容,再填充新增的元素,最后更新 _finish。所以,reserve 调整的是容量,而 resize 调整的是有效元素的个数;缩小 size 时,容量并不会随之缩小。

代码块:

cpp 复制代码
template<class T>
void vector<T>::resize(size_t n, const T& x)
{
	if (n > capacity())
	{
		reserve(n);
	}

	if (n > size())
	{
		for (size_t i = size(); i < n; i++)
		{
			push_back(x);
		}
	}
	else
	{
		_finish = _start + n;
	}
}

10.构造函数

我们之在使用vector的构造函数时,是可以用一个vector的迭代器区间构造的,那么这个的实现也是十分的简单的,如代码:

cpp 复制代码
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

之前我们实现的构造是vector的默认构造函数,那么此时我们就需要实现vector的拷贝构造 ,那么就需要用到现代的写法,那么就需要用到swap函数了,如代码所示:

cpp 复制代码
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}

拷贝构造:

cpp 复制代码
vector(const vector<T>& v)
{
	vector tmp(v.cbegin(), v.cend());
	swap(tmp);
}

还有个构造函数,如代码所示:

cpp 复制代码
vector(size_t n, const T& x = T())
{
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		push_back(x);
	}
}

测试代码:

cpp 复制代码
void test_vector3()
{
    kong::vector<int> v1(10,1);
    kong::vector<int> v2(v1);
    for(auto&x:ch)
    {
        cout<<x<<" ";
    }
}

运行结果:

这又整啥幺蛾子呢?

答案:

问题主要出在这一句:

cpp 复制代码
kong::vector<int> v1(10, 1);

你想调用的是"创建 10 个值为 1 的元素"的构造函数,但编译器优先匹配了迭代器构造函数。

看这两个构造函数:

cpp 复制代码
vector(size_t n, const T& x = T());

template<class InputIterator>
vector(InputIterator first, InputIterator last);

10 和 1 都是 int:

  • 第一个构造函数需要将 10 从 int 转成 size_t。
  • 第二个构造函数可以直接推导出 InputIterator = int,两个参数都精确匹配。

但是第二个匹配得更好,编译器会尝试实例化:

cpp 复制代码
vector(int first, int last)
{
	while (first != last)
	{
		push_back(*first); 
		first++;
	}
}

但是first 是 int,不能解引用。

你可以先这样修改调用,让第一个参数明确为 size_t:

cpp 复制代码
kong::vector<int> v1(10u, 1);

此时两个参数类型分别为 size_t 和 int,迭代器构造函数无法将它们推导为同一个 InputIterator,就会调用你需要的构造函数。

我们也可以再添加一个int的,如代码所示:

cpp 复制代码
vector(int n, const T& x = T())
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(x);
	}
}

11.赋值重载函数

首先呢,我们需要重载一下\[\],如代码:

cpp 复制代码
T& operator[](size_t pos)
{
	assert(pos < size());
	return _start[pos];
}
const T& operator[](size_t pos)const
{
	assert(pos < size());
	return _start[pos];
}

然后重载一下=,如代码所示:

cpp 复制代码
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

12.析构函数

cpp 复制代码
~vector()
{
	delete[] _start;
	_start = _finish = _end_of_storage = nullptr;
}

注意:当我们没有写析构函数时,我们还没有写拷贝构造和关于=的赋值重载,那么此时的拷贝构造和vector和vector间的赋值都是浅拷贝。

13.通用打印

前面我们在测试 vector 时,每次都要写一段遍历代码来打印里面的数据,那么能不能将这部分代码封装起来,写成一个通用的打印函数呢?自然就想到了函数模板,让 Container 表示传入的容器类型。只要容器支持这里使用的迭代器接口,我们就可以通过同一个函数来遍历并打印其中的元素。

由于打印只需要读取数据,不需要修改容器,所以参数使用 const Container&,既避免了拷贝,也限制了对容器的修改。同时,我们通过 cbegin() 和 cend() 获取遍历范围,使用 Container::const_iterator 来访问元素。这里的迭代器类型依赖模板参数 Container,因此在前面加上 typename,明确告诉编译器它是一个类型名。

代码块:

cpp 复制代码
template<class Container>
void print(const Container& con)
{
	typename Container::const_iterator it = con.cbegin();
	while (it != con.cend())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

14.浅拷贝问题

或许到了这里,有人觉得浅拷贝的问题都已经解决了,倘若我给你个这样的测试用例呢?

cpp 复制代码
void test_vector3()
{
	kong::vector<string> v;
	v.push_back("terhsehseheshdhdhhfg");
	v.push_back("terhsehseheshdhdhhfg");
	v.push_back("terhsehseheshdhdhhfg");
	v.push_back("terhsehseheshdhdhhfg");
	v.push_back("terhsehseheshdhdhhfg");
	v.push_back("terhsehseheshdhdhhfg");
	print(v);
}

运行结果:

这个打印结果是乱码而且还报错了吗、,那么这到底是什么问题呢?哪么这就要提到memcpy的浅拷贝问题了。

问题:

这里要分清两层空间:vector 开辟的空间存放的是 string 对象 ,而 string 对象自身还可能管理另一块存放字符的动态空间。

cpp 复制代码
T* tmp = new T[n];
memcpy(tmp, _start, sizeof(T) * old_size);

我们确实给 vector 开辟了新的空间,但是 memcpy 只是将旧 string 对象的字节原样复制到新对象中。如果旧对象内部保存着指向字符空间的指针,这个指针的地址也会被直接复制,而不会另外开辟字符空间。 这样,新旧两个 string 对象就会指向同一块字符空间,这就是这里所说的浅拷贝问题。

接下来执行:

cpp 复制代码
delete[] _start;

它不仅释放旧 vector 的空间,还会先调用其中每个 string 对象的析构函数 ,释放它们管理的字符空间 。但是,新对象中复制过来的指针仍然保存着原来的地址,此时这些指针就成了悬空指针。

所以,后续打印时可能访问已经释放的空间,出现乱码或报错;等新对象析构时,还可能再次释放同一块空间,造成重复释放。

示意图 :

所以我们应该将memcpy替换成如下的代码:

cpp 复制代码
for (size_t i = 0; i < old_size; i++)
{
    tmp[i] = _start[i];
}

这时执行的是 T 的赋值操作,如果是自定义类型就调用它的赋值重载函数,没有的话仍然是浅拷贝, 所以当 T 为 std::string 时,会调用它的拷贝赋值运算符,正确复制字符串内容,让新对象拥有独立的字符串数据。这样,销毁旧对象就不会影响新对象。

同理,memmove 也有同样的问题,它和 memcpy 都是直接复制字节 ,不会调用对象的拷贝构造或赋值运算符。两者的区别在于:memmove 支持源区间和目标区间重叠 ,但这并不能解决 string 内部资源的浅拷贝问题。

修改后的insert代码:

cpp 复制代码
template<class T>
typename vector<T>::iterator vector<T>::insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);
	if (_finish == _end_of_storage)
	{
		//我们需要先找到旧空间的空间大小
		size_t len = pos - _start;
		size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newCapacity);
		pos = _start + len;
	}

	for (iterator i = _finish; i >pos; i--)
	{
		*(i) = *(i-1);
	}

	*pos = x;
	_finish++;

	return pos;
}

总结:

本篇通过模拟实现 vector 的常用接口,理解了它如何利用三个指针管理连续空间,以及 size 与 capacity 的区别。通过插入、删除和扩容中的问题,我们认识了迭代器失效,并了解了如何通过返回值继续正确遍历。最后,通过 vector<string> 的测试,明白了容器开辟新空间并不意味着其中的对象完成了深拷贝,处理这类对象时,应使用类型自身的拷贝或赋值操作,而不能直接使用 memcpy、memmove 复制字节。

相关推荐
无名猿1 小时前
std::variant 完全指南:类型安全的 union 与 std::visit 用法
c++·stl·标准库·现代c++
乐迪信息1 小时前
AI防爆摄像机,监测港口船舶航行偏航隐患
大数据·人工智能·深度学习·算法·计算机视觉
变与不变8062 小时前
js同步和异步难点重点详解
开发语言·javascript·ecmascript
无名猿2 小时前
C++ 迭代器五分类与失效场景汇总:一张表避开所有 UB
c++·stl·标准库·踩坑记录
0+1112 小时前
算法 --模拟
c++·算法·leetcode
青山是哪个青山2 小时前
LeetCode 300:最长递增子序列
算法
hetao17338372 小时前
2026-10-01~02 hetao1733837 的刷题记录
c++·算法
小虎牙^O^2 小时前
模糊数学综合评价:从模糊数学到建模评价类模型
人工智能·算法
随意起个昵称3 小时前
【单调队列优化DP】U301134 绿色通道
算法·动态规划