在 C++ 中,std::vector
是最常用的动态数组容器。它具有高效的内存管理、动态扩容、随机访问等特点。在这篇博客中,我们将从零开始,实现一个功能强大、灵活、并且具有高性能的 Vector
类,具备 std::vector
的大部分功能,包括动态扩容、迭代器、模板支持、随机访问等,尽可能模仿 C++ 标准库中的 std::vector
。本文将详细解释每个功能模块,并附加详细的解释和代码注释,以帮助初学者深入理解容器的内部工作原理和实现逻辑。
一、设计目标和规划
在设计一个 Vector
类时,我们需要考虑以下功能:
- 动态扩容:支持自动增长容量,避免内存溢出。
- 模板支持:容器应该能够存储任意类型的数据。
- 迭代器:提供正向和反向迭代器,使得容器可以通过迭代器进行遍历。
- 边界检查:防止访问越界,提高容器的健壮性。
- 性能优化:通过使用移动语义和完美转发提升性能,减少不必要的拷贝。
- 异常安全:确保在操作失败时不出现内存泄漏等问题。
让我们从基础的 Vector
实现开始,逐步添加这些功能。
注意:这篇博客所涉及的所有代码可以从我的代码仓库获得:https://git.lenyiin.com/Lenyiin/Vector
二、Vector 容器的基础实现
首先,我们需要定义一个 Vector
类来管理内部的数据。这个类包含动态分配的数组,维护当前元素数量和容量,并且提供基础的操作方法,比如插入、访问等。
2.1、Vector 容器的成员的设计与初始化
首先,我们定义一个 Vector
类的基础框架,它需要包括以下几个核心成员变量:
_start
:指向存储实际元素的数组首地址。_finish
:指向存储实际元素的数组下一个可存储的地址。_endofstorage
:当前已分配的内存空间的末地址。
template<typename T>
class Vector {
public:
// 默认构造
Vector() // 构造函数,初始化为空
: _start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{
}
// 析构函数
~Vector() // 析构函数,释放动态内存
{
if (_start)
{
delete[] _start;
}
_start = _finish = _endofstorage = nullptr;
}
private:
T* _start;
T* _finish;
T* _endofstorage;
};
详细解释:
- 构造函数:在构造函数中,初始状态下
_start
为空,_finish
为 空,_endofstorage
也为空。 - 析构函数:确保当
Vector
对象销毁时释放分配的内存。
2.2、获取容器状态
为了更好的了解容器状态,还需要添加获取容器状态的接口
// 获取当前元素有效个数
size_t size() const
{
return _finish - _start;
}
// 获取当前容器容量
size_t capacity() const
{
return _endofstorage - _start;
}
详细解释:
size()
和capacity()
:分别返回当前的元素个数和容器容量,帮助用户了解容器的状态。
2.3、拷贝与赋值
在 C++
中,拷贝一个对象时,默认的拷贝行为是浅拷贝,即仅复制对象的指针或引用。这对管理动态内存的类而言,可能会导致问题,例如多个对象指向同一块内存,导致重复释放或修改冲突。为避免这些问题,我们需要实现深拷贝。
2.3.1、拷贝构造函数
拷贝构造函数用于创建一个新对象,该对象是通过复制另一个现有对象生成的。对于 Vector
类,我们需要确保在拷贝时,新对象有自己独立的内存副本。
// 拷贝构造
Vector(const Vector<T>& v)
{
_start = new T[v.capacity()];
_finish = _start;
_endofstorage = _start + v.capacity();
for (size_t i = 0; i < v.size(); i++)
{
*_finish = v[i];
++_finish;
}
}
详细解释:
- 深拷贝:通过分配新内存来创建新对象的独立副本,而不是简单地复制指针。这样,两个
Vector
对象可以独立管理各自的内存,避免潜在的内存管理冲突。 - 值得注意的是
*_finish = v[i];
并不是浅拷贝,而是调用了operator=
赋值运算,是深拷贝。
进阶:
- 也可以复用插入函数
push_back
Vector(const Vector<T>& v)
: _start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{
reserve(v.capacity());
for (const auto& e : v)
{
push_back(e);
}
}
2.3.2、赋值运算
赋值运算符用于将一个对象的内容复制到另一个已经存在的对象中。为了避免自赋值和内存泄漏,我们需要在实现赋值运算符时特别小心。
// 赋值运算符重载
Vector<T>& operator=(const Vector<T>& v)
{
if (this != &v)
{
delete[] _start;
_start = new T[v.capacity()];
//memcpy(_start, v._start, sizeof(T) * v.size()); 按字节拷贝,浅拷贝
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i]; // 调用的是 operator= 深拷贝
}
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
return *this;
}
详细解释:
- 自赋值检查:在赋值运算符实现中,首先检查是否为自赋值,即
this
指针是否与v
相同。如果是自赋值,则无需进行任何操作,直接返回当前对象。 - 内存管理:在分配新内存之前,记得释放当前对象所持有的旧内存,防止内存泄漏。
- 深拷贝:与拷贝构造函数类似,通过分配新内存来存储字符串的副本,确保两个对象独立管理各自的内存。
进阶:
- 也可以复用拷贝构造函数
// 进阶写法
void swap(Vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
Vector<T>& operator=(Vector<T> v)
{
swap(v);
return *this;
}
2.4、移动语义
C++11 引入的移动语义(Move Semantics)是提升性能的一个重要机制,允许在一定条件下避免不必要的深拷贝,从而提高程序性能。移动构造函数和移动赋值运算符是移动语义的核心。
2.4.1、移动构造函数
移动构造函数用于将资源从一个对象 “搬迁” 到另一个对象,而不是复制。这在避免不必要的内存分配和拷贝时非常有用。
// 移动构造函数
Vector(Vector&& v) noexcept
: _start(v._start), _finish(v._finish), _endofstorage(v._endofstorage)
{
v._start = nullptr;
v._finish = nullptr;
v._endofstorage = nullptr;
}
详细解释:
-
移动构造函数:将
v
对象的资源直接转移到当前对象(通过简单地复制指针),然后将v
的_start, _finish, _endofstorage
重置为默认状态。通过窃取资源(即指针和相关的大小和容量),避免昂贵的拷贝操作。 -
noexcept
:标记为noexcept
的函数表示在其内部不会抛出异常。这对移动构造函数尤其重要,因为这确保了在某些情况下(如在标准容器中使用)不会因为抛出异常而触发回滚操作。
2.4.2、移动赋值运算
移动赋值运算用于将资源从一个对象 “搬迁” 到另一个已存在的对象中。
// 移动赋值运算符
Vector& operator=(Vector&& v) noexcept
{
if (this != &v) {
delete[] _start;
_start = v._start;
_finish = v._start + v.size();
_endofstorage = v._start + v.capacity();
v._start = nullptr;
v._finish = nullptr;
v._endofstorage = nullptr;
}
return *this;
}
详细解释:
- 资源搬迁:通过简单地复制指针,将
v
对象的资源转移到当前对象中,并释放当前对象的旧资源。然后,将v
对象重置为默认状态,避免两个对象共享同一块内存。 - 自赋值检查:和赋值运算符一样,首先检查是否为自赋值。
2.5、下标运算符重载
为了支持像数组一样通过下标访问 Vector
中的元素,我们需要重载 operator[]
。
T& operator[](size_t index)
{
if (index >= size())
{
throw std::out_of_range("Index out of range");
}
return _start[index];
}
const T& operator[](size_t index) const
{
if (index >= size())
{
throw std::out_of_range("Index out of range");
}
return _start[index];
}
详细解释:
operator[]
重载:支持通过下标访问元素,并且提供const
版本,确保容器在只读情况下的访问安全性。- 越界检查:若访问的下标超出范围,抛出
std::out_of_range
异常,避免非法访问。
三、进阶功能
3.1、动态扩容
C++ 中 std::vector
最强大的功能之一是动态扩容。当容器中的元素超过当前容量时,它会自动分配更大的内存,并将已有的数据复制到新的内存区域。为此,我们实现一个 reserve 和 resize
函数,用于在需要时扩容。
void reserve(size_t newcapacity)
{
if (newcapacity > capacity())
{
T* tmp = new T[newcapacity]; // 分配新内存
size_t len = size();
if (_start)
{
for (size_t i = 0; i < len; i++)
{
tmp[i] = std::move(_start[i]); // 移动已有数据
}
delete[] _start; // 释放旧的内存
}
// 更新指针
_start = tmp;
_finish = tmp + len;
_endofstorage = tmp + newcapacity;
}
}
// resize 不仅会开空间,还会初始化
void resize(size_t newsize, const T& val = T())
{
if (newsize < size())
{
_finish = _start + newsize;
}
else
{
if (newsize > capacity())
{
reserve(newsize);
}
while (_finish < _start + newsize)
{
*_finish = val;
++_finish;
}
}
}
详细解释:
reserve
函数:用于重新分配内存,当当前容量不足时,分配一个新的数组并将原有数据复制过去。resize
函数:用于重置有效数据大小,重置的时候还能进行初始化。当当前容量不足时,分配一个新的数组并将原有数据复制过去。std::move
:我们使用std::move
来避免不必要的深度拷贝,提升性能。
3.2、插入元素 push_back
向 Vector
中添加元素时,需要判断是否需要扩容。如果当前元素数量已经等于容量,就调用 reserve
函数扩容。
void push_back(const T& data)
{
if (_finish == _endofstorage) // 若容量不足,则扩容
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = data; // 插入新元素
++_finish;
}
详细解释:
-
push_back
:当_finish == _endofstorage
时,调用reserve
扩容,然后插入新元素。 -
reserve
容量扩充策略:当容器为空时,初次扩容至 4,随后每次扩容翻倍。
3.3、增加迭代器支持
3.3.1、基础迭代器
std::vector
提供了强大的迭代器支持,允许用户使用 for-each
循环来遍历容器。我们可以通过自定义迭代器类,来实现这一功能。
typedef T* iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
详细解释:
-
iterator
:封装了指针,使用默认解引用操作符*
和递增操作符++
,支持for
循环遍历。 -
begin 与 end 函数:返回指向容器起始和末尾的迭代器,类似
std::vector::begin
和std::vector::end
。
3.3.2、常量迭代器
在一些场景中,我们希望容器只能读取而不能修改数据。为此,我们可以实现 const_iterator
来保证只读遍历。
typedef const T* const_iterator;
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
详细解释:
-
const_iterator
:与普通迭代器不同的是,它返回const T*
,确保数据不能被修改。 -
begin 与 end 函数:返回常量迭代器,支持只读遍历。
3.4、删除元素与清空操作
为了使我们的 Vector
容器更加完整,我们还需要支持删除元素和清空容器的操作。
void pop_back()
{
assert(_start < _finish);
--_finish;
}
void clear()
{
_finish = _start;
}
详细解释:
pop_back
函数:移除最后一个元素,减小_finish
,但不改变已分配的容量。clear
函数:将_finish
设为_start
,保留容量但清空元素。
四、扩展功能
4.1、插入与删除
标准库的 std::vector
还支持在中间位置插入和删除元素。我们可以通过移动元素来实现这些功能。
iterator insert(iterator pos, const T& data)
{
assert(pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
// 如果增容,原来的pos就失效了,这里需要重新计算位置
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = data;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos < _finish);
iterator it = pos;
while (it < _finish)
{
*it = *(it + 1);
++it;
}
--_finish;
return pos;
}
详细解释:
insert
函数:在指定位置插入元素,插入后,所有后续元素向后移动一位。erase
函数:删除指定位置的元素,删除后,所有后续元素向前移动一位。
4.2、模板支持
我们通过使用模板,使 Vector
能够存储任意类型的元素。C++ 的模板机制允许我们实现对不同数据类型的支持,而无需为每种类型编写单独的代码。在上面的代码中,我们已经通过 template<typename T>
实现了模板支持。只需传递类型参数,即可为任何类型创建 Vector
容器。
Vector<int> intVector;
Vector<std::string> stringVector;
4.3、使用移动语义与完美转发
在 C++11 中引入的移动语义和完美转发是提升性能的重要工具,特别是在处理对象拷贝时。为了避免不必要的深度拷贝,我们可以使用 std::move
和 std::forward
。
void push_back(T&& data) // 移动版本
{
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = std::move(data); // 使用 move 语义
++_finish;
}
详细解释:
push_back
(移动版本):允许直接移动元素,而不是进行拷贝操作,大大提升了效率。
4.4、 完善边界检查与防止越界访问
为了使容器更加安全,我们可以在访问元素时进行边界检查,防止越界访问导致程序崩溃。
T& at(size_t index)
{
if (index >= size()) {
throw std::out_of_range("Index out of bounds");
}
return _start[index];
}
详细解释:
at
函数:提供安全的数组访问方法,超出范围时抛出std::out_of_range
异常。
五、完整实现代码和测试
5.1、Vector.hpp
新建头文件
Vector.hpp
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace Lenyiin
{
template <class T>
class Vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
// 默认构造
Vector()
: _start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{
}
// 拷贝构造
//Vector(const Vector<T>& v)
//{
// _start = new T[v.capacity()];
// _finish = _start;
// _endofstorage = _start + v.capacity();
// for (size_t i = 0; i < v.size(); i++)
// {
// *_finish = v[i];
// ++_finish;
// }
//}
// 进阶写法
Vector(const Vector<T>& v)
: _start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{
reserve(v.capacity());
for (const auto& e : v)
{
push_back(e);
}
}
// 赋值运算符重载
//Vector<T>& operator=(const Vector<T>& v)
//{
// if (this != &v)
// {
// delete[] _start;
// _start = new T[v.capacity()];
// //memcpy(_start, v._start, sizeof(T) * v.size()); 按字节拷贝,浅拷贝
// for (size_t i = 0; i < v.size(); i++)
// {
// _start[i] = v._start[i]; // 调用的是 operator= 深拷贝
// }
// _finish = _start + v.size();
// _endofstorage = _start + v.capacity();
// }
// return *this;
//}
// 进阶写法
void swap(Vector<T>& v) // 自己写的swap是浅拷贝,代价小
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
Vector<T>& operator=(Vector<T> v)
{
swap(v); // 库里面的swap是深拷贝,代价极大
return *this;
}
// 移动构造函数
Vector(Vector&& v) noexcept
: _start(v._start), _finish(v._finish), _endofstorage(v._endofstorage)
{
v._start = nullptr;
v._finish = nullptr;
v._endofstorage = nullptr;
}
// 移动赋值运算符
Vector& operator=(Vector&& v) noexcept
{
if (this != &v) {
delete[] _start;
_start = v._start;
_finish = v._start + v.size();
_endofstorage = v._start + v.capacity();
v._start = nullptr;
v._finish = nullptr;
v._endofstorage = nullptr;
}
return *this;
}
// 析构函数
~Vector()
{
if (_start)
{
delete[] _start;
}
_start = _finish = _endofstorage = nullptr;
}
// 下标运算符重载
T& operator[](size_t index)
{
if (index >= size())
{
throw std::out_of_range("Index out of range");
}
return _start[index];
}
const T& operator[](size_t index) const
{
if (index >= size())
{
throw std::out_of_range("Index out of range");
}
return _start[index];
}
// 动态扩容
void reserve(size_t newcapacity)
{
if (newcapacity > capacity())
{
T* tmp = new T[newcapacity]; // 分配新内存
size_t len = size();
if (_start)
{
for (size_t i = 0; i < len; i++)
{
tmp[i] = std::move(_start[i]); // 移动已有数据
}
delete[] _start; // 释放旧的内存
}
// 更新指针
_start = tmp;
_finish = tmp + len;
_endofstorage = tmp + newcapacity;
}
}
// resize 不仅会开空间,还会初始化
void resize(size_t newsize, const T& val = T())
{
if (newsize < size())
{
_finish = _start + newsize;
}
else
{
if (newsize > capacity())
{
reserve(newsize);
}
while (_finish < _start + newsize)
{
*_finish = val;
++_finish;
}
}
}
void push_back(const T& data)
{
if (_finish == _endofstorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = data;
++_finish;
}
// 移动版本
void push_back(T&& data)
{
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = std::move(data); // 使用 move 语义
++_finish;
}
// 更安全的访问方式
T& at(size_t index)
{
if (index >= size()) {
throw std::out_of_range("Index out of bounds");
}
return _start[index];
}
void pop_back()
{
assert(_start < _finish);
--_finish;
}
void clear()
{
_finish = _start;
}
iterator insert(iterator pos, const T& data)
{
assert(pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
// 如果增容,原来的pos就失效了,这里需要重新计算位置
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = data;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos < _finish);
iterator it = pos;
while (it < _finish)
{
*it = *(it + 1);
++it;
}
--_finish;
return pos;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
5.2、Vector.cpp
新建测试文件
Vector.cpp
#include "Vector.hpp"
#include <string>
using namespace Lenyiin;
void print_Vector(const Vector<int>& v)
{
Vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
// Vector 容器遍历
void test1()
{
Vector<int> v;
// 插入
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
// 查看容量
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
// 1. 下标运算符 [] 遍历
cout << "1. 下标运算符 [] 遍历 \t\t";
for (size_t i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
// 2. 迭代器遍历
cout << "2. 迭代器遍历 \t\t\t";
Vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 3. const_iterator 迭代器遍历
cout << "3. const_iterator 迭代器遍历 \t";
print_Vector(v);
// 4. 范围 for 遍历
cout << "4. 范围 for 遍历 \t\t";
for (auto& e : v)
{
cout << e << " ";
}
cout << endl;
// 4. 范围 for 遍历 const
cout << "5. 范围 for 遍历 const \t\t";
for (const auto& e : v)
{
cout << e << " ";
}
cout << endl;
}
void test2()
{
Vector<int> 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);
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
// 随即插入
v.insert(v.begin(), 0);
print_Vector(v);
// 删除所有偶数
Vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
{
it++;
}
}
print_Vector(v);
}
void test3()
{
Vector<int> 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);
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
// resize
v.resize(4);
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
v.resize(8);
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
v.resize(15);
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
// 清除
v.clear();
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
// reserve
v.reserve(20);
print_Vector(v);
cout << v.size() << endl;
cout << v.capacity() << endl << endl;;
}
void test4()
{
// 默认构造
Vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
v1.push_back(7);
print_Vector(v1);
// 拷贝构造
Vector<int> v2(v1);
print_Vector(v2);
Vector<int> v3;
v3.push_back(10);
v3.push_back(20);
v3.push_back(30);
v3.push_back(40);
// 赋值
v1 = v3;
print_Vector(v1);
print_Vector(v3);
}
void test5()
{
// 模板
Vector<string> v;
v.push_back("111");
v.push_back("222");
v.push_back("333");
v.push_back("444");
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
//test1();
//test2();
//test3();
test4();
//test5();
return 0;
}
六、总结
通过这篇博客,我们从最基础的存储与管理机制开始,逐步构建了一个强大且灵活的 Vector
容器,具备动态扩容、模板支持、迭代器、常量迭代器、移动语义、边界检查等高级功能,能够媲美 std::vector
。在这一过程中,我们学会了如何高效管理内存、优化性能并保证容器的安全性。此外,我们还增加了随机访问、边界检查、插入删除、异常处理等高级功能。通过这篇文章的学习,你应该对 C++ 容器的内部实现有了更深入的理解,并掌握了构建自定义容器的关键技术。
希望这篇博客对您有所帮助,也欢迎您在此基础上进行更多的探索和改进。如果您有任何问题或建议,欢迎在评论区留言,我们可以共同探讨和学习。更多知识分享可以访问我的个人博客网站 : https://blog.lenyiin.com/ 。本博客所设计的代码也可以访问我的 git 仓库获取 :https://git.lenyiin.com/Lenyiin/Vector