当前位置: 首页 > news >正文

C++入门自学Day10-- Vector类的自实现

 往期内容回顾

         Vector类(注意事项)

          初识Vector

          String类的自实现

         String类的使用(续)  

         String类(续)

         String类(初识)

前言:

        在 C++ 开发中,std::vector 是最常用的容器之一,它提供了动态数组的功能,兼顾了高效的随机访问和动态扩容的便利性。然而,我们平时只是调用它的 API,很少真正去思考它背后的实现原理。

自己动手实现一个简化版的 vector,不仅能帮助我们理解 动态数组 的内存管理机制,还能让我们更深入地掌握 模板编程、构造与析构、迭代器、异常安全 等 C++ 核心知识。


主要实现内容介绍

我们将一步步实现一个简化版的 vector,功能包括:

  1. 动态内存管理(模拟 new / delete)

  2. 构造与析构(支持对象类型存储)

  3. 元素访问与修改(operator[]、at())

  4. 插入与删除(push_back()、pop_back()、insert()、erase())

  5. 容量管理(size()、capacity()、reserve()、resize())

  6. 迭代器支持(基本指针型迭代器)


一、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. 浅拷贝的问题

  1. 双重释放(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. 1、在中间插入或删除元素慢

  2. 需要移动插入/删除位置后的所有元素,复杂度 O(n)。

  3.  举例:在 vector 中间插入元素,需要从插入点到末尾每个元素向后移动。

  4. 2、容量扩展可能触发拷

  5. 当 vector 扩容时,需要分配新内存并拷贝全部元素。

  6. 对于大对象或者对象频繁移动的场景成本高。

  7. 3、迭代器/指针失效问题

  8. 插入或删除可能使迭代器、指针失效,需要小心管理。

  9. 4、内存连续

  10. 对于超大数据,如果连续内存不足,会分配失败。

2、List(双向链表)的特点

  1. 每个节点独立存储,前后节点通过指针链接

  2. 优点

    • 在中间插入/删除元素 O(1),只需修改指针,不移动其他元素。

    • 插入删除操作不会触发大规模元素移动。

  3. 缺点

    • 随机访问慢:list[i] 必须从头遍历,时间复杂度 O(n)。

    • 空间开销大:每个节点需要存储额外的前/后指针。

    • CPU 缓存利用差:节点不连续,缓存命中率低。

操作类型

vector

list

随机访问

✅ O(1)

❌ O(n)

尾部插入

✅ 平摊 O(1)

✅ O(1)

中间插入/删除

❌ O(n)

✅ O(1)

空间/缓存效率

✅ 高

❌ 低

http://www.dtcms.com/a/330152.html

相关文章:

  • Nginx学习与安装
  • Docker(springcloud笔记第三期)
  • docker 将本地python环境(有系统依赖)进行打包移到另一个服务器进行部署
  • 飞算AI:企业智能化转型的新引擎——零代码重塑生产力
  • sql查询优化方式常见情况总结
  • TLSv1.2协议与TCP/UDP协议传输数据内容差异
  • 【Redis】Sentinel (哨兵)
  • 深度学习实战114-基于大模型的深度研究(DeepResearch)架构:从自主信息探索到洞察生成的革命
  • games101 第三讲 Transformation(变换)
  • RK3568项目(十五)--linux驱动开发之进阶驱动
  • Linux应用层开发--进程处理
  • 【完整源码+数据集+部署教程】医学报告图像分割系统源码和数据集:改进yolo11-HGNetV2
  • @Linux进程管理工具 - PM2全面指南
  • 理财 - 基金
  • 【React】use-immer vs 原生 Hook:谁更胜一筹?
  • PromptPilot — AI 自动化任务的下一个环节
  • 云蝠智能 Voice Agent 多模型接入技术架构与实践
  • 微信小程序实现导航至目的地
  • 腾讯位置商业授权微信小程序关键词输入提示
  • python自学笔记7 可视化初步
  • 并发编程(八股)
  • epoll模型解析
  • 数据科学与计算:从基础到实践的全面探索
  • 深度学习(6):参数初始化
  • 动画相关 属性动画+animateToImmediately+ImageAnimator帧动画组件+模态转场
  • 【C++】哈希表的实现
  • EUDR的核心内容,EUDR认证的好处,EUDR意义
  • web开发,在线%射击比赛管理%系统开发demo,基于html,css,jquery,python,django,三层mysql数据库
  • lesson37:MySQL核心技术详解:约束、外键、权限管理与三大范式实践指南
  • SpringBoot工程妙用:不启动容器也能享受Fat Jar的便利