教程:XEngine::utl::vector 类详解
这份教程将深入探讨 XEngine::utl
命名空间下的 vector
类模板的实现细节和使用方法。这个自定义的 vector
类旨在提供一个轻量级、可定制的动态数组容器,尤其关注性能和对内存管理的控制。
1. 概述
XEngine::utl::vector
是一个模板类,它模拟了标准库中的 std::vector
,提供动态数组的功能。这意味着它可以根据需要自动调整大小。与 std::vector
相比,这个自定义版本可能更注重某些特定的性能考量或者提供了更底层的内存管理选项。
模板参数:
typename T
: 指定vector
中存储的元素类型。可以是任何 C++ 数据类型,包括内置类型和自定义类型。bool destruct = true
: 一个布尔值模板参数,控制在容器销毁或元素移除时是否调用元素的析构函数。默认为true
,表示会执行析构。如果设置为false
,则不会调用析构函数,这在某些性能敏感的场景下可能有用,但需要谨慎使用,以避免资源泄漏。
2. 构造函数
vector
类提供了多种构造函数,以支持不同的初始化方式:
默认构造函数
vector() = default;
:这是一个默认构造函数,使用
= default
显式声明,表示使用编译器提供的默认实现。特点: 不会分配任何内存。初始状态下,
vector
是空的,capacity
和size
均为 0,_data
指针为nullptr
。用法:
C++
XEngine::utl::vector<int> vec1; // 创建一个空的 vector
计数构造函数
constexpr explicit vector(u64 count)
:使用
explicit
关键字,防止隐式类型转换。constexpr
表明此构造函数在编译时也可能执行。参数:
count
(u64
类型) - 指定vector
的初始大小。功能: 调用
resize(count)
来分配能容纳count
个元素的内存,并使用默认构造函数初始化这些元素。用法:
C++
XEngine::utl::vector<int> vec2(10); // 创建一个包含 10 个默认初始化 int 元素的 vector
计数和值构造函数
constexpr explicit vector(u64 count, const T& value)
:同样使用
explicit
和constexpr
。参数:
count
(u64
类型) - 指定vector
的初始大小。value
(const T&
类型) - 用于初始化所有元素的常量引用值。
功能: 调用
resize(count, value)
分配内存并使用value
的拷贝初始化所有元素。用法:
C++
XEngine::utl::vector<float> vec3(5, 3.14f); // 创建一个包含 5 个 float 元素,均初始化为 3.14 的 vector
拷贝构造函数
constexpr vector(const vector& o)
:参数:
o
(const vector&
类型) - 要拷贝的vector
对象的常量引用。功能: 执行深拷贝。它会分配新的内存,并将源
vector
o
中的所有元素拷贝到新的vector
中。用法:
C++
XEngine::utl::vector<int> vec4 = vec2; // 使用拷贝构造函数,vec4 是 vec2 的一个副本
移动构造函数
constexpr vector(const vector&& o)
:参数:
o
(const vector&&
类型) - 要移动的vector
对象的右值引用。功能: 执行浅拷贝(移动语义)。它会将源
vector
o
的内部指针(_capacity
,_size
,_data
)移动到新的vector
,而不会复制元素。之后,源vector
o
会被置于有效但未指定的状态 (通过o.reset()
实现)。这避免了深拷贝的开销,提高了性能,尤其是在处理大型vector
时。用法:
C++
XEngine::utl::vector<int> vec5 = std::move(vec2); // 使用移动构造函数,vec5 接管 vec2 的资源,vec2 变为 empty
迭代器范围构造函数
template<typename it, typename = std::enable_if_t<std::_Is_iterator_v<it>>> constexpr explicit vector(it first, it last)
:模板参数:
it
- 迭代器类型。约束: 使用
std::enable_if_t
和std::_Is_iterator_v
确保it
必须是一个迭代器类型。参数:
first
(迭代器) - 范围的起始迭代器。last
(迭代器) - 范围的结束迭代器(不包含)。
功能: 通过迭代器范围
[first, last)
初始化vector
。它会遍历迭代器范围内的元素,并使用emplace_back
将每个元素添加到vector
中。用法:
C++
int arr[] = {1, 2, 3, 4, 5}; XEngine::utl::vector<int> vec6(std::begin(arr), std::end(arr)); // 从数组 arr 初始化 vector
3. 析构函数和资源管理
析构函数
~vector() { destroy(); }
:负责在
vector
对象生命周期结束时释放其占用的资源。它调用
destroy()
成员函数来完成实际的清理工作。
destroy()
成员函数 (private):功能:
断言检查:
assert([&] {return _capacity ? _data != nullptr : _data == nullptr; }());
这是一个复杂的断言,用于检查内部状态的一致性。它确保如果_capacity
大于 0 (即分配了内存),则_data
指针不能为nullptr
;反之,如果_capacity
为 0,则_data
必须为nullptr
。这有助于在开发阶段尽早发现内存管理错误。清理元素: 调用
clear()
函数来析构vector
中所有已构造的元素(如果destruct
模板参数为true
)。释放内存:
_capacity = 0;
和_size = 0;
重置容量和大小。if (_data) free(_data);
如果_data
指针不为空 (即分配了内存),则使用free()
函数释放之前通过realloc()
或其他方式分配的内存。_data = nullptr;
将_data
指针置为nullptr
,防止悬挂指针。
reset()
成员函数 (private):功能: 将
vector
重置为空状态,但不释放已分配的内存。主要用于移动操作后清理源对象。操作: 将
_capacity
和_size
设置为 0,并将_data
指针设置为nullptr
。
destruct_range(u64 first, u64 last)
成员函数 (private):功能: 析构
vector
中指定范围[first, last)
内的元素。条件编译:
if constexpr (destruct)
只有当vector
模板参数destruct
为true
时,才会执行元素的析构。断言:
assert(destruct); assert(first <= _size && last <= _size && first <= last);
确保destruct
为true
,且范围索引有效。析构循环: 如果
_data
指针有效,则循环遍历索引从first
到last
的元素,并对每个元素调用析构函数_data[first].~T();
。
4. 赋值运算符
拷贝赋值运算符
constexpr vector& operator=(const vector& o)
:参数:
o
(const vector&
类型) - 要赋值的源vector
对象的常量引用。功能: 实现深拷贝赋值。
自赋值检查:
assert(this != std::addressof(o)); if (this != std::addressof(o))
防止自赋值,如果尝试vec = vec;
,则直接返回*this
。清理现有资源:
clear();
首先清理当前vector
对象已有的元素 (并根据destruct
模板参数决定是否析构)。预留空间:
reserve(o._size);
为新的元素预留足够的容量,大小与源vector
o
相同,以减少后续emplace_back
操作中的内存重新分配。元素拷贝:
for (auto& item : o) { emplace_back(item); }
遍历源vector
o
的每个元素,并使用emplace_back
将其拷贝到当前vector
中。emplace_back(item)
会调用类型T
的拷贝构造函数。大小断言:
assert(_size == o._size);
赋值完成后,断言当前vector
的大小与源vector
的大小相同,确保拷贝正确完成。
返回值:
return *this;
返回当前vector
对象的引用,支持链式赋值操作 (例如vec1 = vec2 = vec3;
)。
移动赋值运算符
constexpr vector& operator=(vector&& o)
:参数:
o
(vector&&
类型) - 要赋值的源vector
对象的右值引用。功能: 实现移动赋值。
自赋值检查:
assert(this != std::addressof(o)); if (this != std::addressof(o))
防止自赋值。销毁现有资源:
destroy();
销毁当前vector
对象已有的资源 (包括已分配的内存和元素)。资源移动:
move(o);
调用私有成员函数move(o)
将源vector
o
的内部指针 (_capacity
,_size
,_data
) 移动到当前vector
对象。
返回值:
return *this;
返回当前vector
对象的引用,支持链式赋值。
5. 容量操作
reserve(u64 new_capacity)
:参数:
new_capacity
(u64
类型) - 新的容量大小。功能: 预留至少能容纳
new_capacity
个元素的内存空间。容量检查:
if (new_capacity > _capacity)
只有当new_capacity
大于当前容量_capacity
时,才需要重新分配内存。内存重分配:
void* new_buffer{ realloc(_data, new_capacity * sizeof(T)) };
使用realloc()
函数尝试重新分配内存。realloc()
的优点是,如果可能,它会在原有内存块的基础上扩展内存,如果无法扩展,则会分配新的内存块,并将原有数据拷贝到新的内存块中。断言检查:
assert(new_buffer);
断言内存分配成功。如果realloc()
失败返回nullptr
,则程序会终止。更新内部状态:
if (new_buffer)
如果内存分配成功 (new_buffer
不为nullptr
):_data = static_cast<T*>(new_buffer);
更新_data
指针指向新的内存块。_capacity = new_capacity;
更新_capacity
为新的容量值。
resize(u64 new_size)
:参数:
new_size
(u64
类型) - 新的大小。功能: 改变
vector
的大小为new_size
。静态断言:
static_assert(std::is_default_constructible_v<T>, "Type must be default-constructible.");
要求元素类型T
必须是默认可构造的 (即有默认构造函数)。因为如果new_size
大于当前_size
,需要创建新的元素。扩容:
if (new_size > _size)
reserve(new_size);
首先预留至少new_size
的容量。while (_size < new_size) { emplace_back(); }
循环调用emplace_back()
添加新的元素,直到_size
达到new_size
。由于emplace_back()
内部会使用默认构造函数T()
创建新元素,因此要求T
必须是默认可构造的。
缩容:
else if (new_size < _size)
if constexpr (destruct) { destruct_range(new_size, _size); }
如果new_size
小于当前_size
,则需要移除多余的元素。如果destruct
模板参数为true
,则调用destruct_range()
析构索引从new_size
到_size
范围内的元素。
大小断言:
assert(new_size == _size);
操作完成后,断言vector
的大小确实变为new_size
。
resize(u64 new_size, const T& value)
:参数:
new_size
(u64
类型) - 新的大小。value
(const T&
类型) - 用于新元素的拷贝值。
功能: 改变
vector
的大小为new_size
,并可以指定新元素的初始值。静态断言:
static_assert(std::is_copy_constructible_v<T>, "Type must be copy-constructible.");
要求元素类型T
必须是拷贝可构造的 (即有拷贝构造函数),因为新元素需要用value
的拷贝来初始化。扩容:
if (new_size > _size)
reserve(new_size);
预留容量。while (_size < new_size) { emplace_back(value); }
循环调用emplace_back(value)
添加新元素,并使用value
的拷贝进行初始化。
缩容:
else if (new_size < _size)
if constexpr (destruct) { destruct_range(new_size, _size); }
如果new_size
小于当前_size
,则移除多余的元素,并根据destruct
模板参数决定是否析构。
大小断言:
assert(new_size == _size);
操作完成后,断言大小为new_size
。
capacity() const
:返回值:
u64
- 返回当前vector
的容量,即已分配的内存空间可以容纳的元素数量。
size() const
:返回值:
u64
- 返回当前vector
中实际存储的元素数量。
empty() const
:返回值:
bool
- 如果vector
为空 (size
为 0),则返回true
,否则返回false
。
6. 元素访问
operator[](u64 index)
(非const
和const
版本):参数:
index
(u64
类型) - 要访问的元素的索引。功能: 提供对
vector
中元素的直接访问,类似于数组访问。断言:
assert(_data && index < _size);
在访问前进行断言检查,确保_data
指针有效且索引index
在有效范围内[0, _size)
。如果断言失败,程序会终止,这有助于调试越界访问错误。返回值:
非
const
版本:T&
- 返回索引index
处元素的引用 (左值引用),允许修改元素的值。const
版本:const T&
- 返回索引index
处元素的常量引用 (常量左值引用),只允许读取元素的值,不能修改。
用法:
C++
XEngine::utl::vector<int> vec = {10, 20, 30}; vec[0] = 15; // 使用非 const operator[] 修改元素 int val = vec[1]; // 使用 const operator[] 读取元素
front()
(非const
和const
版本):功能: 访问
vector
的第一个元素。断言:
assert(_data && _size);
断言_data
指针有效且vector
不为空 (_size > 0
)。返回值:
非
const
版本:T&
- 返回第一个元素的引用 (左值引用)。const
版本:const T&
- 返回第一个元素的常量引用 (常量左值引用)。
用法:
C++
int first_element = vec.front(); // 获取第一个元素
back()
(非const
和const
版本):功能: 访问
vector
的最后一个元素。断言:
assert(_data && _size);
断言_data
指针有效且vector
不为空。返回值:
非
const
版本:T&
- 返回最后一个元素的引用 (左值引用)。const
版本:const T&
- 返回最后一个元素的常量引用 (常量左值引用)。
用法:
C++
int last_element = vec.back(); // 获取最后一个元素
data()
(非const
和const
版本):功能: 获取指向
vector
内部原始数据数组的指针。返回值:
非
const
版本:T*
- 返回指向第一个元素的非const
指针,允许通过指针修改元素。const
版本:T *const
- 返回指向第一个元素的const
指针,只允许通过指针读取元素,不能修改。
用法:
C++
int* data_ptr = vec.data(); // 获取指向 vector 数据的指针
7. 迭代器
begin()
(非const
和const
版本):功能: 返回指向
vector
起始位置的迭代器。断言:
assert(_data);
断言_data
指针有效。返回值:
非
const
版本:T*
- 返回指向第一个元素的指针,作为非const
迭代器。const
版本:const T*
- 返回指向第一个元素的const
指针,作为const
迭代器。
end()
(非const
和const
版本):功能: 返回指向
vector
结束位置的迭代器,即最后一个元素之后的位置。断言:
assert(_data);
断言_data
指针有效。返回值:
非
const
版本:T*
- 返回指向_data[_size]
的指针,作为非const
迭代器。const
版本:const T*
- 返回指向_data[_size]
的const
指针,作为const
迭代器。
用法:
C++
for (auto it = vec.begin(); it != vec.end(); ++it) { // 使用迭代器遍历 vector std::cout << *it << " "; } for (const auto& element : vec) { // 范围 for 循环,底层使用迭代器 std::cout << element << " "; }
8. 修改器
push_back(const T& value)
:参数:
value
(const T&
类型) - 要添加到vector
末尾的元素的常量引用。功能: 在
vector
的末尾添加一个元素,元素的值是value
的拷贝。实现: 直接调用
emplace_back(value)
完成添加操作。
push_back(const T&& value)
:参数:
value
(const T&&
类型) - 要添加到vector
末尾的元素的右值引用。功能: 在
vector
的末尾添加一个元素,元素的值是value
的移动。用于高效地添加临时对象或即将销毁的对象,避免不必要的拷贝。实现: 调用
emplace_back(std::move(value))
,使用std::move()
将value
转换为右值引用,然后传递给emplace_back()
,以触发移动构造。
emplace_back(params&&... p)
:模板参数:
params&&... p
- 可变参数模板,接受任意数量、任意类型的参数,并完美转发 (perfect forwarding) 到元素类型的构造函数。功能: 在
vector
的末尾就地构造一个新元素。与push_back
的区别在于,emplace_back
直接在vector
的内存空间中构造元素,而不是先创建临时对象再拷贝或移动,通常更高效。扩容:
if (_size == _capacity)
检查当前大小_size
是否已达到容量_capacity
。reserve(((_capacity + 1) * 3) >> 1);
如果容量不足,则调用reserve()
扩容。扩容策略是:新容量 =((_capacity + 1) * 3) / 2
,即增加当前容量的 50% (再加上原来的容量)。>> 1
是右移一位,等价于除以 2。(_capacity + 1)
是为了防止当_capacity
为 0 时,新容量仍然为 0。assert(_size < _capacity);
扩容后,断言_size
仍然小于新的_capacity
。
就地构造:
new (std::addressof(_data[_size])) T(std::forward<params>(p)...);
这是emplace_back
的核心。std::addressof(_data[_size])
获取vector
内部数据数组_data
的索引为_size
的内存地址。注意,此时这块内存是未构造的。new (placement new)
使用** placement new** 运算符。 placement new 的作用是在已分配但未构造的内存上就地构造对象。T(std::forward<params>(p)...)
调用类型T
的构造函数,并将可变参数p
完美转发给T
的构造函数。完美转发保留了参数的值类别 (左值或右值),确保使用最合适的构造函数版本 (例如,移动构造或拷贝构造)。
更新大小:
++_size;
构造成功后,将vector
的大小_size
增加 1。返回值:
return _data[_size - 1];
返回指向新构造元素的指针。_size
已经增加,所以_size - 1
是新添加元素的索引。
erase(u64 index)
:参数:
index
(u64
类型) - 要删除的元素的索引。功能: 删除
vector
中指定索引位置的元素。删除后,后续元素会向前移动以填补空位,保持元素的连续存储。断言:
assert(_data && index < _size);
确保_data
指针有效且索引index
有效。调用
erase(T* const item)
: 实际删除操作委托给重载版本的erase(T* const item)
函数,并将指向要删除元素的指针std::addressof(_data[index])
传递给它。
erase(T* const item)
:参数:
item
(T* const
类型) - 指向要删除元素的指针。功能: 删除
vector
中指针item
指向的元素。断言:
assert(_data && item >= std::addressof(_data[0]) && item < std::addressof(_data[_size]));
确保_data
指针有效,且item
指针指向vector
内部数据数组的有效元素范围内。元素析构 (条件编译):
if constexpr (destruct) item->~T();
如果destruct
模板参数为true
,则显式调用要删除元素的析构函数item->~T();
。减小大小:
--_size;
vector
的大小减 1。元素前移 (如果需要):
if (item < std::addressof(_data[_size])) { memcpy(item, item + 1, (std::addressof(_data[_size]) - item) * sizeof(T)); }
if (item < std::addressof(_data[_size]))
判断要删除的元素是否不是最后一个元素。如果是最后一个元素,则不需要移动元素。memcpy(item, item + 1, (std::addressof(_data[_size]) - item) * sizeof(T));
如果不是最后一个元素,则使用memcpy()
函数将要删除元素之后的所有元素向前移动一个位置。item
- 目标地址,即要删除元素的位置。item + 1
- 源地址,即要删除元素之后的位置。(std::addressof(_data[_size]) - item) * sizeof(T)
- 要复制的字节数,即从要删除元素之后的位置到vector
最后一个元素 (但不包括最后一个元素) 的所有元素的大小总和。
返回值:
return item;
返回指向已删除元素的指针item
。虽然元素已经被删除和移动,但返回指针本身可能在某些特殊情况下有用 (例如,用于迭代器失效处理)。
erase_unordered(u64 index)
:参数:
index
(u64
类型) - 要删除元素的索引。功能: 无序删除
vector
中指定索引位置的元素。与erase()
的区别在于,erase_unordered()
为了追求更高的删除效率,不保证删除元素后元素的顺序。它通常将最后一个元素移动到要删除元素的位置,然后直接删除最后一个元素。断言:
assert(_data && index < _size);
确保索引有效。调用
erase_unordered(T* const item)
: 实际操作委托给重载版本,传递要删除元素的指针。
erase_unordered(T* const item)
:参数:
item
(T* const
类型) - 指向要删除元素的指针。功能: 无序删除指针
item
指向的元素。断言:
assert(_data && item >= std::addressof(_data[0]) && item < std::addressof(_data[_size]));
确保指针有效。元素析构 (条件编译):
if constexpr (destruct) item->~T();
条件析构。减小大小:
--_size;
大小减 1。元素替换 (如果需要):
if (item < std::addressof(_data[_size])) { memcpy(item, std::addressof(_data[_size]), sizeof(T)); }
if (item < std::addressof(_data[_size]))
判断要删除的元素是否不是最后一个元素。memcpy(item, std::addressof(_data[_size]), sizeof(T));
如果是,则使用memcpy()
将最后一个元素 (位于_data[_size]
, 注意_size
此时已经减 1 了,但这里使用的是减 1 之前的_size
,即原vector
的最后一个元素的地址) 拷贝到要删除元素的位置item
。 这样就用最后一个元素覆盖了要删除的元素。
返回值:
return item;
返回指向已删除元素位置的指针。
clear()
:功能: 移除
vector
中的所有元素,使其变为空vector
。但它不释放已分配的内存空间 (容量_capacity
不变)。元素析构 (条件编译):
if constexpr (destruct) { destruct_range(0, _size); }
如果destruct
为true
,则调用destruct_range(0, _size)
析构所有元素。重置大小:
_size = 0;
将_size
设置为 0,表示vector
中不再包含任何有效元素。
swap(vector& o)
:参数:
o
(vector&
类型) - 要交换的另一个vector
对象的引用。功能: 交换当前
vector
对象和另一个vector
对象o
的内容。交换的内容包括_capacity
,_size
, 和_data
指针。自交换检查:
if (this != std::addressof(o))
防止与自身交换,如果尝试vec.swap(vec);
,则直接返回。交换实现:
auto temp(o);
创建一个o
的拷贝temp
(注意这里是拷贝,不是移动,因为要保证交换的正确性)。o = *this;
将当前vector
对象的内容赋值给o
(这里是赋值,会调用拷贝赋值或移动赋值,取决于*this
是左值还是右值。在swap
函数中,*this
是左值,所以会调用拷贝赋值)。*this = temp;
将临时拷贝temp
的内容赋值给当前vector
对象 (同样是拷贝赋值)。
注意: 这种
swap
的实现方式实际上是通过拷贝和赋值来模拟交换,效率可能不如直接交换内部指针的方式高。更高效的swap
实现通常会直接交换_capacity
,_size
, 和_data
这三个成员变量。 (代码中的swap
实现可能需要修正为更高效的版本,直接交换内部成员)
9. 总结
XEngine::utl::vector
类提供了一个基本的动态数组实现,包含:
多种构造方式: 默认构造,计数构造,带值计数构造,拷贝构造,移动构造,迭代器范围构造。
动态内存管理: 使用
realloc
动态调整容量,reserve
预留空间,destroy
和clear
清理资源。元素访问:
operator[]
,front
,back
,data
,begin
,end
等方法。修改操作:
push_back
,emplace_back
,resize
,erase
,erase_unordered
,clear
,swap
。可定制的析构行为: 通过模板参数
destruct
控制是否在元素移除时调用析构函数。
与 std::vector
的对比:
相似之处: 功能上类似,都提供了动态数组的基本操作。
不同之处:
定制化:
XEngine::utl::vector
提供了destruct
模板参数,允许用户控制析构行为,这在某些特殊场景下可能有用。内存管理: 使用了
realloc
和free
进行内存管理,可能在某些方面与std::vector
的内存分配策略有所不同。异常安全性: 代码中使用了
assert
进行断言检查,主要用于开发阶段的错误检测。在生产环境中,std::vector
通常提供更完善的异常安全性保证。效率:
std::vector
经过高度优化,通常在各种场景下都具有良好的性能。自定义的vector
可能在某些特定场景下为了追求极致性能而做出权衡。功能完整性:
std::vector
是 C++ 标准库的一部分,功能更全面,提供了更多的成员函数和算法支持。