造轮子记:从零开始实现一个简易Vector
一、引言:从一段“优雅”的代码开始
最近在学习 std::vector 的底层机制时,我看到一位大佬写的 Vector 实现,
代码风格极其优雅,几乎能直接当作 STL 的简化版阅读。
大佬代码如下:
#include <algorithm>
#include <iostream>
#include <sstream>
#include <vector>
using namespace std;
template <typename T>
class MyVector {
private:
T *_data;
size_t _size;
size_t _capacity;
void check_range(size_t index) {
if (index >= _size) {
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
}
public:
class iterator {
private:
T *ptr;
public:
using value_type = T;
using iterator_category = random_access_iterator_tag;
using difference_type = ptrdiff_t;
using pointer = T*;
using reference = T&;
iterator() {}
iterator(T *ptr) : ptr(ptr) {}
T &operator*() const { return *ptr; }
T *operator->() const { return ptr; }
iterator &operator++() {
ptr++;
return *this;
}
iterator operator++(int) {
iterator temp = *this;
ptr++;
return temp;
}
iterator &operator--() {
ptr--;
return *this;
}
iterator operator--(int) {
iterator temp = *this;
ptr--;
return temp;
}
iterator operator+(int n) const { return iterator(ptr + n); }
iterator operator-(int n) const { return iterator(ptr - n); }
int operator-(const iterator &other) const { return ptr - other.ptr; }
bool operator<(const iterator &other) const { return ptr < other.ptr; }
bool operator==(const iterator &other) const { return ptr == other.ptr; }
bool operator!=(const iterator &other) const { return ptr != other.ptr; }
};
MyVector() : _data(nullptr), _size(0), _capacity(0) {}
MyVector(size_t n, const T &init = T())
: _data(new T[n]), _size(n), _capacity(n) {
for (size_t i = 0; i < n; i++) _data[i] = init;
}
MyVector(const MyVector<T> &other)
: _data(new T[other.size()]),
_size(other.size()),
_capacity(other.size()) {
for (size_t i = 0; i < other.size(); i++) _data[i] = other[i];
}
MyVector(const initializer_list<T> &init)
: _data(new T[init.size()]), _size(0), _capacity(init.size()) {
for (T x : init) _data[_size++] = x;
}
~MyVector() { delete[] _data; }
T &operator[](size_t index) { return _data[index]; }
const T &operator[](size_t index) const { return _data[index]; }
T &at(size_t index) {
check_range(index);
return _data[index];
}
const T &at(size_t index) const {
check_range(index);
return _data[index];
}
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
void reserve(size_t new_capacity) {
if (new_capacity > _capacity) {
_capacity = new_capacity;
T *new_data = new T[_capacity];
for (size_t i = 0; i < _size; i++) new_data[i] = _data[i];
delete[] _data;
_data = new_data;
}
}
void push_back(const T &elem) {
if (_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
_data[_size++] = elem;
}
void resize(size_t new_size, const T &init = T()) {
if (new_size > _capacity) reserve(new_size);
for (size_t i = _size; i < new_size; i++) _data[i] = init;
_size = new_size;
}
void pop_back() {
if (_size == 0) throw out_of_range("can not pop_back empty vector");
_size --;
}
void clear() { _size = 0; }
iterator begin() const { return iterator(_data); }
iterator end() const { return iterator(_data + _size); }
void insert(const iterator &pos, T x) {
size_t index = pos - begin();
if (_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
for (size_t i = _size; i > index; i --) _data[i] = _data[i - 1];
_data[index] = x;
_size ++;
}
void erase(const iterator &pos) {
for (size_t i = pos - begin(); i < _size - 1; i ++) {
_data[i] = _data[i + 1];
}
_size --;
}
void erase(const iterator &start, const iterator &end) {
int diff = end - start;
for (size_t i = start - begin(); i < _size - diff; i ++) {
_data[i] = _data[i + diff];
}
_size -= diff;
}
MyVector<T>& operator=(const MyVector<T> &other) {
if (this != &other) {
delete[] _data;
_data = new T[other.size()];
_size = other.size();
_capacity = other.capacity();
for (size_t i = 0; i < _size; i ++) _data[i] = other[i];
}
return *this;
}
T &front() { return _data[0]; }
const T&front() const { return _data[0]; }
T &back() { return _data[_size - 1]; }
const T&back() const { return _data[_size - 1]; }
bool empty() const { return _size == 0; }
template<typename... Args>
void emplace_back(const Args&... args) {
if (_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
_data[_size++] = T(args...);
}
};
int main() {
}
这段代码对 Vector 的实现启发了我:要想真正理解标准库,不如自己动手写一个出来。
于是我开始了一场从零造轮子的实践 —— 手写一个自己的 MyVec,一边模仿学习,一边分析 std::vector 背后的设计。
二、思路
1. 模板 & 初始化
直接按数据类型去写发现有很多重复部分,如下列代码
class MyVec_int{
int* data;
size_t _size;
};
class MyVec_double{
double* data;
size_t _size;
};
class MyVec_char{
char* data;
size_t _size;
};
...
int main(){
MyVec_int vec_int;
MyVec_double vec_double;
MyVec_char vec_char;
...
return 0;
}
这时候就可以引入模板。
模板:一次编写,到处复用。起一个占位符作用,由编译器去帮助生成重复代码。
注意到不同模版下只是数据类型不同,我们可以引入c++中的模版template
此处T为占位符,根据类型灵活调整
template <typename T>
class MyVec{
private:
T* _data; // 指向动态数组首元素的指针
size_t _size; // 当前已存储的元素个数,模仿c++官方风格,设计成size_t,无符号longlong类型,足够大
};
2. 构造函数
- 构造函数 (Constructor): 默认构造、带大小的构造、列表初始化等。
常见的一些申请构造形式:
// vector(size_t n, T init = T() )
// 有大小 有初始值
vector<int> arr(10);
vector<int> arr(10, 1);
//多维数组 vector(vector<T> &other)
vector<int> a(arr);
//拷贝数组操作
vector<int> a = arr;
// 列表初始化
vector<int> arr = {1, 2, 3, 4, 5};
vector<int> arr{1, 2, 3, 4, 5};
我们去尝试着实现它们
template <typename T>
class MyVec{
private:
T* _data; // 指向动态数组首元素的指针
size_t _size; // 当前已存储的元素个数
public:
//当构造函数没有复杂逻辑时,只有对变量的赋值时,可以采取以下的简练写法
MyVec() : _data(nullptr), _size(0) {} //默认构造函数
// 带大小的构造函数
MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = init;
}
}
// 拷贝构造函数
MyVec(MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = other[i];
}
}
// 列表初始化的构造函数
MyVec(initializer_list<T> init) : _data(new T [init.size()]), _size(init.size()) {
size_t i = 0;
for(T x : init){
_data[i++] = x;
}
}
};
3. 下标访问
operator[]: “实现下标访问,通常不进行边界检查。”at(): “提供边界检查的下标访问,若索引超出范围,抛出std::out_of_range异常。”
//进行重构[]和at
template <typename T>
class MyVec{
private:
T* _data; // 指向动态数组首元素的指针
size_t _size; // 当前已存储的元素个数
public:
MyVec() : _data(nullptr), _size(0) {}
// 带大小的构造函数
MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = init;
}
}
// 拷贝构造函数
MyVec(MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = other[i];
}
}
// 列表初始化的构造函数
MyVec(initializer_list<T> init) : _data(new T [init.size()]), _size(init.size()) {
size_t i = 0;
for(T x : init){
_data[i++] = x;
}
}
// 释放内存
~MyVec() { delete[] _data; }
// &保证成功修改(引用地址)
T& operator[](size_t index) {
return _data[index];
}
// at(),提供边界检查的下标访问
T& at(size_t index) {
if(index >= _size){
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
return _data[index];
}
size_t size() {
return _size;
}
};
4. 动态扩容 (Reallocation)
数组是一段内存连续的空间 data[index] -> date + index
那么请思考:
vector是如何做到可以不断的增加元素,依旧保持数组内存连续这一特性的?
指针? 指针指来指去把内存连接起来?指针连起的内存并不连续,通过下标访问,通过指针连接起来,逻辑会非常复杂,并非好的选项。
动态扩容? 概念很精辟,但有些抽象,下面开始探究这个抽象概念,尝试去弄清楚。
std::vector 的容量与扩容
size():当前已存储元素个数capacity():底层数组当前分配的总空间- 当
size() == capacity()时,vector会自动申请更大的连续内存块,并搬移已有元素。
扩容策略
- 常见实现(libstdc++)采用倍增策略:新容量约为旧容量的 2 倍。
- 这样做能让插入的均摊时间复杂度为 O(1)。
可视化结果展示

模拟动态扩容时所涉及的一些函数:
int capacity() return _capacity;
void reserve(size_t new_capacity);//预留空间
void push_back(T elem);//内存不足便申请空间
void resize(size_t new_size, T init = T());// 改变size大小, 超出部分用init填充
void pop_back();// size --
void clear();// size = 0
实现:
template <typename T>
class MyVec{
private:
T* _data; // 指向动态数组首元素的指针
size_t _size; // 当前已存储的元素个数
// 加入容量部分
size_t _capacity; // 当前分配的内存容量
// 为先前代码加入capacity部分
public:
MyVec() : _data(nullptr), _size(0), _capacity(0) {}
// 带大小的构造函数
MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n), _capacity(n) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = init;
}
}
// 拷贝构造函数
MyVec(MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()), _capacity(other.size()) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = other[i];
}
}
// 列表初始化的构造函数
MyVec(initializer_list<T> init) : _data(new T [init.size()]), _size(init.size()), _capacity(init.size()) {
size_t i = 0;
for(T x : init){
_data[i++] = x;
}
}
// 析构函数释放内存
~MyVec() { delete[] _data; }
// &保证成功修改(引用地址)
T& operator[](size_t index) {
return _data[index];
}
// 提供边界检查的下标访问
T& at(size_t index) {
if(index >= _size){
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
return _data[index];
}
size_t size() {
return _size;
}
size_t capacity() {
return _capacity;
}
void reserve(size_t new_capacity) {
if(new_capacity > _capacity){
_capacity = new_capacity;
T* new_data = new T[_capacity];
for(size_t i = 0; i < _size; ++i){
new_data[i] = std::move(_data[i]);
}
delete[] _data;
_data = new_data;
}
}
// 这里采用了倍增扩容策略,确保 push_back 的 摊还时间复杂度为 O(1)。
void push_back(T elem) {
if(_size == _capacity){
// 扩容处理
reserve(_capacity == 0 ? 1 : _capacity * 2);
}
_data[_size++] = elem;
}
void resize(size_t new_size, T init = T()) {
if(new_size > _size) reserve(new_size);
for(size_t i = _size; i < new_size; ++i){
_data[i] = init;
}
_size = new_size;
}
void pop_back() {
if(_size == 0){
throw out_of_range("pop_back: vector is empty");
}
--_size;
}
void clear() {
_size = 0;
}
};
5. 常量const
引用传参&
现做出演示:
为方便我们观察情况 为每部分输出相应内容
我们发现调用函数时,MyVec发生了拷贝
为func函数加上&,再次观察

再次去测试临时变量,发生报错

报错分析
参数类型默认为左值引用,它只能绑定到左值(有名字、可被再次引用的对象),不能绑定到右值(临时对象、匿名值)。
因此,编译器拒绝将右值 {1,23,4} 传递给左值引用参数。
小结
拿拷贝构造函数做出小结
| 拷贝构造形式 | 能否接受左值 | 能否接受右值(临时对象) | 说明 |
|---|---|---|---|
MyVec(MyVec<T>& other) | ✅ | ❌ | 仅左值可绑定 |
MyVec(const MyVec<T>& other) | ✅ | ✅ | 常用写法 |
MyVec(MyVec<T>&& other) | ❌ | ✅ | 移动构造函数(C++11) |
为func函数加上const,再次观察

为什么加上const就可以正常调用了?
这是因为 const 左值引用可以绑定到右值。
C++ 标准特意允许这种绑定,以便可以高效地传递临时对象而不发生拷贝。
而临时对象的生命周期会被延长到函数调用结束
tip:
当写成func(MyVec&&)时 会变为右值引用
右值引用下临时变量反而可以正常运行


做出表格:
| 调用形式 | MyVec<int>& | const MyVec<int>& | MyVec<int>&& |
|---|---|---|---|
MyVec<int> v; func(v); | ✅ | ✅ | ❌ |
func(MyVec<int>{1,2,3}); | ❌ | ✅ | ✅ |
func({1,2,3}); | ❌ | ✅ | ✅ |
综上可分析得出:
- &可避免大数据产生多次复制
- const可以实现临时变量的引用
根据对于const的学习,我们可以进一步完善我们的构造函数,运行不会发生改变内部数据的函数也可以根据const去完善
// 拷贝构造函数
MyVec(const MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()), _capacity(other.size()) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = other[i];
}
}
// 列表初始化的构造函数
MyVec(const initializer_list<T> &init) : _data(new T [init.size()]), _size(init.size()), _capacity(init.size()) {
size_t i = 0;
for(T x : init){
_data[i++] = x;
}
}
size_t size() const{
return _size;
}
size_t capacity() const {
return _capacity;
}
尝试在函数里输出数据,发现报错

const 对象无法调用非常量成员函数
在自定义类中,如果某个成员函数没有声明为
const,那么常量对象就无法调用它。举例:
const MyVec<int> a = {1, 2, 3}; cout << a[0]; // ❌ 报错:不能调用非常量 operator[]原因在于编译器会将
a的类型视作const MyVec<int>*,此时只能调用被标记为const的成员函数。STL 的
std::vector内部提供了两套下标访问接口:T& operator[](size_t); const T& operator[](size_t) const;这样既能支持非常量访问(读写),也能支持常量访问(只读)。
-
为什么要两套:
- 非 const 版本:可修改对象内容
- const 版本:允许常量对象访问元素(只读),保证编译器安全性
-
对应 STL:
std::vector也是这样做的,保证了左值/右值、常量/非常量对象都能使用
向标准学习,我们也去写两套下标访问接口
// &保证成功修改(引用地址)
T& operator[](size_t index) {
return _data[index];
}
// &保证成功修改(引用地址)
const T& operator[](size_t index) const {
return _data[index];
}
同时去更新 at 操作
T& at(size_t index) {
if(index >= _size){
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
return _data[index];
}
const T& at(size_t index) const {
if(index >= _size){
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
return _data[index];
}
6. 迭代器
平常写代码时,我们总会利用到迭代器,例如
// 利用迭代器访问 vector 中的所有元素
for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++){
cout << *iter << endl;
}
// 利用迭代器访问 map 中的所有元素
for(map<int, int>::iterator iter = mp.begin(); iter != mp.end(); iter++){
cout << iter->first << '' << iter->second << endl;
}
//排序 vector
sort(vec.begin(), vec.end());
// 获取最大元素
vector<int>::iterator iter = max_element(vec.begin(), vec.end());
cout << *iter << endl;
// 寻找 >=target 的第一个元素
vector<int>::iterator iter = lower_bound(vec.begin(), vec.end(), target);
cout << iter - vec.begin() << endl;
当然平常大多数时候我们会写 auto,让程序自动识别迭代器。
那么请思考:
迭代器实质是个什么?
是指针吗?不太确切,严格来说它是一个 行为类似指针的类(或结构),封装了对容器内部元素的访问方式,不同底层结构的容器我们可以用相同的语法去访问
for(auto it = c.begin(); it != c.end(); ++it){}
迭代器的好处是什么?
让整个stl容器非常统一,封装成迭代器可以让容器们用相同方式去访问使用,泛型算法也可以相应被使用
为实现上述例子,我们先去更新 begin() 和 end() 两个迭代器,并重载 ++ , != ,*。让编译器在 iteratror 认识这些符号
同时更新前置++与后置++。了解这个++过程后,我们也明白为什么非基础类型++要尽可能前置。
template <typename T>
class MyVec{
// 上述内容暂时省略
class iterator{
private:
T* ptr;//底层是个指针,并封装
public:
iterator() : ptr(nullptr) {}
iterator(T* ptr) : ptr(ptr) {}
bool operator!=(const iterator& other){
return ptr != other.ptr;
}
//++iter
iterator& operator++(){
ptr++;
return *this;
}
//iter++(empty)
iterator operator++(int){
iterator temp = *this;
ptr++;
return temp;
}
T& operator*() const { return *ptr; }
};
iterator begin(){
return iterator(_data);
}
iterator end(){
return iterator(_data + _size);
}
};
经过更新后就可以利用迭代器去访问元素了

注: 支持begin(), end(), ++ , != 就可以利用范围循环了
我们去尝试使用算法,这里以 sort 为例, 直接尝试发现出现一堆报错, 报错内容非常长这里就先不放了
大致就是相关运算符缺失
我们去更新算法相关的操作符
template <typename T>
class MyVec{
// 上述内容暂时省略
class iterator{
private:
T* ptr;//底层是个指针,并封装
public:
using value_type = T;// STL算法依赖此类型定义
iterator() : ptr(nullptr) {}
iterator(T* ptr) : ptr(ptr) {}
//++iter
iterator& operator++(){
ptr++;
return *this;
}
//iter++(empty)
iterator operator++(int){
iterator temp = *this;
ptr++;
return temp;
}
//--iter
iterator& operator--(){
ptr--;
return *this;
}
//iter--(empty)
iterator operator--(int){
iterator temp = *this;
ptr--;
return temp;
}
iterator operator+(int n) const { return iterator(ptr + n); }
iterator operator-(int n) const { return iterator(ptr - n); }
iterator& operator+=(int n) { ptr += n; return *this; }
iterator& operator-=(int n) { ptr -= n; return *this; }
int operator-(const iterator& other) const {
return ptr - other.ptr;
}
//做比较的相关操作符,比较指针地址
bool operator==(const iterator& other) const { return ptr == other.ptr; }
bool operator<(const iterator& other) const { return ptr < other.ptr; }
bool operator>(const iterator& other) const { return ptr > other.ptr; }
bool operator<=(const iterator& other) const { return ptr <= other.ptr; }
bool operator>=(const iterator& other) const { return ptr >= other.ptr; }
bool operator!=(const iterator& other) const { return ptr != other.ptr; }
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
};
iterator begin(){
return iterator(_data);
}
iterator end(){
return iterator(_data + _size);
}
};
经过调整 sort 函数可正常运行。

lower_bound 也可正常运行

总结:迭代器没什么函数其实,主要是运算符重载,以便仿造指针来实现操作,进一步适应函数。
7.插入删除
void insert(const iterator &pos, const T &elem) {};
void erase(const iterator &pos){};
void erase(const iterator &start, const iterator &end) {}
void insert(const iterator &pos, const T &elem) {
size_t index = pos - begin();
if(_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
for(size_t i = _size; i > index; i--) _data[i] = _data[i - 1];
_data[index] = elem;
_size++;
};

试验下来可正常运行
但可能会存在这样一个问题,执行以下操作为什么会出现 0?
a.insert(a.begin(), a[a.size() - 1]);
for(int x : a){
cout << x << ' ';
}

这是一个浅拷贝问题,我们来看以下代码片段:
a.insert(a.begin(), a[a.size() - 1]);
这里发生了以下操作:
-
a[a.size() - 1]:- 这里返回的是
a中最后一个元素的 引用(即T&类型),它直接指向数组_data中的最后一个元素。如果在其他地方修改这个引用,它会影响a数组中的实际数据。
- 这里返回的是
-
a.insert(a.begin(), a[a.size() - 1]):- 在执行
insert函数时,传递的参数是a[a.size() - 1],这是对数组最后一个元素的引用。
- 在执行
-
insert过程中的问题:-
insert函数会将新的元素插入到容器的开头,它会修改容器的底层数据结构(在底层数组_data上进行操作)。同时,为了给新元素腾出位置,原来元素会被向后移动。 -
问题出在 插入时引用失效。由于插入的元素是引用传递的,而引用绑定的是原数据的位置。当调用
insert后,底层数组发生了数据移动,原本引用的那块内存位置就可能不再有效。这就导致了 “悬空引用”,并且插入的数据不再是原始数据,而是变成了未定义行为,可能出现不正确的结果,比如输出0。
-
悬空引用的解释
引用是绑定到内存地址的
a[a.size() - 1] 是一个 引用,这意味着它引用了 a 内部的某个内存位置。例如,假设 a[a.size() - 1] 引用的是数组中的第 n-1 个元素。
T& last = a[a.size() - 1]; // last 是对 a 数组最后一个元素的引用
这个 last 引用会绑定到内存中某个位置——例如地址 0x100。如果我们通过 insert 把数据插入到数组开头,整个数组的内容将会被移动,这意味着原先的 0x100 位置可能已经被移走。此时,原本绑定到 0x100 的引用 last 变得“悬空”了,也就是出现了所谓的悬挂指针。
3.2. 移动数据后引用变得无效
在 insert 过程中:
- 首先将数据元素向后移动(为了腾出空间),然后插入新元素。
- 这就意味着,原本引用的数据已经被移动到了新的内存位置,而引用还指向原先的地址,这样就导致了 悬空引用,这个引用指向了一个已被修改或者已释放的内存位置,从而引发不确定的行为(比如
0)。
解决方案:使用拷贝而不是引用
为了解决这个问题,最简单的方法就是 传值 或 显式拷贝,而不是传递引用。这样,insert 函数就能得到一个数据的副本,而不是指向原始数据的引用。这样即使 insert 修改了底层数组,原始数据也不会受到影响。
故我们可以去掉&,做传值而非传地址。
void insert(const iterator &pos, const T elem) {
size_t index = pos - begin();
if(_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
for(size_t i = _size; i > index; i--) _data[i] = _data[i - 1];
_data[index] = elem;
_size++;
};
可以发现可以正常运行了

接着去编写 erase 函数
void erase (const iterator &pos){
for(size_t i = pos - begin(); i < _size - 1; i++){
_data[i] = _data[i + 1];
}
_size --;
}
void erase (const iterator &start, const iterator &end){
int diff = end - start;
for(size_t i = start - begin(); i < _size - diff; i++){
_data[i] = _data[i + diff];
}
_size -= diff;
}
通过编写 我们也知道了vector erase 和 insert 操作复杂度会是O(n) 的。
8.一些补充

当运行这样的程序时会出现报错,这是由于浅拷贝所导致的指针悬挂问题。
我们需要去实现深拷贝
1. 拷贝赋值运算符 operator=
MyVec<T>& operator=(const MyVec<T> &other) {
if (this != &other) {
delete[] _data; // 释放原有内存,防止内存泄漏
_data = new T[other.size()]; // 为新数据申请空间
_size = other.size(); // 更新当前元素个数
_capacity = other.capacity(); // 更新容量
for (size_t i = 0; i < _size; i++) // 逐个拷贝元素
_data[i] = other[i];
}
return *this; // 返回自身引用,支持连锁赋值 a = b = c;
}
说明:
- 这个函数是深拷贝赋值,保证两个
MyVector实例互不干扰。 - 先判断
this != &other避免自我赋值,否则会先释放自己的数据而导致错误。 - 注意:这里每个元素都调用了拷贝构造/赋值操作符,如果元素类型很大或非 POD 类型,可考虑使用移动语义优化。
同时更新一些先前未提及但很常用的函数
1. 访问首尾元素
T &front() { return _data[0]; }
const T& front() const { return _data[0]; }
T &back() { return _data[_size - 1]; }
const T& back() const { return _data[_size - 1]; }
说明:
front()返回第一个元素,back()返回最后一个元素。- 提供 非 const 版本(可修改元素)和 const 版本(只读)两种重载,以支持常量对象访问。
- 使用时需要注意容器是否为空,否则访问会越界(可以在实际实现中加异常检查)。
2. 判断容器是否为空
bool empty() const { return _size == 0; }
说明:
- 简单判断
_size是否为 0。 const修饰函数保证了常量对象也可以调用。- 用于快速判断容器状态,类似 STL 的
std::vector::empty()。
3. 可变参数构造元素并插入 emplace_back
template<typename... Args>
void emplace_back(const Args&... args) {
if (_size == _capacity)
reserve(_capacity == 0 ? 1 : _capacity * 2); // 动态扩容
_data[_size++] = T(args...); // 使用传入参数构造元素
}
说明:
-
emplace_back可以直接在容器内部原地构造对象,避免先创建临时对象再拷贝,提高效率。 -
使用 可变参数模板 (
...Args),支持任意数量和类型的构造参数。 -
内部逻辑:
- 先检查容量是否足够,不够则调用
reserve扩容。 - 使用
T(args...)在_data[_size]位置直接构造对象。 _size++更新元素数量。
- 先检查容量是否足够,不够则调用
-
对比
push_back:push_back需要先构造对象再拷贝进容器。emplace_back可以直接构造在容器内存中,更高效,特别是复杂类型。
补充部分总结:
operator=:安全深拷贝赋值,防止自我赋值问题。front()/back():方便访问首尾元素,同时支持 const 和非 const 对象。empty():快速检查容器是否为空。emplace_back:利用模板和可变参数,实现原地构造对象,减少不必要拷贝,提高性能。
这四个操作是 std::vector 中最基础但高频的操作,理解它们有助于掌握 C++ 容器设计思想:安全性 + 高效性 + 泛型支持。
完整代码
根据上述一步步的分析学习,我们可以汇总下各部分知识,得到如下代码
template <typename T> class MyVec {
private:
T *_data; // 指向动态数组首元素的指针
size_t _size; // 当前已存储的元素个数
// 加入容量部分
size_t _capacity; // 当前分配的内存容量
// 为先前代码加入capacity部分
public:
MyVec() : _data(nullptr), _size(0), _capacity(0) {}
// 带大小的构造函数
MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n), _capacity(n) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = init;
}
}
// 拷贝构造函数
MyVec(const MyVec<T> &other)
: _data(new T[other.size()]), _size(other.size()),
_capacity(other.size()) {
for (size_t i = 0; i < _size; ++i) {
_data[i] = other[i];
}
}
// 列表初始化的构造函数
MyVec(const initializer_list<T> &init)
: _data(new T[init.size()]), _size(init.size()), _capacity(init.size()) {
size_t i = 0;
for (T x : init) {
_data[i++] = x;
}
}
size_t size() { return _size; }
size_t size() const { return _size; }
size_t capacity() { return _capacity; }
size_t capacity() const { return _capacity; }
// 析构函数释放内存
~MyVec() { delete[] _data; }
// &保证成功修改(引用地址)
T &operator[](size_t index) { return _data[index]; }
// &保证成功修改(引用地址)
const T &operator[](size_t index) const { return _data[index]; }
// 提供边界检查的下标访问
T &at(size_t index) {
if (index >= _size) {
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
return _data[index];
}
const T &at(size_t index) const {
if (index >= _size) {
stringstream ss;
ss << "out of range, index: " << index << ", size = " << _size;
throw out_of_range(ss.str());
}
return _data[index];
}
void reserve(size_t new_capacity) {
if (new_capacity > _capacity) {
_capacity = new_capacity;
T *new_data = new T[_capacity];
for (size_t i = 0; i < _size; ++i) {
new_data[i] = std::move(_data[i]);
}
delete[] _data;
_data = new_data;
}
}
// 这里采用了倍增扩容策略,确保 push_back 的 摊还时间复杂度为 O(1)。
void push_back(T elem) {
if (_size == _capacity) {
// 扩容处理
reserve(_capacity == 0 ? 1 : _capacity * 2);
}
_data[_size++] = elem;
}
void resize(size_t new_size, T init = T()) {
if (new_size > _size)
reserve(new_size);
for (size_t i = _size; i < new_size; ++i) {
_data[i] = init;
}
_size = new_size;
}
void pop_back() {
if (_size == 0) {
throw out_of_range("pop_back: vector is empty");
}
--_size;
}
void clear() { _size = 0; }
class iterator {
private:
T *ptr; // 底层是个指针,并封装
public:
using value_type = T; // STL算法依赖此类型定义
iterator() : ptr(nullptr) {}
iterator(T *ptr) : ptr(ptr) {}
//++iter
iterator &operator++() {
ptr++;
return *this;
}
// iter++(empty)
iterator operator++(int) {
iterator temp = *this;
ptr++;
return temp;
}
//--iter
iterator &operator--() {
ptr--;
return *this;
}
// iter--(empty)
iterator operator--(int) {
iterator temp = *this;
ptr--;
return temp;
}
iterator operator+(int n) const { return iterator(ptr + n); }
iterator operator-(int n) const { return iterator(ptr - n); }
iterator &operator+=(int n) {
ptr += n;
return *this;
}
iterator &operator-=(int n) {
ptr -= n;
return *this;
}
int operator-(const iterator &other) const { return ptr - other.ptr; }
// 做比较的相关操作符,比较指针地址
bool operator==(const iterator &other) const { return ptr == other.ptr; }
bool operator<(const iterator &other) const { return ptr < other.ptr; }
bool operator>(const iterator &other) const { return ptr > other.ptr; }
bool operator<=(const iterator &other) const { return ptr <= other.ptr; }
bool operator>=(const iterator &other) const { return ptr >= other.ptr; }
bool operator!=(const iterator &other) const { return ptr != other.ptr; }
T &operator*() const { return *ptr; }
T *operator->() const { return ptr; }
};
void insert(const iterator &pos, const T elem) {
size_t index = pos - begin();
if (_size == _capacity)
reserve(_capacity == 0 ? 1 : _capacity * 2);
for (size_t i = _size; i > index; i--)
_data[i] = _data[i - 1];
_data[index] = elem;
_size++;
};
void erase(const iterator &pos) {
for (size_t i = pos - begin(); i < _size - 1; i++) {
_data[i] = _data[i + 1];
}
_size--;
}
void erase(const iterator &start, const iterator &end) {
int diff = end - start;
for (size_t i = start - begin(); i < _size - diff; i++) {
_data[i] = _data[i + diff];
}
_size -= diff;
}
MyVec<T> &operator=(const MyVec<T> &other) {
if (this != &other) {
delete[] _data;
_data = new T[other.size()];
_size = other.size();
_capacity = other.capacity();
for (size_t i = 0; i < _size; i++)
_data[i] = other[i];
}
return *this;
}
T &front() { return _data[0]; }
const T &front() const { return _data[0]; }
T &back() { return _data[_size - 1]; }
const T &back() const { return _data[_size - 1]; }
bool empty() const { return _size == 0; }
template <typename... Args> void emplace_back(const Args &...args) {
if (_size == _capacity)
reserve(_capacity == 0 ? 1 : _capacity * 2);
_data[_size++] = T(args...);
}
iterator begin() { return iterator(_data); }
iterator end() { return iterator(_data + _size); }
};

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



