本文目录如下:
目录
5)重载 ==, !=, 前置,后置++, 前置,后置--, =
一、vector容器简介
Vector容器是C++编程语言中的一种数据结构,属于标准模板库(STL)的一部分。它是一个动态数组的实现,可以根据需要动态调整大小。Vector容器具有以下特点:
1.动态大小:Vector容器的大小可以根据需要进行动态调整。这意味着你可以在运行时根据实际需求添加或删除元素,而无需在编译时确定容器的大小。Vector容器会自动处理内存管理。
2.连续存储:Vector容器中的元素在内存中是连续存储的。这意味着可以通过下标来访问容器中的元素,并且支持快速的随机访问。这种连续存储的特性使得Vector容器在需要频繁访问元素时非常高效。
3.自动扩容:当向Vector容器中添加元素时,如果容器的当前大小不足以容纳新元素,容器会自动扩容。扩容是通过重新分配内存并将原有元素复制到新内存空间中来实现的。这种自动扩容的特性使得Vector容器能够动态地适应元素的增长。
4.尾部操作高效:由于Vector容器中的元素是连续存储的,因此在尾部进行插入和删除操作是高效的。这是因为在尾部插入或删除元素时,不需要移动其他元素,只需调整尾部指针即可。
二、自己实现一个vector容器的好处
如果你是一个c++初学者,那么自己实现一个vector容器,可以巩固c++语法相关的知识,并且可以让自己对c++这门语言会有更加深入的了解,更重要的是能实实在在的提升自己的编程能力。
如果你对c++已经有了一定的了解,那么自己实现一个vector容器可以对vector有更加全面的认知,能够更加深入的掌握c++的模板、内存管理等内容,并且能可以锻炼问题解决能力和调试技巧。
总而言之,看完这篇文章,你将获得或强化如下技能点:
1.c++类模板的使用
2.c++的内存管理
3.c++移动语义的应用
4.c++中的深拷贝和浅拷贝
5.c++中的运算符重载
6.c++左值引用、右值引用的使用
7.c++引用折叠的使用
8.更加了解vector
三、代码实现
1.环境准备
笔者使用的代码编辑环境是visual studio2022,阅读本篇文章,请确保自己使用的是c++20版本
废话少说,接下来笔者将参照c++标准,正式开始vector的实现。
2.新建类
我们首先需要新建两个文件,一个Vector.hpp,一个test.cpp,Vector.hpp用来存放我们自己实现的Vector类的,test.cpp内包含main函数,用来测试代码是否正确。
Vector是一个模板类,这里我们将定义一个默认类型为allocator类的类模板参数,之后我们将用这个类来进行内存分配。
当然,你可能会问,为什么不用new和delete来管理内存,因为new分配内存时,会顺带调用类的构造函数创建对象,换句话说,new关键字把内存分配和创建对象这两个操作耦合了,这样无法满足一些特定需求(我们之后会谈到),所以这里使用allocator类来分配内存(只分配内存,不会创建对象)
//Vector.hpp
#pragma once
#include <memory>
#include <iostream>
#include <xmemory>
//定义了两个模板参数,一个是存放的数据类型_T,
//另一个是_Alloc模板参数,默认类型为std::allocator类,用于管理内存
template<class _T, class _Alloc = std::allocator<_T>>
class Vector {
};
3.成员变量
我们的Vector类中总共有三个成员变量,分别为size,cap,data和iterator
size指定了Vector中实际的元素个数,cap指明了Vector中实际的内存大小(需满足cap >= size),而data是一个_T类型的指针,用于指向Vector需要维护的数组,iterator是Vector类的迭代器,这个我们最后再去实现。
这里笔者需多强调一下,size只与Vector中元素的数量有关,而cap只与Vector中的data指向的实际内存大小有关,千万不能混淆。
//Vector.hpp
#pragma once
#include <memory>
#include <iostream>
#include <xmemory>
//定义了_Alloc模板参数,默认类型为std::allocator类,用于管理内存
template<class _T, class _Alloc = std::allocator<_T>>
class Vector {
private:
//给_Alloc起个别名
using _allocator_type = _Alloc;
//指向实际数组的指针
_T* _data;
//_data中元素数量
size_t _size;
//_data实际内存大小
size_t _cap;
};
4.构造函数和析构函数
1)无参默认构造
默认无参构造默认没有元素,所以size为0,但是为了让Vector不要频繁的开辟内存,所以我们需提前分配好内存,这里我们将初始内存大小设为16(你也可以自己决定),所以cap也为16。
当然,这里就说明了我们为什么不用new来分配内存了,因为new分配内存后会创建对象,而我们这里,元素个数为零,只需要分配内存,不需要额外创建对象(浪费性能),所以我们使用std::allocator来分配内存。
public:
//元素个数为0,内存大小16
Vector() : _size(0), _cap(16) {
//调用allocator的allocate方法来分配大小为16(16 * sizeof(_T))大小的内存
_data = _allocator_type{}.allocate(16);
}
2)初始化size的有参构造 函数
该构造函数指明了容器创建时有多少个元素,这里就需要分配内存的同时,也要创建默认的对象,这里直接将参数里的size赋值给_size和_cap,需注意的是,这里需提前判断一下,如果size参数是0的话,就不需要分配内存,并需要把_data置为空
//public
//explicit关键字,防止其隐式转换,提高代码可读性
explicit Vector(size_t size) : _size(size), _cap(size) {
//如果size为0就将_data置为空
if (_cap == 0) {
_data = nullptr;
return;
}
//分配内存
_data = _allocator_type().allocate(_cap);
//调用std::construct_at函数为_data指向的内存创建对象,这里会调用_T的默认构造函数
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i);
}
3)初始化size,cap和默认对象elem的有参构造函数
该构造函数有3个参数,size指定元素个数,cap指定内存(容量)大小,elem是用于填充满size个大小的内存的对象。
//public
Vector(size_t size, _T& elem, size_t cap) : _size(size), _cap(cap) {
//因为cap要大于或等于size,所以当size大于cap时,非法,抛出异常
if (_size > _cap) throw std::out_of_range("out of range");
//cap为零说明没有元素,置为空
if (_cap == 0) {
_data = nullptr;
return;
}
//为_data申请cap大小的内存
_data = _allocator_type().allocate(_cap);
//从下标为零开始创建size个对象,用elem来填充
for (size_t i = 0; i < _size; i++)
//as_const把elem转为常量引用
std::construct_at(_data + i, std::as_const(elem));
}
4)初始化size和默认填充对象elem的有参构造函数
该构造函数会初始化_size并用对象elem填充,_cap = _size = size,其实该构造函数就等同于我们实现的上一个构造函数cap和size相等时的调用,为了避免产生冗余代码,我们可以委托给上一个构造函数,这里我们需要实现两个版本,一个接收左值引用的elem,一个接收右值引用的elem
//public
//委托给其他构造函数构造
Vector(size_t size, _T&& elem) : Vector(size, elem, size) {}
Vector(size_t size, _T& elem) : Vector(size, elem, size) {}
5)拷贝构造函数
拷贝构造函数接收另一个Vector的常量引用进行深拷贝,需要把另一个Vector中的_data中的数据全都拷贝过来,这里我们依旧是使用std::construct_at来构造,可能有人会问,为什么不用memcpy这类的函数来拷贝,这里解释一下,因为memcpy是按位拷贝内存,不会调用对象的构造函数,也就是说当_data中存放的类比较复杂时,memcpy本质上还是浅拷贝,所以我们要使用std::construct_at来显式的构造对象。
//public
Vector(const Vector<_T>& other) : _size(other._size), _cap(other._cap) {
//如果cap为0,_data置为空
if (_cap == 0) _data = nullptr;
else {
//分配内存
_data = _allocator_type().allocate(_cap);
//使用other中_data存放的类来创建对象(拷贝)
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i, std::as_const(other._data[i]));
}
}
6)移动构造函数
移动构造函数用于转移另一个类的资源的所有权,也就是说调用了Vector的移动构造函数后,此对象(this)就拥有了另一个对象(other)的_data指向内存的所有权,与拷贝构造不同的是,拷贝构造后此对象(this)和另一个对象(other)拥有相同的两份资源,而调用移动构造后只是把资源所有权从另一个对象(other)转移给了此对象(this),本质还是只有一份资源,这样做可以避免拷贝,提高性能,代价是另一个对象(other)失去了这份资源。下面笔者用两张图来说明拷贝构造和移动构造的区别。
//public
//explicit防止隐式转换,将other._data赋值给this->_data(转移资源所有权)
explicit Vector(Vector<_T>&& other) noexcept : _data(other._data), _size(other._size), _cap(other._cap) {
//将other的_data置为空,size,cap置为0
other._data = nullptr;
other._cap = 0;
other._size = 0;
}
7)列表初始化构造函数
我们知道,stl中的vector可以使用这种方式来构造vector对象——"std::vector<int> v = {1, 2, 3}",如果想要实现这样的效果,就需要实现列表初始化构造函数,实现很简单,传入一个std::initializer_list<_T>类对象作为参数,然后把里面的数据移动(不是拷贝)给_data即可。
//public
//传入std::initializer_list对象右值引用作为参数(减少拷贝)
Vector(std::initializer_list<_T>&& list) : _size(list.size()), _cap(list.size()) {
//如果初始化列表大小为0,则把_data置为空
if (_cap == 0) {
_data = nullptr;
return;
}
//分配内存
_data = _allocator_type().allocate(_cap);
//将list中的元素移动到_data指向的内存中
size_t __idx = 0;
for (auto it = list.begin(); it != list.end(); it++) {
//用std::move移动语义调来完成移动
std::construct_at(_data + __idx, std::move(*it));
__idx++;
}
}
这样我们就可以使用初始化列表来构造Vector了 Vector<int> v = {1, 2, 3, 4}
8)析构函数
析构前需判断_data是否为空,在析构时,首先使用std::destroy_at来显示的析构_data内存中的对象,然后再用std::allocator的deallcate方法来释放内存。
//public
~Vector() {
//_data不为空则执行析构逻辑
if (_data) {
//显式销毁内存中的对象
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
//根据_cap的大小来释放内存
_allocator_type().deallocate(_data, _cap);
_data = nullptr;
}
}
至此,Vector中的构造函数和析构函数的编写就此结束。
5.获取_size和_cap以及下标访问元素的功能实现
1)获取_size和_cap
这个实现较简单,直接编写两个名为size和cap的函数返回值即可。
//public
size_t size() noexcept { return _size; }
size_t cap() noexcept { return _cap; }
2)通过at()和[]进行下标访问
然后我们开始实现通过下标访问元素。vector中提供了两种访问方式,一种是中括号[]访问,一种是通过at函数访问,区别在于[]方式不会进行非法检测,不会抛出一异常。而at()访问则可能会抛出异常。 需要注意的是,因为访问返回的元素是可以被修改的,所以需返回左值引用。而且如果声明Vector时是一个常量,访问返回的值是不能修改的,所以需返回常量引用。因此这里需提供两个版本,一个给常量的,一个给非常量。
//public
_T& at(size_t idx) {
//如果下标非法则抛出异常
if (idx >= _size) throw std::out_of_range("out of range");
return _data[idx];
}
//在圆括号后声明const,表示匹配的为常量
const _T& at(size_t idx) const {
if (idx >= _size) throw std::out_of_range("out of range");
return _data[idx];
}
//通过operator关键字重载[]运算符
_T& operator[] (size_t idx) noexcept {
return _data[idx];
}
const _T& operator[] (size_t idx) const noexcept {
return _data[idx];
}
至此我们就可以测试一下了,我们在test.cpp中编写一下测试代码。
//test.cpp
#include "Vector.hpp"
int main() {
Vector<int> v = { 1, 2, 3, 4, 5 };
std::cout << "cap: " << v.cap() << std::endl;
std::cout << "size: " << v.size() << std::endl;
std::cout << "v.at(2): " << v.at(2) << std::endl;
std::cout << "v[3]: " << v[3] << std::endl;
return 0;
}
结果如下:
符合预期。
6.访问第一个元素和最后一个元素
vector中提供了front和back来访问第一个元素和最后一个元素,也就是0号元素和size -1号元素,因为返回的是左值引用,所以我们同样需要准备两个版本,一个绑定常量,一个绑定非常量。
//public
_T& back() {
//判断容器是否为空,为空则抛出异常
if (_size == 0) throw std::out_of_range("out of range");
return _data[_size - 1];
}
const _T& back() const {
if (_size == 0) throw std::out_of_range("out of range");
return _data[_size - 1];
}
_T& front() {
if (_size == 0) throw std::out_of_range("out of range");
return _data[0];
}
const _T& front() const {
if (_size == 0) throw std::out_of_range("out of range");
return _data[0];
}
7.resize函数的实现
resize函数用于给_data重新分配内存大小,是支持Vector容量变化的核心函数,以size作为参数,为_data开辟新的内存空间,并将原来内存中的元素移动到新内存中,这里我们需考虑两种情况,新size比原来的cap大时,就移动原size个元素到新内存,但如果新size比原cap小则移动新size个元素到新内存中。如果新size为0则把_data置为空。最后要切记一定要释放原
的内存,否则会内存泄漏。
//public
void resize(size_t size) {
//如果_data不为空
if (_data) {
if (size == 0) {
//如果size为0,则释放所有内测,把_data置为空
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_allocator_type().deallocate(_data, _cap);
_data = nullptr;
_size = 0;
_cap = 0;
return;
}
//如果size不为0,则给_data开辟新的指定大小的内存
//这里用一个临时指针存放一下原内存地址,待会释放掉
_T* temp = _data;
_data = _allocator_type().allocate(size);
for (size_t i = 0; i < std::min(size, _size); i++)
std::construct_at(_data + i, temp[i]);
//析构原内存中的对象并释放内存。
for (size_t i = 0; i < _size; i++)
std::destroy_at(temp + i);
_allocator_type().deallocate(temp, _cap);
_size = size;
_cap = size;
return;
}
//如果_data为空且size也为空,就什么也不用做了
if (size == 0) return;
//否则直接开辟内存,因为之前_data为空,所以也没有对象,也就不用移动了
_data = _allocator_type().allocate(size);
_size = size;
_cap = size;
}
8.push_back()函数的实现
push_back()应该是大家非常熟悉的函数了,它的功能是在容器尾部插入一个元素。需要说明的是这里的尾部是相对于_size而言的,而当插入后_size大于了_cap则就需要动态扩容,这也是vector容器的灵魂,因此要实现push_back,我们需要先实现容器自动扩容的函数。
1)容器扩容函数 _data_cap_grow_的实现
_data_cap_grow_接受一个size参数,用来指明容器新的size大小,用过vector的同学应该知道,vector是双倍扩容,也就是说,如果vector需要扩容的话,那它扩容后的容量会成为原来的两倍。所以这里我们需要提前判断,如果新的size大于_cap则需要扩容,新的容量为原来的两倍,但需要注意到是,如果新的size比两倍_cap还要大的话,扩容大小就直接取size就行。这个函数我们设为私有。
//private
void _data_cap_grow_(size_t size) {
//首先判断原cap是否为0
if (_cap) {
//如果新size大于_cap直接扩容
if (size > _cap) {
//可能新size比两倍cap还要大,所以取两者最大值
resize(std::max(_cap * 2, size));
return;
}
return;
}
//如果原cap为0,则直接扩容。
resize(size);
}
2)_emplace_one_at_back函数的实现
在实现vector之前我们需要额外实现一个函数来执行真正的尾插操作,之所以要额外包装一层是因为我们的push_back需要提供两个版本,一个左值引用,一个右值引用,这里我们可以使用c++的引用折叠来解决,如果我们不使用引用折叠的话就需要实现两份功能一样的代码,这样会增加冗余代码。_emplace_one_at_back函数接收一个_T类型的右值引用。
//private
void _emplace_one_at_back(_T&& elem) {
//因为调用_data_cap_grow_后,size就改变了,所以我们需要临时存储一下
size_t size_tmp = _size;
//新的size大小为_size + 1
_data_cap_grow_(_size + 1);
_size = size_tmp + 1;
//在内存中构造对象
std::construct_at(_data + _size - 1, elem);
}
3)实现push_back()
现在我们可以真正的实现push_back了,需要提供左值引用和右值引用两个版本
//public
void push_back(const _T& elem) {
_emplace_one_at_back(elem);
}
void push_back(_T&& elem) {
_emplace_one_at_back(std::move(elem));
}
9.pop_back()函数
实现pop_back函数较为简单,将最后一个元素析构,_size--即可。
//public
void pop_back() {
//如果容器中没有元素则返回
if (_size == 0) return;
//析构最后一个元素
std::destroy_at(_data + _size - 1);
_size--;
}
10.erase函数
erase接收一个下标,用于删除指定下标的元素。当删除第i个元素时,可以把i + 1开始往后的元素依次往前移动,最后析构最后一个元素,并把_size--即可。
//public
bool erase(size_t idx) {
//判断下标是否合法,非法返回false
if (idx >= _size) return false;
//将idx之后的元素依次向前一个元素移动
for (size_t i = idx + 1; i < _size; i++)
_data[i - 1] = std::move(_data[i]);
//析构最后一个元素
std::destroy_at(_data + _size - 1);
_size--;
return true;
}
11.insert函数
insert函数接收一个下标,在指定下标的位置插入指定元素,当在第i个元素插入元素时,将第i个元素及其之后的元素向后移动,然后析构下标为i的元素,最后在下标为i的地方构造新的对象,再把_size++。需要注意的是,因为size增大,我们需要判断是否要进行扩容,所以这里还需要调用_data_cap_grow_函数。因为insert函数同样需要提供两个版本,一个参数为右值引用,一个为常量左值引用,为了减少冗余代码,这里使用引用折叠,所以插入的实际逻辑我们全放在_emplace_one_at_idx这个函数里,insert只需要调用即可。
1)_emplace_one_at_idx函数
接收两个参数,size_t下标和_T的右值引用
//private
bool _emplace_one_at_idx(size_t idx, _T&& elem) {
//判断idx是否合法
if (idx >= _size) return false;
//因为调用扩容函数,_size会发生变化,临时存储一下
size_t size_tmp = _size;
//扩容
_data_cap_grow_(_size + 1);
_size = size_tmp + 1;
//将idx及其之后的元素向后移动
for (size_t i = idx; i < _size; i++)
_data[i] = std::move(_data[i - 1]);
std::destroy_at(_data + idx);
//在idx位置构建新的对象
std::construct_at(_data + idx, std::as_const(elem));
return true;
}
2)实现insert
两个版本的insert,直接调用_emplace_one_at_idx即可。
//public
bool insert(size_t idx, const _T& elem) {
return _emplace_one_at_idx(idx, elem);
}
bool insert(size_t idx, _T&& elem) {
return _emplace_one_at_idx(idx, std::move(elem));
}
12.swap函数
swap函数用于交换两个Vector的元素,我们可以直接交换两个Vector的_data指针和_size,_cap的值即可。
//public
void swap(Vector<_T>& other) noexcept {
std::swap(_size, other._size);
std::swap(_cap, other._cap);
std::swap(_data, other._data);
}
13.重载=运算符
重载=,使两个Vector可以使用=运算,需提供两个版本,一个是常量左值引用,用于拷贝另一个Vector的数据,另一个为右值引用,可以使用移动语义避免拷贝,需要注意的是两个版本都需要重新开辟内存。
//public
//拷贝
void operator= (const Vector& other) {
_size = other._size;
_cap = other._cap;
if (_cap == 0) {
_data = nullptr;
return;
}
_data = _allocator_type().allocate(_cap);
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i, std::as_const(other._data[i]));
}
//移动
void operator= (Vector<_T>&& other) noexcept {
if (_data) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_allocator_type().deallcate(_data, _cap);
}
_data = other._data;
_size = other._size;
_cap = other._cap;
//移动完后一定要把other的各项资源置为0或空
other._data = nullptr;
other._cap = 0;
other._size = 0;
}
14.empty函数
返回元素个数是否为零,也就是_size是否等于零
//public
bool empty() noexcept { return _size == 0; }
15.clear()
清空容器重点元素,但容量不变
//public
void clear() {
if (_data) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_size = 0;
}
}
16.assign函数
assign函数有两个重载,一个接收两个参数,元素个数和指定元素,可以给Vector重新写进指定个数的指定元素,另一个重载接收一个初始化参数列表,可以把列表里的元素重新移动给容器。需要注意,移动前需要提前将容器清空。
//public
void assign(size_t cnt, const _T& elem) {
//提前清空
_clear_all_();
_cap = cnt;
_size = cnt;
if (_cap == 0) return;
_data = _allocator_type().allocate(cnt);
for (size_t i = 0; i < cnt; i++)
std::construct_at(_data + i, std::as_const(elem));
}
void assign(std::initializer_list<_T> list) {
_clear_all_();
_cap = list.size();
_size = list.size();
if (_cap == 0) return;
_data = _allocator_type().allocate(_cap);
size_t __idx = 0;
for (auto it = list.begin(); it != list.end(); it++) {
std::construct_at(_data + __idx, std::move(*it));
__idx++;
}
}
17.shrink_to_fit函数
shrink_to_fit函数的作用是抛弃多余的容量,使容量大小完全等于_size,这里我们可以直接调用resize,把_size当作参数传进去即可。
//public
void shrink_to_fit() {
resize(_size);
}
18.Vector迭代器
我们都知道vector是支持增强for循环的,要使我们的Vector也支持增强for循环或者使用迭代器循环的话,我们就需要自己实现一个迭代器。我们新建一个文件名叫Iterator.hpp,在里面编写Iterator。
1)新建一个模板类,模板参数为_T
template<typename _T>
class Iterator {
};
2)私有成员变量_T* ptr
ptr指针用于指向容器中的一个元素
template<typename _T>
class Iterator {
private:
_T* _ptr;
};
3)构造函数
提供三个构造函数,一个无参构造,一个接收一个_T类型的指针,还有一个拷贝构造函数。
//public
Iterator(_T* ptr) noexcept : _ptr(ptr) {}
Iterator() noexcept : _ptr(nullptr) {}
Iterator(const Iterator<_T>& other) : _ptr(other._ptr) {}
4)重载解引用(*)运算符
将_ptr解引用,返回其指向的值的左值引用
//public
_T& operator* () const {
return *_ptr;
}
5)重载 ==, !=, 前置,后置++, 前置,后置--, =
以上运算符其实都是指针常用的运算符,实现起来并无难度,其实就是用函数包装了一下逻辑。
//public
void operator= (const Iterator<_T>& other) {
_ptr = other._ptr;
}
bool operator!= (const Iterator<_T>& other) {
return _ptr != other._ptr;
}
bool operator== (const Iterator<_T>& other) {
return _ptr == other._ptr;
}
Iterator& operator++ () noexcept {
++_ptr;
return *this;
}
//后置++需要一个int作为占位符
Iterator& operator++ (int) {
++_ptr;
return *this;
}
//后置--需要int作为占位符
Iterator& operator-- (int) {
++_ptr;
return *this;
}
Iterator& operator-- () noexcept {
--_ptr;
return *this;
}
至此,Vector的迭代器编写完毕。
6) 在Vector中使用迭代器
我们回到Vector.hpp,在这里引入Iterator.hpp的头文件。在Vector类里的共有作用域里using一下,方便外部引用,然后我们需要实现begin和end两个函数,来返回第一个元素和最后一个元素的Iterator指针,这两个函数是能够使用增强for的关键,实现了这两个函数,Vector就可以使用增强for了。
//public
using iterator = Iterator<_T>;
iterator begin() noexcept {
//如果元素个数为0,则返回null
if (_size == 0) return nullptr;
return &_data[0];
}
iterator end() noexcept {
if (_size == 0) return nullptr;
return &_data[_size];
}
到这,我们的Vector就全部实现完毕了,我们在test.cpp里再来测试一下
//test.cpp
#include "Vector.hpp"
int main() {
Vector<int> v = { 1, 2, 3, 4, 5 };
Vector<int> v2 = v;
v2.push_back(88);
int e = 0;
Vector<int> v3 = Vector<int>(8, 9);
v3[2] = 5;
v3.at(6) = 34;
for (Vector<int>::iterator it = v.begin(); it != v.end(); it++)
std::cout << *it << " ";
std::cout << std::endl;
for (auto i : v2)
std::cout << i << " ";
std::cout << std::endl;
for (size_t i = 0; i < v3.size(); i++)
std::cout << v3[i] << " ";
std::cout << std::endl;
return 0;
}
运行结果:
结果符合预期。当然你也可以自己写一个复杂一点的结构体或类来进行测试,这里笔者因为已经进行过复杂的测试,这里就不多演示了。
三、总结
本文详细介绍了如何自己实现一个vector动态数组,本文中的Vector是参考的官方vector文档进行的实现,基本上实现了vector容器的大部分功能。完整的源码如下所示。
Vector.hpp
#pragma once
#include <memory>
#include <iostream>
#include <xmemory>
#include "Iterator.hpp"
template<class _T, class _Alloc = std::allocator<_T>>
class Vector {
private:
using _allocator_type = _Alloc;
_T* _data;
size_t _size;
size_t _cap;
bool _emplace_one_at_idx(size_t idx, _T&& elem) {
if (idx >= _size) return false;
size_t size_tmp = _size;
_data_cap_grow_(_size + 1);
_size = size_tmp + 1;
for (size_t i = idx; i < _size; i++)
_data[i] = std::move(_data[i - 1]);
std::destroy_at(_data + idx);
std::construct_at(_data + idx, std::as_const(elem));
return true;
}
void _clear_all_() {
if (_data) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_allocator_type().deallocate(_data, _cap);
_data = nullptr;
}
}
void _emplace_one_at_back(_T&& elem) {
size_t size_tmp = _size;
_data_cap_grow_(_size + 1);
_size = size_tmp + 1;
std::construct_at(_data + _size - 1, elem);
}
void _data_cap_grow_(size_t size) {
if (_cap) {
if (size > _cap) {
resize(std::max(_cap * 2, size));
return;
}
return;
}
resize(size);
}
public:
using iterator = Iterator<_T>;
Vector() : _size(0), _cap(16) {
_data = _allocator_type{}.allocate(16);
}
Vector(const Vector<_T>& other) : _size(other._size), _cap(other._cap) {
if (_cap == 0) _data = nullptr;
else {
_data = _allocator_type().allocate(_cap);
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i, std::as_const(other._data[i]));
}
}
Vector(size_t size, _T&& elem, size_t cap) : _size(size), _cap(cap) {
if (_size > _cap) throw std::out_of_range("out of range");
if (cap == 0) {
_data = nullptr;
return;
}
_data = _allocator_type().allocate(_cap);
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i, elem);
}
Vector(size_t size, _T&& elem) : Vector(size, elem, size) {}
Vector(size_t size, _T& elem, size_t cap) : _size(size), _cap(cap) {
if (_size > _cap) throw std::out_of_range("out of range");
if (_cap == 0) {
_data = nullptr;
return;
}
_data = _allocator_type().allocate(_cap);
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i, std::as_const(elem));
}
Vector(size_t size, _T& elem) : Vector(size, elem, size) {}
explicit Vector(Vector<_T>&& other) noexcept : _data(other._data), _size(other._size), _cap(other._cap) {
other._data = nullptr;
other._cap = 0;
other._size = 0;
}
explicit Vector(size_t size) : _size(size), _cap(size) {
if (_cap == 0) {
_data = nullptr;
return;
}
_data = _allocator_type().allocate(_cap);
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i);
}
Vector(std::initializer_list<_T>&& list) : _size(list.size()), _cap(list.size()) {
if (_cap == 0) {
_data = nullptr;
return;
}
_data = _allocator_type().allocate(_cap);
size_t __idx = 0;
for (auto it = list.begin(); it != list.end(); it++) {
std::construct_at(_data + __idx, std::move(*it));
__idx++;
}
}
void operator= (const Vector& other) {
_size = other._size;
_cap = other._cap;
if (_cap == 0) {
_data = nullptr;
return;
}
_data = _allocator_type().allocate(_cap);
for (size_t i = 0; i < _size; i++)
std::construct_at(_data + i, std::as_const(other._data[i]));
}
void operator= (Vector<_T>&& other) noexcept {
if (_data) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_allocator_type().deallcate(_data, _cap);
}
_data = other._data;
_size = other._size;
_cap = other._cap;
other._data = nullptr;
other._cap = 0;
other._size = 0;
}
_T& at(size_t idx) {
if (idx >= _size) throw std::out_of_range("out of range");
return _data[idx];
}
const _T& at(size_t idx) const {
if (idx >= _size) throw std::out_of_range("out of range");
return _data[idx];
}
_T& operator[] (size_t idx) noexcept {
return _data[idx];
}
const _T& operator[] (size_t idx) const noexcept {
return _data[idx];
}
bool empty() noexcept { return _size == 0; }
size_t size() noexcept { return _size; }
size_t cap() noexcept { return _cap; }
_T& back() {
if (_size == 0) throw std::out_of_range("out of range");
return _data[_size - 1];
}
const _T& back() const {
if (_size == 0) throw std::out_of_range("out of range");
return _data[_size - 1];
}
_T& front() {
if (_size == 0) throw std::out_of_range("out of range");
return _data[0];
}
const _T& front() const {
if (_size == 0) throw std::out_of_range("out of range");
return _data[0];
}
void clear() {
if (_data) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_size = 0;
}
}
void resize(size_t size) {
if (_data) {
if (size == 0) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_allocator_type().deallocate(_data, _cap);
_data = nullptr;
_size = 0;
_cap = 0;
return;
}
_T* temp = _data;
_data = _allocator_type().allocate(size);
for (size_t i = 0; i < std::min(size, _size); i++)
std::construct_at(_data + i, temp[i]);
for (size_t i = 0; i < _size; i++)
std::destroy_at(temp + i);
_allocator_type().deallocate(temp, _cap);
_size = size;
_cap = size;
return;
}
if (size == 0) return;
_data = _allocator_type().allocate(size);
_size = size;
_cap = size;
}
void swap(Vector<_T>& other) noexcept {
std::swap(_size, other._size);
std::swap(_cap, other._cap);
std::swap(_data, other._data);
}
void push_back(const _T& elem) {
_emplace_one_at_back(elem);
}
void push_back(_T&& elem) {
_emplace_one_at_back(std::move(elem));
}
void pop_back() {
if (_size == 0) return;
std::destroy_at(_data + _size - 1);
_size--;
}
bool erase(size_t idx) {
if (idx >= _size) return false;
for (size_t i = idx + 1; i < _size; i++)
_data[i - 1] = std::move(_data[i]);
std::destroy_at(_data + _size - 1);
_size--;
return true;
}
void shrink_to_fit() {
resize(_size);
}
void assign(size_t cnt, const _T& elem) {
_clear_all_();
_cap = cnt;
_size = cnt;
if (_cap == 0) return;
_data = _allocator_type().allocate(cnt);
for (size_t i = 0; i < cnt; i++)
std::construct_at(_data + i, std::as_const(elem));
}
void assign(std::initializer_list<_T> list) {
_clear_all_();
_cap = list.size();
_size = list.size();
if (_cap == 0) return;
_data = _allocator_type().allocate(_cap);
size_t __idx = 0;
for (auto it = list.begin(); it != list.end(); it++) {
std::construct_at(_data + __idx, std::move(*it));
__idx++;
}
}
bool insert(size_t idx, const _T& elem) {
return _emplace_one_at_idx(idx, elem);
}
bool insert(size_t idx, _T&& elem) {
return _emplace_one_at_idx(idx, std::move(elem));
}
iterator begin() noexcept {
if (_size == 0) return nullptr;
return &_data[0];
}
iterator end() noexcept {
if (_size == 0) return nullptr;
return &_data[_size];
}
~Vector() {
if (_data) {
for (size_t i = 0; i < _size; i++)
std::destroy_at(_data + i);
_allocator_type().deallocate(_data, _cap);
_data = nullptr;
}
}
};
Iterator.hpp
template<typename _T>
class Iterator {
private:
_T* _ptr;
public:
Iterator(_T* ptr) noexcept : _ptr(ptr) {}
Iterator() noexcept : _ptr(nullptr) {}
_T& operator* () const {
return *_ptr;
}
Iterator(const Iterator<_T>& other) : _ptr(other._ptr) {}
void operator= (const Iterator<_T>& other) {
_ptr = other._ptr;
}
bool operator!= (const Iterator<_T>& other) {
return _ptr != other._ptr;
}
bool operator== (const Iterator<_T>& other) {
return _ptr == other._ptr;
}
Iterator& operator++ () noexcept {
++_ptr;
return *this;
}
Iterator& operator++ (int) {
++_ptr;
return *this;
}
Iterator& operator-- (int) {
++_ptr;
return *this;
}
Iterator& operator-- () noexcept {
--_ptr;
return *this;
}
};