从零实现一个完整的vector类:深入理解C++动态数组
在C++编程中,vector是最常用的容器之一,它提供了动态数组的功能。今天我们将从实现一个相对完整的vector类来深入理解vector的封装与使用,并在过程中解决各种实际问题。
整体代码框架
首先,让我们看看vector类的整体结构:(用这份伪代码进行理解性默写手撕可以加深对vector的理解)ps:完整源码在文章最下方
cpp
namespace ddd
{
template<class T>
class vector
{
public:
// 迭代器相关
typedef T* iterator;
typedef const T* const_iterator;
// 构造函数和析构函数
vector();
vector(int n, const T& val = T());
template<class InputIterator>
vector(InputIterator first, InputIterator last);
vector(const vector<T>& v);
vector<T>& operator=(vector<T> v);
~vector();
// 容量相关
size_t size() const;
size_t capacity() const;
void reserve(size_t n);
void resize(size_t n, const T& val = T());
// 元素访问
T& operator[](size_t pos);
const T& operator[](size_t pos) const;
// 修改操作
void push_back(const T& x);
void pop_back();
void swap(vector<T>& v);
iterator insert(iterator pos, const T& x);
iterator erase(iterator pos);
// 迭代器方法
iterator begin();
iterator end();
const_iterator cbegin() const;
const_iterator cend() const;
private:
iterator _start; // 指向数据块的开始
iterator _finish; // 指向有效数据的尾
iterator _endOfstorage; // 指向存储容量的尾
};
}
构造函数:创建vector的不同方式
默认构造函数
cpp
vector() :
_start(nullptr)
, _finish(nullptr)
, _endOfstorage(nullptr)
{}
最简单的构造函数,用初始化列表创建一个空的vector,所有指针都初始化为nullptr。
指定大小和初始值的构造函数
cpp
vector(int n, const T& val = T()) {
reserve(n);
for(size_t i = 0; i < n; i++) {
push_back(val);
}
}
这个构造函数创建包含n个元素的vector,每个元素都初始化为val。如果val没有提供,就使用T类型的默认值(缺省参数)。
可能看不懂T():
T()表示调用模板参数T类型的默认构造。
迭代器范围构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last) {
while(first != last) {
push_back(*first);
++first;
}
}
关键理解:比方说我们有一个数组arr,里面有arr[1,2,3,4,5]。
我们可以不用知道*first具体是什么,只需利用模板的威力:编译时只需传入对应指针,它就会为每种迭代器生成对应的代码,例如传入(arr,arr+5),它就自动将arr的内容用于构造一个内容基本一致的vector。
这个构造函数可以从任何迭代器范围创建vector,比如数组、list等其他容器的迭代器。
拷贝构造函数
cpp
vector(const vector<T>& v) {
reserve(v.capacity());
for(auto& e : v) {
push_back(e);
}
}
创建一个与参数v相同的vector,需要分配相同大小的内存并复制所有元素。
用法示例:v1(v2);即使用v2的内容直接构造一个对象v1。
这一个构造类似于赋值
析构函数:资源清理
cpp
~vector() {
delete[] _start;
_start = _finish = _endOfstorage = nullptr;
}
析构函数负责释放vector动态分配的内存,并将所有指针设为nullptr,防止悬空指针。
迭代器:让vector支持范围遍历
cpp
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator cbegin() const { return _start; }
const_iterator cend() const { return _finish; }
end()返回的是_finish,而_finish指向最后一个元素的下一个位置(也就是说_finish和end()本质差不多)
容量操作:管理内存大小
获取大小和容量
size_t size() const { return _finish - _start; }
size_t capacity() const { return _endOfstorage - _start; }
通过指针相减来计算元素个数和总容量,既简单又高效。
size()可以理解为表示当前vector当中存储的数据量,capacity()表示vector可以存储的最大量
预留空间
cpp
void reserve(size_t n) {
if (n > capacity()) {
T* tmp = new T[n];
size_t sz = size();
for(size_t i = 0; i < sz; i++) {
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = _start + sz;
_endOfstorage = _start + n;
}
}
关键点:只有在请求的容量大于当前容量时才重新分配内存,避免不必要的(过多的)内存操作。
注意此操作会导致_start的位置有所改变。(此处不用纠结,后面会再次提到)
调整大小
cpp
void resize(size_t n, const T& val = T()) {
if (n <= size()) {
_finish = _start + n; // 缩小size
}
else {
reserve(n); // 扩大容量
while (_finish < _start + n) {
*_finish = val; // 用val填充新增位置
++_finish;
}
}
}
resize可以增大或减小vector的大小,新增的元素用val初始化。
如果说要增大空间,我们就直接复用reserve()
元素访问:像数组一样使用vector
cpp
T& operator[](size_t pos) {
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos)const {
assert(pos < size());
return _start[pos];
}
通过重载[]运算符,vector可以像普通数组一样使用。assert确保不会越界访问,是一种强制检查,越界就直接报错并指出报错位置,也方便调试。
修改操作:动态改变vector内容
添加和删除元素
cpp
void push_back(const T& x) {
insert(end(), x); // 在末尾插入
}
void pop_back() {
erase(end() - 1); // 删除最后一个元素
}
在类内部,我们可以直接使用end()等价于_finish,因为end()返回的就是_finish。
插入元素
cpp
iterator insert(iterator pos, const T& x) {
assert(pos >= _start && pos <= _finish);
if (_finish == _endOfstorage) {
size_t len = pos - _start; // 记录相对位置
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len; // 重新计算位置
}
iterator end = _finish - 1;
while (end >= pos) {
*(end + 1) = *end; // 向后移动元素
--end;
}
*pos = x; // 插入新元素
++_finish;
return pos;
}
关键技巧:在扩容前用len记录插入位置的相对偏移量,然后再用新的_start+len来定位新的pos。原因:因为reserve会改变_start的地址,使原来的迭代器失效。
删除元素
cpp
iterator erase(iterator pos) {
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it < _finish) {
*(it - 1) = *it; // 向前移动元素
++it;
}
--_finish;
return pos;
}
注意:erase的断言条件与insert不同,因为不能删除end()位置(它没有元素)。
交换操作
cpp
void swap(vector<T>& v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endOfstorage, v._endOfstorage);
}
交换操作只需交换三个指针,非常高效,常用于实现拷贝赋值运算符。
拷贝赋值运算符
cpp
vector<T>& operator= (vector<T> v) {
swap(v);
return *this;
}
巧妙设计:使用传值参数,让编译器自动创建临时副本,然后通过交换内容来实现赋值,既简单又安全。
测试我们的vector
cpp
void test_vector() {
// 测试各种构造函数
ddd::vector<int> v1;
ddd::vector<int> v2(5, 10);
int arr[] = {1, 3, 5, 7, 9};
ddd::vector<int> v3(arr, arr + 5);
// 测试修改操作
v3.push_back(11);
v3.insert(v3.begin() + 2, 99);
v3.erase(v3.begin() + 1);
// 测试容量操作
v3.resize(10, 100);
// 测试迭代器
for(auto it = v3.begin(); it != v3.end(); ++it) {
std::cout << *it << " ";
}
}
int main() {
test_vector();
return 0;
}
总结
通过从头实现vector,我们深入理解了:
-
动态内存管理:new/delete的使用和内存分配策略
-
迭代器失效:插入删除操作对迭代器的影响
-
模板编程:如何编写通用的容器类
-
异常安全:通过swap技巧实现安全的赋值操作
-
性能优化:合理的扩容策略和内存管理
完整源码:
namespace ddd
{
template<class T>
class vector
{
public:
// Vector的迭代器是一个原生指针
typedef T* iterator;
typedef const T* const_iterator;
iterator begin(){
return _start;
}
iterator end(){
return _finish;
}
const_iterator cbegin() const{
return _start;
}
const_iterator cend() const{
return _finish;
}
// construct and destroy
vector(){
}
vector(int n, const T& val = T()){
reserve(n);
for(size_t i=0;i<n;i++){
push_back(val);
}
}
template<class InputIterator>
vector(InputIterator first, InputIterator last){
while(first!=last){
push_back(*first);
++first;
}
}
vector(const vector<T>& v){
reserve(v.capacity());
for(auto& e:v){
push_back(e);
}
}
vector<T>& operator= (vector<T> v){
swap(v);
return *this;
}
~vector(){
delete[] _start;
_start=_finish=_endOfstorage=nullptr;
}
// capacity
size_t size() const{
return _finish-_start;
}
size_t capacity() const{
return _endOfstorage-_start;
}
void reserve(size_t n) {
if (n > capacity()) {
T* tmp = new T[n];
size_t sz = size();
if (_start) {
for (size_t i = 0; i < sz; i++) {
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endOfstorage = _start + n;
}
}
void resize(size_t n, const T& val = T()) {
if (n <= size()) {
_finish = _start + n;
}
else {
reserve(n);
while (_finish < _start + n) {
//多余元素赋值(多出来的空间初始化)
*_finish = val;
++_finish;
}
}
}
T& operator[](size_t pos) {
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
void push_back(const T& x) {
insert(end(), x);
}
void pop_back() {
erase(end() - 1);
}
void swap(vector<T>& v){
std::swap(_start,v._start);
std::swap(_finish,v._finish);
std::swap(_endOfStoratge,v._endOfStorage);
}
iterator insert(iterator pos, const T& x) {
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endOfstorage) {
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}//容量不足扩容
//需要用len记录位置,是因为reserve后会改变原型_start的位置
iterator end = _finish - 1;
while (end >= pos) {
*(end + 1) = *end;
--end;
}//向后挪动覆盖,腾出空间插入
*pos = x;
++_finish;
}
iterator erase(iterator pos) {
assert(pos >= _start);
assert(pos < _finish);//此处没有=号了,因为insert是可以尾插,而erase必须有元素可删除
iterator it = pos + 1;
while (it < end()) {
*(it -1 ) = *it;
++it;
}//向前挪动覆盖
--_finish;
return pos;
}
private:
iterator _start; // 指向数据块的开始
iterator _finish; // 指向有效数据的尾
iterator _endOfStorage; // 指向存储容量的尾
};
}

1876

被折叠的 条评论
为什么被折叠?



