【项目实现】 C++ vector实现(空间配置器+异常处理)


目录

前言

[Part1. 整体架构概述](#Part1. 整体架构概述)

[Part1.1 核心成员变量](#Part1.1 核心成员变量)

[Part1.2 类型别名体系](#Part1.2 类型别名体系)

[Part1.3 自定义分配器 allocator](#Part1.3 自定义分配器 allocator)

[Part2. 构造与析构函数](#Part2. 构造与析构函数)

[Part2.1. 默认构造](#Part2.1. 默认构造)

[Part2.2. 数目初始化构造](#Part2.2. 数目初始化构造)

[Part2.3. 迭代器区间构造](#Part2.3. 迭代器区间构造)

[Part2.4. 拷贝构造 & 移动构造](#Part2.4. 拷贝构造 & 移动构造)

[Part2.5. 赋值重载](#Part2.5. 赋值重载)

[Part2.6. 析构函数](#Part2.6. 析构函数)

[Part3. 核心容量操作接口](#Part3. 核心容量操作接口)

[Part3.1. reserve](#Part3.1. reserve)

[Part3.2. resize](#Part3.2. resize)

[Part3.3. shrink_to_fit](#Part3.3. shrink_to_fit)

[Part4. 元素增删改查核心接口](#Part4. 元素增删改查核心接口)

[Part4.1. 尾插 push_back](#Part4.1. 尾插 push_back)

[Part4.2. 原位构造 emplace_back](#Part4.2. 原位构造 emplace_back)

[Part4.3. 尾删 pop_back](#Part4.3. 尾删 pop_back)

[Part4.4. 任意位置 insert 插入](#Part4.4. 任意位置 insert 插入)

[Part4.5. 任意位置 erase 删除](#Part4.5. 任意位置 erase 删除)

[Part4.6. 访问接口](#Part4.6. 访问接口)

[Part5. 结语](#Part5. 结语)


前言

最近在复习C++,复习的方式就是实现STL,结合后面异常的知识,接下来来跟随小编的视角来看看吧。


let's go!!!!!!!!


Part1. 整体架构概述

Part1.1 核心成员变量

手写Vector完全对标STL原生实现,核心依靠三个指针管控内存:

  • _start:指向容器内存起始位置

  • _end:指向有效元素末尾的下一位,用于记录当前元素个数

  • _end_of_storage:指向总内存容量末尾的下一位,用于记录内存总大小

三者的关系直接决定了Vector的两大核心属性:

  • 有效元素个数:size() = _end - _start

  • 内存容量大小:capacity() = _end_of_storage - _start

除此之外,保留STL标准的分配器成员 _alloc,实现内存分配、元素构造/销毁的解耦,符合STL容器的通用设计规范

cpp 复制代码
T* _start;
T* _end;
T* _end_of_storage;
Alloc _alloc;

Part1.2 类型别名体系

源码中定义了一套完整的类型别名,是所有STL容器的通用写法,目的是屏蔽底层类型差异,统一接口规范,方便泛型编程适配:

  • value_type:容器存储的元素类型T

  • pointer/const_pointer:元素指针、常量指针

  • reference/const_reference:元素引用、常量引用

  • size_type/difference_type:无符号尺寸类型、指针差值类型

  • 迭代器:原生指针实现(Vector迭代器是随机访问迭代器)

  • 反向迭代器:基于标准库reverse_iterator封装

cpp 复制代码
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;

using iterator = T*;
using const_iterator = const T*;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;

Part1.3 自定义分配器 allocator

源码配套实现了极简版STL分配器,核心职责分为两层:内存管理和对象生命周期管理,彻底解耦「内存开辟释放」和「对象构造销毁」,这是STL容器的核心设计思想。

  • allocate:仅开辟原始内存,不构造对象,底层调用operator new

  • deallocate:仅释放原始内存,不析构对象,底层调用operator delete

  • construct:在已开辟的内存上,定位new构造对象,支持可变参数、完美转发

  • destroy:主动调用对象析构函数,销毁对象但不释放内存

  • max_size:返回容器最大可容纳元素数,规避内存溢出

  • rebind:模板重绑定,适配STL容器嵌套场景

cpp 复制代码
template<class T>
class allocator
{
	using value_type = T;
	using pointer = T*;
	using const_pointer = const T*;
	using reference = T&;
	using const_reference = const T&;
	using size_type = std::size_t;
	using difference_type = std::ptrdiff_t;

public:
	template<class U>
	struct rebind
	{
		using other = allocator<U>;
	};

	allocator() noexcept {};

	allocator(const allocator<value_type>& alloc) noexcept {};

	template<class U>
	allocator(const allocator<U>& alloc) noexcept {};

	pointer allocate(size_type n)
	{
		pointer p = static_cast<pointer>(::operator new(n * sizeof(value_type)));
		if (p == nullptr)
		{
			throw std::bad_alloc();//里面不能加字符串 不支持
		}
		return p;
	}

	void deallocate(pointer p,size_type) noexcept
	{
		::operator delete(p);
	}

	size_type max_size() const noexcept
	{
		return static_cast<size_type>(-1) / sizeof(value_type);
	}

	template<class U, class... Args>
	void construct(U* p, Args&&... args)
	{
		new(p) U(std::forward<Args>(args)...);
	}

	template<class U>
	void destroy(U* p) noexcept
	{
		p->~U();
	}

};

template<class T, class U>
bool operator==(const allocator<T>&, const allocator<U>&) noexcept
{
	return true;
}

template<class T, class U>
bool operator!=(const allocator<T>&, const allocator<U>&) noexcept
{
	return false;
}

Part2. 构造与析构函数

手写Vector完整实现了STL标准的所有构造、拷贝、移动构造、赋值重载,且全部实现强异常安全,杜绝内存泄漏。


Part2.1. 默认构造

初始化三个管控指针为空,接收自定义分配器,无内存开辟,零开销初始化。

cpp 复制代码
vector(const Alloc& alloc = Alloc())
	:_start(nullptr)
	, _end(nullptr)
	, _end_of_storage(nullptr)
	, _alloc(alloc)
{
	//std::cout << "调用了构造函数" << std::endl;
}

Part2.2. 数目初始化构造

核心逻辑:先校验最大容量,再开辟内存,循环构造元素。关键亮点:异常回滚机制

在元素循环构造过程中,若任意一次构造抛出异常,会捕获异常、销毁已构造的所有对象、释放已开辟的内存,杜绝内存泄漏后重新抛出异常,保证操作的原子性。

cpp 复制代码
vector(size_type n, const value_type& val = value_type(), const Alloc& alloc = Alloc())
	:_start(nullptr)
	, _end(nullptr)
	, _end_of_storage(nullptr)
	, _alloc(alloc)
{
	//std::cout << "调用了数目构造函数" << std::endl;
	if (n >= max_size())
	{
		throw std::length_error("length too big");
	}
	_start = _alloc.allocate(n);
	_end = _start + n;
	_end_of_storage = _start + n;
	size_type now = 0;
	try
	{
		for (now = 0; now < n; now++)
		{
			_alloc.construct(_start + now, val);//这里只能走拷贝构造 因为这是对于一个对象 也就是只有val本身 当我们使用移动构造之后 这个资源已经被移动走 从而导致后续无资源调用了
		}
	}
	catch (...)
	{
		for (size_type i = 0; i < now; i++)
		{
			_alloc.destroy(_start + i);
		}
		_alloc.deallocate(_start, n);
		throw;
	}
}

Part2.3. 迭代器区间构造

支持任意合法输入迭代器区间初始化,核心优化点:编译期判断移动语义安全性

通过C++17 if constexpr + is_nothrow_move_constructible_v,编译期判断元素是否有无异常移动构造:

  • 无异常移动:使用std::move移动构造,提升效率

  • 有异常风险:降级为拷贝构造,保证异常安全

同时配套完整的异常回滚逻辑,是高性能+高安全的实现。

cpp 复制代码
template<class InputIterator>
vector(InputIterator first, InputIterator last, const Alloc& alloc = Alloc(), typename std::iterator_traits<InputIterator>::iterator_category* = nullptr)
	:_start(nullptr)
	, _end(nullptr)
	, _end_of_storage(nullptr)
	, _alloc(alloc)
{
	//std::cout << "调用了迭代器区间构造函数" << std::endl;

	size_type n = static_cast<size_type>(std::distance(first, last));
	_start = _alloc.allocate(n);
	_end = _start + n;
	_end_of_storage = _end;
	InputIterator it = first;
	value_type* constructed = _start;
	try
	{
		while (it != last)
		{
			if constexpr (std::is_nothrow_move_constructible_v<T>)//用这个的目的是 强异常保证 就是防止我们在移动的中途突发异常 导致中断 此时怎么也救不会被移动走的资源 这个函数检查的是我们移动的这个对象他的移动构造是否被声明noexcept
			{
				_alloc.construct(constructed, std::move(*it));
			}
			else
			{
				_alloc.construct(constructed, *it);
			}
			constructed++;
			it++;
		}
	}
	catch (...)
	{
		iterator del_it = _start;
		while (del_it != constructed)
		{
			_alloc.destroy(del_it);
		}
		_alloc.deallocate(_start, n);
		throw;
	}
}

Part2.4. 拷贝构造 & 移动构造

  • 拷贝构造:深拷贝实现,独立开辟内存、逐元素拷贝构造,完全隔离新旧容器内存,杜绝浅拷贝问题,带异常回滚。

  • 移动构造:noexcept修饰,直接swap接管原容器的内存资源,零拷贝、零开销,仅转移指针控制权,是C++11移动语义的核心落地。

cpp 复制代码
vector(const vector<T>& other)
	:_start(nullptr)
	, _end(nullptr)
	, _end_of_storage(nullptr)
	, _alloc(other.get_allocator())
{
	//std::cout << "调用了拷贝构造函数" << std::endl;

	size_type n = other.size();
	_start = _alloc.allocate(n);
	_end = _start + n;
	_end_of_storage = _end;
	iterator it = other._start;
	value_type* cur = _start;
	try
	{
		while (it != other.end())
		{
			_alloc.construct(cur, *it);
			cur++;
			it++;
		}
	}
	catch (...)
	{
		iterator del_it = _start;
		while (del_it != cur)
		{
			_alloc.destroy(del_it);
		}
		_alloc.deallocate(_start, n);
		throw;
	}
}

vector(vector<T>&& other) noexcept
	:_start(nullptr)
	, _end(nullptr)
	, _end_of_storage(nullptr)
	, _alloc(std::move(other.get_allocator()))
{
	//std::cout << "调用了移动构造函数" << std::endl;

	swap(other);
}

Part2.5. 赋值重载

  • 拷贝赋值:采用「先构造临时对象,再swap交换」的经典写法,代码简洁且天然保证异常安全,无需手动处理旧内存释放。

  • 移动赋值:noexcept修饰,先判断自赋值,直接swap资源转移,高效安全。

cpp 复制代码
vector<T>& operator=(const vector<T>& other)
{
	//std::cout << "调用了赋值重载函数" << std::endl;

	vector<T> tmp = other;
	swap(tmp);
	//_alloc = other._alloc;  不一定要拷贝依照分配器的特性来看
	return *this;
}

vector<T>& operator=(vector<T>&& other) noexcept
{
	//std::cout << "调用了移动赋值函数" << std::endl;
	if (this == &other) return *this;
	swap(other);
	return *this;
}

Part2.6. 析构函数

严格遵循STL逻辑:先循环销毁所有有效元素(调用析构函数),再释放整块内存,杜绝内存泄漏。全程noexcept,保证析构绝对安全。

cpp 复制代码
~vector() noexcept
{
	//std::cout << "调用了析构函数" << std::endl;

	iterator it = begin();
	while (it != end())
	{
		_alloc.destroy(it);
		it++;
	}
	_alloc.deallocate(begin(), capacity());
}

Part3. 核心容量操作接口

Part3.1. reserve

核心作用:预分配内存,修改capacity,不修改size,避免频繁扩容拷贝,优化性能。

核心流程:

  1. 校验目标容量,小于当前容量直接返回

  2. 开辟新的大容量内存空间

  3. 编译期适配移动/拷贝构造,迁移旧元素

  4. 异常回滚:迁移失败则销毁新内存、杜绝泄漏

  5. 销毁旧内存元素、释放旧内存,更新管控指针

关键优化:优先使用无异常移动构造,大幅提升扩容效率。

cpp 复制代码
void reserve(size_type n)
{
	if (n <= capacity())
		return;
	if (n >= max_size())
	{
		throw std::length_error("length too big");
	}
	size_type low_size = size();
	pointer new_ptr = _alloc.allocate(n);//这里不需要捕获 有异常会自动跳过下面的代码 直接到下面的catch 
	pointer constructed = new_ptr;
	try
	{
		iterator it = begin();
		while (it != end())
		{
			if constexpr (std::is_nothrow_move_constructible_v<T>)//if constexpr C++17 用于在编译期间判断 不满足的代码会不生成
			{
				_alloc.construct(constructed, std::move(*it));
			}
			else
			{
				_alloc.construct(constructed, *it);
			}
			constructed++;
			it++;
		}
	}
	catch (...)
	{
		pointer ptr = new_ptr;
		while (ptr != constructed)
		{
			_alloc.destroy(ptr);
			ptr++;
		}
		_alloc.deallocate(new_ptr, n);
		throw;
	}
	iterator it = begin();
	while (it != end())
	{
		_alloc.destroy(it);
		it++;
	}
	_alloc.deallocate(_start, capacity());
	_start = new_ptr;
	_end = _start + low_size;
	_end_of_storage = _start + n;
}

Part3.2. resize

同时修改size,根据新旧大小分为两种场景:

  • n > 当前size:扩容内存,并用默认值val填充新增位置的元素,带异常回滚

  • n < 当前size:直接截断尾部元素,销毁多余对象,不释放内存(capacity不变)

cpp 复制代码
void resize(size_type n, const value_type& val = value_type())
{
	if (n > max_size())
	{
		throw std::length_error("length too big");
	}
	if (n == size())
		return;
	else if (n > size())
	{
		reserve(n);
		iterator it = end();
		try
		{
			while (it != _start + n)
			{
				_alloc.construct(it, val);
				it++;
			}
		}
		catch (...)
		{
			iterator del_it = end();
			while (del_it != it)
			{
				_alloc.destroy(del_it); 
				del_it++;
			}
			throw;
		}
			_end = _start + n;
	}
	else
	{
		iterator final = end();
		_end = _start + n;
		iterator it = end();
		while (it != final)
		{
			_alloc.destroy(it);
			it++;
		}
	}
}

Part3.3. shrink_to_fit

将容器容量收缩至当前有效元素大小,释放多余空闲内存。逻辑与扩容相反:开辟精准大小的新内存、迁移元素、释放旧大容量内存,实现内存极致复用。

cpp 复制代码
void shrink_to_fit()
{
	size_type low_size = size();
	pointer new_ptr = _alloc.allocate(low_size);
	pointer constructed = new_ptr;
	try
	{
		iterator it = begin();
		while (it != end())
		{
			if constexpr (std::is_nothrow_move_constructible_v<T>)
			{
				_alloc.construct(constructed, std::move(*it));
			}
			else
			{
				_alloc.construct(constructed, *it);
			}
			constructed++;
			it++;
		}
	}
	catch (...)
	{
		pointer ptr = new_ptr;
		while (ptr != constructed)
		{
			_alloc.destroy(ptr);
			ptr++;
		}
		_alloc.deallocate(new_ptr, low_size);
		throw;
	}
	iterator it = begin();
	while (it != end())
	{
		_alloc.destroy(it);
	}
	_alloc.deallocate(_start, capacity());
	_start = new_ptr;
	_end = _start + low_size;
	_end_of_storage = _start + low_size;
}

Part4. 元素增删改查核心接口

Part4.1. 尾插 push_back

完美适配左值拷贝插入、右值移动插入:

  • 容量不足时触发扩容:空容器默认初始10容量,非空容器2倍扩容(STL经典扩容策略)

  • 左值:拷贝构造元素,保证原数据有效

  • 右值:移动构造元素,高效复用临时资源

cpp 复制代码
void push_back(const T& val)
{
	if (size() + 1 > capacity())
	{
		size() == 0 ? reserve(FIRST_SIZE) : reserve(size() * 2);
	}
	_alloc.construct(_end, val);	
	_end++;
}

void push_back(T&& val)
{
	if (size() + 1 > capacity())
	{
		size() == 0 ? reserve(FIRST_SIZE) : reserve(size() * 2);
	}
	_alloc.construct(_end, std::move(val));
	_end++;
}

Part4.2. 原位构造 emplace_back

C++11核心优化接口,支持参数完美转发,直接在容器内存中原位构造对象,省去「构造临时对象+拷贝/移动」的开销,是效率最高的尾插方式。

cpp 复制代码
template<class... Args>
reference emplace_back(Args&&... args)
{
	if (size() + 1 > capacity())
	{
		size() == 0 ? reserve(FIRST_SIZE) : reserve(size() * 2);
	}
	_alloc.construct(_end, std::forward<Args>(args)...);
	_end++;
	return *(_end - 1);
}

Part4.3. 尾删 pop_back

noexcept无异常操作:仅销毁尾部元素、前移_end指针,不释放内存,保留容量,方便后续复用。

cpp 复制代码
void pop_back() noexcept
{
	_alloc.destroy(_end - 1);
	_end--;
}

Part4.4. 任意位置 insert 插入

实现了单元素左值/右值插入、批量插入三种重载,核心难点:

  1. 插入前校验扩容,重点保存偏移量:扩容会导致原迭代器失效,通过偏移量重新定位插入位置,解决迭代器失效问题

  2. 从尾部开始整体后移元素,适配移动/拷贝赋值

  3. 完成元素插入,更新有效元素个数

同时适配批量插入场景,支持一次性插入n个元素,逻辑严谨兼容。

cpp 复制代码
iterator insert(iterator pos, const T& val)
{
	if (size() + 1 > capacity())
	{
		size_type offset = pos - begin();
		size() == 0 ? reserve(FIRST_SIZE) : reserve(size() * 2);
		pos = begin() + offset;//扩容后更新
	}
	iterator it = _end;
	try
	{
		while (it != pos)
		{
			if constexpr (std::is_nothrow_move_constructible_v<T>)
			{
				_alloc.construct(it, std::move(*(it - 1)));
			}
			else
			{
				_alloc.construct(it, *(it - 1));
			}
			_alloc.destroy(it - 1);
			it--;
		}
	}
	catch (...)
	{
		throw;//无法做到强异常安全
	}
	_alloc.construct(pos, val);
	_end++;
	return pos;
}

iterator insert(iterator pos, T&& val)
{
	if (size() + 1 > capacity())
	{
		size_type offset = pos - begin();
		size() == 0 ? reserve(FIRST_SIZE) : reserve(size() * 2);
		pos = begin() + offset;//扩容后更新
	}
	iterator it = _end;
	try
	{
		while (it != pos)
		{
			if constexpr (std::is_nothrow_move_constructible_v<T>)
			{
				_alloc.construct(it, std::move(*(it - 1)));
			}
			else
			{
				_alloc.construct(it, *(it - 1));
			}
			_alloc.destroy(it - 1);
			it--;
		}
	}
	catch (...)
	{
		throw;//无法做到强异常安全
	}
	try
	{
		_alloc.construct(pos, std::move(val));
	}
	catch (...)
	{
		throw;//无法做到强异常安全
	}
	_end++;
	return pos;
}

iterator insert(iterator pos, size_type n, const T& val)//这个没有右值版本 因为这个要构造多个对象 在构造第一个的时候直接就是没有资源了
{
	if (n > max_size())
	{
		throw std::length_error("length too big");
	}
	if (size() + n > capacity())
	{
		size_type offset = pos - begin();
		size() == 0 ? reserve(FIRST_SIZE + n) : reserve(size() * 2 + n);
		pos = begin() + offset;
	}
	iterator it = _end + n - 1;
	try
	{
		while (it != pos + n - 1)
		{
			if constexpr (std::is_nothrow_move_constructible_v<T>)
			{
				_alloc.construct(it, std::move(*(it - n)));
			}
			else
			{
				_alloc.construct(it, *(it - n));
			}
			_alloc.destroy(it - n);
			it--;
		}
	}
	catch (...)
	{
		throw;//无法做到强异常安全
	}
	try
	{
		size_type now = 0;
		for (size_type now = 0; now < n; now++)
		{
			_alloc.construct(pos + now, val);
		}
	}
	catch (...)
	{
		throw;
	}
	_end += n;
	return pos;
}

Part4.5. 任意位置 erase 删除

实现单元素删除、区间删除,核心逻辑:

  1. 用后序元素覆盖待删除元素(移动赋值/拷贝赋值)

  2. 销毁尾部多余元素

  3. 更新有效元素个数,返回当前有效迭代器(缓解迭代器失效)

通过is_nothrow_move_assignable_v判断移动赋值安全性,兼顾性能与异常安全。

cpp 复制代码
iterator erase(iterator pos)
{
	iterator it = pos;
	try
	{
		while (it != end()-1)
		{
			if constexpr (std::is_nothrow_move_assignable_v<T>)
			{
				*it=std::move(*(it + 1));//全是在已经初始化的内存上实现的
			}
			else
			{
				*it = *(it + 1);
			}
			it++;
		}
	}
	catch (...)
	{
		throw;//无法做到强异常安全
	}
	_alloc.destroy(it);
	_end--;
	return pos;
}

iterator erase(iterator first, iterator last)
{
	iterator it = first;
	size_type n = last - first;
	try
	{
		while (it != end()-n)
		{
			if constexpr (std::is_nothrow_move_assignable_v<T>)//判断移动赋值是否会出异常
			{
				*it = std::move(*(it + n));
			}
			else
			{
				*it = *(it + n);
			}
			it++;
		}
	}
	catch (...)
	{
		throw;//无法做到强异常安全
	}
	while (it != end())
	{
		_alloc.destroy(it);
		it++;
	}
	_end -= n;
	return first;
}

Part4.6. 访问接口

  • []:无越界检查,高效访问,适合确定下标合法的场景

  • at():带越界校验,越界抛出out_of_range异常,安全优先

  • front/back:快速访问首尾元素

cpp 复制代码
reference operator[](size_type n) noexcept
{
	return *(_start + n);
}

const_reference operator[](size_type n) const noexcept
{
	return *(_start + n);
}

reference at(size_type n)
{
	if (n >= size())
		throw std::out_of_range("vector at out of range");
	return *(_start + n);
}

const_reference at(size_type n) const
{
	if (n >= size())
		throw std::out_of_range("vector at out of range");
	return *(_start + n);
}

reference front() noexcept
{
	return *_start;
}

const_reference front() const noexcept
{
	return *_start;
}

reference back() noexcept
{
	return *(_end - 1);
}

const_reference back() const noexcept
{
	return *(_end - 1);
}

Part5. 结语

这篇文章我们认识并知道了vector的实现,接下来,小编还会带来更多知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

相关推荐
qq_199886871 小时前
第8板块·第3节:设备间拷贝与点对点(P2P)传输
c++·人工智能·gpu算力·cuda
爱奥尼欧1 小时前
【C++】map、set 的底层是什么?红黑树五条性质、插入旋转到模拟实现
开发语言·数据结构·c++
汉克老师2 小时前
CSP-J 2026 初赛试题解析(第三部分:完善程序题(第二题))精讲
c++·csp-j·小学生·学c++编程
kukubuzai2 小时前
双指针系列二--末尾篇(3道题)
c++·算法·leetcode
汉克老师2 小时前
CSP-J 2026 初赛试题解析(第二部分:阅读程序题(第二题))精讲
c++·csp-j·小学生·学c++编程
神仙别闹2 小时前
基于 C++ 实现(控制台)关键字检索系统
c++
重生之小比特2 小时前
【初阶C++】stack和queue
开发语言·c++·vs·queue
qq_199886872 小时前
第8板块·第1节:主机与设备内存管理基础与统一内存
c++·人工智能·gpu算力·cuda
h_a_o777oah3 小时前
【图论】网络流:Dinic 算法模板实现原理及解题技巧
c++·算法·图论·acm·网络流·最小割·dinic算法