C++入门自学Day10-- Vector类的自实现
往期内容回顾
Vector类(注意事项)
初识Vector
String类的自实现
String类的使用(续)
String类(续)
String类(初识)
前言:
在 C++ 开发中,std::vector 是最常用的容器之一,它提供了动态数组的功能,兼顾了高效的随机访问和动态扩容的便利性。然而,我们平时只是调用它的 API,很少真正去思考它背后的实现原理。
自己动手实现一个简化版的 vector,不仅能帮助我们理解 动态数组 的内存管理机制,还能让我们更深入地掌握 模板编程、构造与析构、迭代器、异常安全 等 C++ 核心知识。
主要实现内容介绍
我们将一步步实现一个简化版的 vector,功能包括:
-
动态内存管理(模拟 new / delete)
-
构造与析构(支持对象类型存储)
-
元素访问与修改(operator[]、at())
-
插入与删除(push_back()、pop_back()、insert()、erase())
-
容量管理(size()、capacity()、reserve()、resize())
-
迭代器支持(基本指针型迭代器)
一、vector类型实现介绍
1. 基本类框架与成员变量
我们的 vector 需要三个关键数据:
指向首元素的头指针:_start; 指向数据末尾的_finish指针;指向容量底端的_endofstorage指针;
template<typename T> class myvector{public:myvector():_start(nullptr),_finish(nullptr),_endofstorage;{};~myvector(){delete[] _start;_start = nullptr;_finish = nullptr;_endofstorage = nullptr;}size_t size()const{return _finish - _start;}size_t capacity() const{return _endofstorage - _start;}private:T* _start;T* _finish;T* _endofstorage; }
基本成员变量:
基本的成员函数:构造函数,析构函数,返回vector容量,尺寸大小,
为什么 size() 和 capacity()都需要 const
这两个函数只是 查询对象状态(读取 _start, _finish, _endofstorage),不会修改对象内部数据。
为了符合 接口语义,保证常量对象也可以查询状态,必须加上 const。
测试函数:
void test1(){myvector<int> v1;cout<<v1.size()<<endl;cout<<v1.capacity()<<endl; }
2、动态扩容:reserve 和resize函数
这里们提供了reserve 函数修改自定义vertor的容量,resize修改尺寸大小
void reserve(size_t n){size_t sz = size();if(n>capacity){T* tmp = new T[n];if(_start){std::copy(_start,_finish,tmp);delete[] _start;}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;}return; } void resize(size_t n,const T& val = T()){if(n <= size()){_finish = _start + n;}else{if(n > capacity){reserve(n);}while (_finish < _start +n){*_finish = val;_finish++;}} }
测试函数如下:
void test2(){myvector<string> v1;v1.reserve(8);cout<<"capacity = "<<v1.capacity()<<endl;v1.push_back("aaa");v1.push_back("bbb");v1.push_back("cccc");v1.resize(8,"d");Print_vector(v1); }
输出描述:
capacity = 8
aaa bbb cccc d d d d d
3、迭代器和循环的实现
typedef T* iterator; typedef T* const_iterator; iterator begin(){return _start; } iterator end(){return _finish; } const_iterator begin()const{return _start; } const_iterator end()const{return _finish; }template<class T> void Print_vector(const myvector<T>& v){typename myvector<T>::const_iterator it = v.begin();while (it != v.end()){cout<<*it<<" ";it++;}cout<<endl; }
注意迭代器有两种,一种是可修改的迭代器,这种迭代器保证元素是可修改的。
另外一个就是加const修饰的迭代器,元素无法修改后。
测试函数:
void test1(){myvector<int> v1;v1.push_back(1);v1.push_back(3);v1.push_back(4);myvector<int>:: iterator it = v1.begin();while (it != v1.end()){cout<<*it<<" ";it++;}cout<<endl;}
输出描述:
1 3 4;
4、 插入与删除
pushback,popback, erase, Insert,函数实现:
void push_back(const T& val){if(_finish == _endofstorage){size_t Capa = (capacity() == 0)? 2 : capacity()*2;reserve(Capa);}*_finish = val;_finish++; } void pop_back(){assert(_start != _finish);_finish--; }iterator erase (iterator pos, iterator first, iterator last){assert(pos<last && pos>=first);// iterator end = _finish-1;while (pos != _finish-1){*(pos) = *(pos+1);pos++;}--_finish;return _start; } void insert(iterator pos,const T& val){assert(pos<=_finish);size_t num = pos - _start;if(_finish == _endofstorage){size_t newcapacity = (capacity() == 0)?2:2*capacity();reserve(newcapacity);}iterator end = _finish;pos = _start+num;while (end > pos){*end = *(end-1);end--;}*pos = val;++_finish; }
这里我们实现了关于vector类型的尾插,任意位置插入,删除的成员函数
测试函数:
void test5(){myvector<string> v1;v1.push_back("aaa");v1.push_back("bbb");v1.push_back("cccc");Print_vector(v1);v1.pop_back();Print_vector(v1);v1.insert(v1.begin()+2,"e");Print_vector(v1);v1.insert(v1.begin(),"hello");Print_vector(v1);v1.erase(v1.begin(),v1.begin(),v1.end());Print_vector(v1); }
输出描述:
aaa bbb cccc
aaa bbb
aaa bbb e
hello aaa bbb e
aaa bbb e
5、拷贝构造以及赋值运算符重载
//拷贝构造和赋值运算符 myvector(const myvector<T>& v){size_t capa = v.capacity();size_t sz = v.size();T* tmp = new T[capa];std::copy(v._start,v._finish,tmp);_start = tmp;_finish = _start+sz;_endofstorage = _start + capa; } T& operator [](size_t pos){return *(_start+pos); } const T& operator [](size_t pos)const{return *(_start+pos); } T& at(size_t index) {if (index >= size()) throw std::out_of_range("Index out of range");return _start+index; }; void swap(myvector<T>& v){::swap(_start,v._start);::swap(_finish,v._finish);::swap(_endofstorage,v._endofstorage); } myvector<T>& operator=(myvector<T> v){swap(v);return *this; }
这里我们实现了常用的运算符重载函数和拷贝构造。注意这里实现赋值运算符的方式,一定要注意深浅拷贝的区分
测试函数:
void test6(){myvector<string> v1;v1.push_back("aaa");v1.push_back("bbb");v1.push_back("cccc");Print_vector(v1);myvector<string> v2(v1);Print_vector(v2);myvector<string> v3;v3 = v2;Print_vector(v3); }
输出描述:
aa bbb cccc
aaa bbb cccc
aaa bbb cccc
二、基于vector类型的自实现谈谈“深浅拷贝”
1. 什么是浅拷贝(Shallow Copy)
-
浅拷贝只是 复制对象的内存内容,不会为对象内部的动态资源(如 new 分配的数组)单独分配新空间。
-
以自实现 vector 为例:
template<typename T> class MyVector {T* _start;T* _finish;T* _endofstorage; };
如果用 memcpy:
MyVector<int> v2; memcpy(&v2, &v1, sizeof(MyVector<int>));
发生了什么?
v2._start = v1._start
两个对象共享同一块数组内存
这就是浅拷贝
如果我们的类型不是自定义类型,假设我们的类型为string,string里面存储了字符串首元素的地址在进行memcpy,会把地址拷过去,导致两个vector的string指向同一块内存,释放内存时相当于二次释放。
2. 浅拷贝的问题
双重释放(Double Free)
当 v1 和 v2 析构时,它们都会 delete[] _start
同一块内存被释放两次 → 未定义行为,程序崩溃
修改数据互相影响
v2._start[0] = 100; std::cout << v1._start[0]; // v1 的数据也变了
原本想要复制对象得到独立数据,但实际被共享 → 破坏封装性
不调用对象构造/析构
memcpy 只是按字节拷贝,不会调用 T 类型的构造函数、拷贝构造函数或析构函数
对于非 POD 类型(比如 std::string、自定义对象)会导致严重内存错误
3. 为什么不提倡在 C++ 中使用 memcpy复制对象
原因
解释
只做字节拷贝
不会调用构造/析构函数,容易破坏对象内部状态
浅拷贝问题
对象内部的指针或资源会被共享,导致双重释放或数据污染
类型安全
memcpy 没有类型安全,容易忽略对复杂类型的深拷贝需求
难以维护
对象结构变化时,memcpy 可能会导致逻辑错误
4. 正确做法
使用拷贝构造函数或者标准库函数:
T* new_data = new T[v.size()]; std::copy(v._start, v._finish, new_data);
每个对象独立管理自己的内存
对非 POD 类型,std::copy 会调用元素的拷贝构造函数
安全且符合 C++ 面向对象语义
5. 总结
-
memcpy 对自实现 vector 会导致 浅拷贝问题:
-
内存共享 → 双重释放
数据修改互相影响不调用构造/析构函数 → 非 POD 类型危险 -
C++ 中 推荐使用拷贝构造、赋值运算符和标准算法 来做对象复制
-
memcpy 仅适合 POD 类型的原始内存块,比如 int arr[100] 或 char buf[256]
【面试题】
数据结构中我们还有list, 已经有了vector为什么会有list呢?vector的缺点?
1、 Vector 的缺点
-
1、在中间插入或删除元素慢。
-
需要移动插入/删除位置后的所有元素,复杂度 O(n)。
-
举例:在 vector 中间插入元素,需要从插入点到末尾每个元素向后移动。
-
2、容量扩展可能触发拷
-
当 vector 扩容时,需要分配新内存并拷贝全部元素。
-
对于大对象或者对象频繁移动的场景成本高。
-
3、迭代器/指针失效问题
-
插入或删除可能使迭代器、指针失效,需要小心管理。
-
4、内存连续:
-
对于超大数据,如果连续内存不足,会分配失败。
2、List(双向链表)的特点
-
每个节点独立存储,前后节点通过指针链接
-
优点:
-
在中间插入/删除元素 O(1),只需修改指针,不移动其他元素。
-
插入删除操作不会触发大规模元素移动。
-
-
缺点:
-
随机访问慢:list[i] 必须从头遍历,时间复杂度 O(n)。
-
空间开销大:每个节点需要存储额外的前/后指针。
-
CPU 缓存利用差:节点不连续,缓存命中率低。
-
操作类型 | vector | list |
---|---|---|
随机访问 | ✅ O(1) | ❌ O(n) |
尾部插入 | ✅ 平摊 O(1) | ✅ O(1) |
中间插入/删除 | ❌ O(n) | ✅ O(1) |
空间/缓存效率 | ✅ 高 | ❌ 低 |