从零模拟实现 vector -- 理解 C++ 最核心的顺序容器

std::vector 是 C++ 标准库中使用频率最高的容器之一。它用连续内存存储元素,支持 O(1) 随机访问,并且在尾部插入均摊 O(1)------这些特性让它在多数场景下成为默认选择。

然而,会用和真正理解之间隔着一层"自己实现一遍"的薄纱。本文记录了我模拟实现 vector 的过程,涵盖动态扩容、迭代器管理、拷贝控制、插入与删除等核心机制。文章面向已经使用过 STL 容器、希望通过源码级复现来加深理解的 C++ 学习者。

全文结构如下:先回顾 vector 的内存模型与三指针架构,然后依次展开构造函数、容量管理、元素访问、修改操作与迭代器,最后讨论当前实现的不足之处与改进方向。

模拟实现前的工作

由于vector是一个模板类, 因此我们模拟实现的时候不能分装到两个我文件中去 , 只能都放在头文件中, 不然会报编译错误.

之前你可能会以为vector就是普普通通的顺序表, 现在我们去vector的源码里面看看, 那行大佬是怎么定义的.

我们通过源代码可以看到, 这三个成员变量的,是三个迭代器, 这三个迭代器又是由typedef定义的指针,他们三个分别分别指向了不同的位置:

start: 指向空间开始位置

finish: 指向内容结束位置

end_of_storage: 指向空间结束位置

即如图所示.

通过这三个成员变量就可以知道一个vector类的内容大小和空间大小 .

内容大小:finish-start

空间大小: end_of_storage

我们了解了vector底层的成员变量后就可以这样定义一个自己的vector.

注意这里的vector为了与std标准库中的vector做区分, 要写在自己定义的一个命名空间中.

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

using namespace std;

namespace zy
{
	template<class T>
	class vector
	{
	public:
		....//函数
	private:
		_start = nullptr;
		_finish = nullptr;
		_end_of_storage = nullptr;
	}
}

模拟实现

size()和capacity()

通过前面对源码的分析, 我们知道, vector储存的是三个迭代器指针, 通过迭代器指针的计算就可以的到他们的sizecapacity .

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

从这里也可以开出来vector的区间是前闭后开的.通过相减即可得到正确的大小.

vector的访问

使用 随机访问O(1)

我们知道vector是可以通过下标随机访问且可以一步到位的方位到这个元素

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

const T& operator[](size_t i) const
{
	assert(i < size());
	return _start[i];
}

加上断言更严谨.

迭代器区间访问

大部分的容器都是可以使用迭代器进行访问的, vector也不例外.

要想实现迭代器访问我们要定义一个迭代器:

typedef T* iterator;

typedef const T* const_iterator;

而且前面的成员变量也是使用这个定义的迭代器所定义的.

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

那么这样直接返回对应的起始位置和就结束位置, 就得到了所对应的迭代器.

有了迭代器就可以使用范围for进行访问

cpp 复制代码
for(auto c : v)
{
	cou<<c<<' '
}
cout<<endl;

能访问vector的内容后我们就可以写一个打印vector内容的函数方便我们进行打印观察.

cpp 复制代码
template<class T>
void print_vector(const vector<T>& v)
{
	for (auto c : v)
	{
		cout << c << ' ';
	}
	cout << endl;
}

这样写可以是没有问题的, 但是这里的vector< T >就把打印的容器写死了, 只能打印, 多以我们可以改造一下, 将整个传入的参数定义为模板类型参数这样我们就可以打印任意容器了.

cpp 复制代码
template<class  Container>
void print_container(const Container & v)
{
	for (auto c : v)
	{
		cout << c << ' ';
	}
	cout << endl;
}

当调用print_container(v1)时, Container就是vector< int >.

reserve 和 resize

reserve

reserve的功能就是调正vector的空间大小.

通过C++官方文档和使用我们知道, 当重新调正的空间大小小于目前容器的大小时, vector的空间是不改变的.

cpp 复制代码
void reserve(size_t n)
{
	if(n>size())
	{
		size_t old_size = size();
		size_t old_capacity=capacity();
		T* temp = new T[n];
		//memcpy(temp,_start);使用memcpy只是浅拷贝
		for(size_t i =0 ; i<size(); i++)
		{
			temp[i]=_start[i];
		}
		delete [] _start;
		_start=temp;
		_finish=temp+old_size;
		_end_of_storage = temp+old_capacity;
	}
}

这里reserve的实现需要注意的是不能随意使用memcpy() .

因为memcpy()是一个字节一个字节的拷贝的, 也就是浅拷贝, 对于内置类型来说是没问题的即便捷又高效, 但当对自定义类型拷贝时, 且自定义类型涉及到资源管理, memcpy就会出错.

以vector< string >为例子

vector< string> v1;

v1.push_back("111111");

v1.push_back("111111");

v1.push_back("111111");

v1.push_back("111111");

v1.push_back("111111");

我们通尾插入数据造成扩容这个时候就相当于


如果我们使用赋值的操作就会调拷贝构造是得temp的元素所指向的空间是新开申请的独立空间.

这时再释放就空间就没有任何影响了.

resize

resize的作用的是调整容器大小, 如果参数小于容器的size, 则内容就会缩减到前n个元素, 如果大于容器的size, 则会在末尾填充val以达到n个大小. 如果大于capacity容器大小,则会重新分配新的空间.

cpp 复制代码
void resize(size_t n, const T& value = T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);
		for (int i = size(); i < n; i++)
		{
			push_back(value);
		} 
	}
}

这里会出现一个新的概念value=T() 这样写是为了让vector有更好适应性, 也是一种默认参数,这也泛型代码的特点.

对于自定义类型我们也知道value=T()会调用他们拷贝构造那么内置类型呢?

其实对于内置类型也有相对应的拷贝构造, 不过通常是被编译器优化了.

这里我们模拟实现的逻辑是分成:

  1. n < size

缩减内容至前n个

2.n > size

先调用reserve函数, (因为只有n>capacity时才会发生扩容)再对容器进行尾插到n的位置.

push_back和pop_back

尾插和尾删和string类的模拟实现差不多.

cpp 复制代码
void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		size_t n = capacity();
		reserve(n == 0 ? 4 : 2 * n);
	}
	*_finish = x; 
	_finish++;
}

void pop_back()
{
	_finish--;
}

insert 和 erase

分别是对容进行插入和删除, 不过这里的传递的参数不再是整型的下标而是迭代器指针.

cpp 复制代码
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _end_of_storage);
			size_t p = pos - _start;
			if (_finish == _end_of_storage)
			{
				size_t n = capacity();
				reserve(n == 0 ? 4 : 2 * n);
			}
			pos = _start + p;	
			auto it = end();
			while (it != pos)
			{
				*it = *(it - 1);
				it--;
			}
			*pos = x;
			_finish++;
			return pos;
			
		}

上面有一处很奇怪的地方就是对pos的处理, 这里我们将这两行代码放在一起看看

size_t p = pos - _start;

pos = _start + p;

这对pos的操作看上去有些多此一举, 其实这是为了避免扩容导致的迭代器失效, 下面画一个图跟大家展示一下, 如果没有这两行代码会发生什么.

并且这里即使执行了这两行代码, 形参的改变不影响实参, 所以这里就需要重新返回更新一下pos迭代器.

这就是迭代器的失效, 不过虽然执行和返回pos, 但是pos位置的意义也发生改变了.

cpp 复制代码
iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	auto it = pos + 1;
	while (it != end())
	{
		*(it - 1) = *it;
		it++;
	}
	_finish--;
	return pos;
}

这里的erase函数开始减小内容, 不会发生扩容但是迭代器依然会失效.

下面看一个这个例子, 删除所有的偶数.

cpp 复制代码
void test_vector2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(5);

	for (auto i = v.begin(); i != v.end(); i++)
	{
		if (*i % 2 == 0)
		{
			v.erase(i);
		}
	}
}

看着这段代码没有什么毛病, 先遍历,再判断最后删除. 但当你打印出来会发现出来的是

竟然会有一个4没有删掉,我们画个图来观察一下

这里可以看到在第三次循环时i删除后有进行了++就跳过了后面的偶数, 这就没有完全遍历容器,就导致没有完全将偶数删除 这就是因为迭代器失效导致的, 所以我们使用这些的函数的时候要注意迭代器失效的问题 .

那么解决方法也很简单就是在erase后再让i--一下就可以了.

swap

这里构造swap可以避免使用算法库中的swap造成深拷贝, 提高效率

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

这里不需要构造临时对象,直接交换核心内容.

构造函数

cpp 复制代码
vector()//默认构造
{ }

vector(int n, const T& value = T())//构造n个val的vector
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(value);
	}

}

template<class inputiterator>//利用迭代器区间构造vector
vector(inputiterator first, inputiterator last)
{
	
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

vector(const vector<T>& v)//拷贝构造
{
	reserve(v.capacity());
	for (auto c : v)
	{
		push_back(c);
	}

这里有一个特别的点,这里的参数类型必须是int 而不能是size_t.

所以这里函数的参数类型必须是 int 而不能是 size_t,这样编译器按照迭代器区间的构造方式来构造,就会发生报错

析构函数

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

我们通过观察底层知道, vector储存的也就是指针并且指向的同一块空间, 所以析构的时候只需要对这块空间进行释放

赋值重载

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

这里的赋值重载可以使用现代写法, 直接拷贝传参, 然后对形参进行交换, 出函数作用域后就会自动调用析构函数, 销毁this以前的值

结语

至此我们完成一个支持动态扩容, 随机访问, 深拷贝的基本vector. 当前实现尚未支持移动语义与 placement new,对于非平凡类型的元素管理还不够严谨。感谢大家观看, 欢迎大家讨论!!!