☰
【C++篇】:一篇文章带你理解透彻vector底层原理、模拟实现与深浅拷贝问题
2026/10/14 3:26:05 网站建设 项目流程

1.前言

在前面的学习中,我们已经学习了C++中的类和对象,以及string类的底层实现。string主要帮助我们理解:

一个类如何管理动态资源。

而今天学习的vector,其实也是同样的问题。相信大家在C++中一定经常使用:

vector<int> v;

例如:

vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3);

相比静态数组:数组大小一旦确定,就无法改变。

int arr[100];

vector最大的特点就是:

空间可以动态增长。

而vector可以根据数据量自动扩容。但是vector真正值得学习的地方,并不是这些接口。重点在于:

  • vector底层如何管理空间?
  • 为什么扩容、删除之后迭代器会失效?
  • 为什么vector不能简单使用memcpy拷贝?
  • 深拷贝和浅拷贝到底有什么区别?

这些才是vector这一章真正需要掌握的内容。


2.vector是什么?

2.1 vector的介绍

vector是STL中的一个容器。官方定义:

vector是表示可变大小数组的序列容器。

在我的理解上,vector本质上就是:动态数组。它和普通数组一样:

v[0],v[1],v[2]

可以通过下标访问。但是不同的是:数组:

int arr[10];

空间固定,而vector空间可以动态增长。与我们上一期学的string的关系:两者设计思想高度一致,都具有size()、capacity()、reserve()、resize()、push_back()等接口,可以将string看作是面向字符的特殊动态序列,但二者也有些许不同,哪里呢,让我们一探究竟。

2.2 vector底层结构

vector底层其实并不复杂。它主要通过三个指针管理空间:

T* _start; T* _finish; T* _endofstorage;

分别表示:

_start | ↓ [1][2][3][4][ ][ ][ ] ↑ ↑ finish endofstorage

其中:

  • _start表示数据开始位置。
  • _finish表示有效数据结束位置。
  • _endofstorage表示空间结束位置。

千万不要把有效数据和空间结束位搞混了,这在编译的过程中经常容易犯错。

有效数据个数其实就是:

size = _finish - _start

容量就是:

capacity = _endofstorage - _start

3.vector的简单使用

3.1 vector的构造和遍历

构造方式:支持默认构造、指定元素个数和值构造、区间构造,其中STL的区间均遵循左闭右开[first, last)的规则。

例如:

vector<int> v1; vector<int> v2(5, 1); vector<int> v3(v2); int a[] = {1, 2, 3, 4}; vector<int> v4(a, a + 4);

这里的最后一个是迭代器区间构造,区间构造非常常用,尤其在数组、字符串、其他容器之间转换时。迭代器提供了统一的方式去访问容器,这个在list会介绍,它很重要。vector的遍历也是常见的三种:下标遍历、迭代器遍历以及称为“语法糖”的范围for遍历,其实for的底层是迭代器我们也已经说过了。

3.2 迭代器遍历

有提供begin和end两个接口,begin是指向第一个元素的位置,end是指向最后一个元素的下一个位置,那么end()能不解引用?不能,不是有效元素的位置哦。至于rbegin()和rend()就是反过来,前者是最后一个元素的位置,后者是第一个元素的前一个位置。其他两个范围for和下标遍历其实不用过多赘述了,范围for中要修改的话就要加&。

3.3 vector常用接口

掌握这些先,后面还有swap、clear等等,这里附上文档链接,可去官方文档查查,对英文要求也不很高。

接口作用
push_back()尾插
pop_back()尾删
size()获取有效数据个数
capacity()获取容量
reserve()扩容
resize()改变size
operator[]()下标访问
insert()在某个位置插入元素
erase()删除某个位置的元素

例如:

#include<vector> using namespace std; int main() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); for(size_t i = 0; i < v.size(); i++) { cout << v[i] << " "; } return 0; }

输出:


4.vector扩容机制

4.1 vector最大的特点:

空间不足时会自动扩容。

例如:

vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); v.push_back(5);

如果空间不足(假设初始空间只有四个)

旧空间 [1][2][3][4] 申请新空间 [1][2][3][4][ ][ ][ ][ ][ ] 释放旧空间

所以扩容实际上包含:

  1. 检测空间是否不足
  2. 开辟新空间
  3. 拷贝旧数据
  4. 释放旧空间
  5. 修改指针

这里有一个问题:

既然扩容需要重新搬移数据,那么vector为什么还要预留空间?

你想想如果每插入一个数据就扩容一次,效率会低得一批。所以C++规定一般会:

一次申请较大的空间 ↓ 不断插入 ↓ 直到空间不足 ↓ 再次扩容

4.2 reserve和resize区别

这两个接口非常容易混。reserve,只扩个容:

vector<int> v; v.reserve(100);

但:

v.size()

仍然是:

0

resize,才改变有效元素个数:

v.resize(10);

不仅开空间,还会初始化元素。

4.3 和string不同,能代替string吗?

首先,先说答案,不能。试问一下如果真的代替了,虽然C++有向前兼容性,但是string能一直保留必定有它的独特之处。原因我觉得有这两方面:

  1. string作为特意处理字符串数组的结构,它在一个字符串后会自动加'\0',这是vector所没有的,虽然可以有vector<string>,但总归还是不太方便,它不会自动填末尾的斜杠0。
  2. string接口丰富,有比较大小、+=,但是vector不用。

5.vector模拟实现(重要部分)

我们简单实现一个vector,其实说简单也不会简单,有一些坑,因为vector要进行内存管理!这四个字意味着必须要考虑深拷贝,核心成员:

template<class T> class vector { private: T* _start; T* _finish; T* _endofstorage; };

5.1 push_back实现

如果vector内部使用memcpy。那么:

v1中的string ↓ hello ↑ v2中的string

两个对象共享资源,析构时,第一个string:

delete[] _str;

释放空间。第二个string再次delete释放同一块空间。

结果:

double free

程序崩溃。所以vector扩容和拷贝时不能使用memcpy,而应该调用对象自己的赋值,它会去调用operator=()

tmp[i]=_start[i];

因为:

string

自己的赋值运算符会完成深拷贝。


8.vector的深拷贝

vector自身也存在拷贝问题。例如:

vector<int> v1; vector<int> v2(v1);

如果默认拷贝:

v1._start ↓ 一块空间 v2._start ↓ 同一块空间

两个vector管理同一块资源。析构时同样会出现问题。所以vector需要自己实现拷贝构造。


8.1 深拷贝实现

vector(const vector<T>& v) { _start=new T[v.capacity()]; for(size_t i=0;i<v.size();i++) { _start[i]=v._start[i]; } _finish=_start+v.size(); _endofstorage=_start+v.capacity(); }

核心

创建新的空间,再复制数据。


9.vector的赋值运算

赋值:

v1=v2;

由于也是要开辟空间进行内存管理,所以同样存在浅拷贝问题。现代写法:

vector<T>& operator=(vector<T> v) { swap(v); return *this; }

这里参数是传值,而不是传引用,一旦调用a=b,编译器就会自动拷贝一份(深拷贝)b的副本给v,然后进入swap函数体,a和v交换,相当于a变成b的内容,v变成a的内容,返回时,v作为临时副本,把a的旧数据释放掉,析构。


10.vector迭代器失效问题(在insert,erase)

vector迭代器本质:

T*

也就是指针,如果发生扩容或者删除操作:

旧空间释放 ↓ 原来的指针失效

例如:

auto it=v.begin(); v.push_back(10); cout<<*it; for(size_t i=0; i<=v.size();i++) { v[i - 1] = v[i]; }

这里可能崩溃。

it ↓ 旧空间

但是旧空间已经释放。所以vector中以下操作可能导致迭代器失效:

  • insert
  • erase

10.1 vector的模拟实现全部代码参考附上

namespace wj { 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; } // vector<int> v(10, 1); // vector<string> v1(10, "1111"); vector(size_t n, const T& val = T()) { resize(n, val); } vector(int n, const T& val = T()) { resize(n, val); } // [first, last) template<class InputIterator> vector(InputIterator first, InputIterator last) { while (first != last) { push_back(*first); ++first; } } vector(const vector<T>& v) { _start = new T[v.capacity()]; for (size_t i = 0; i < v.size(); i++) { _start[i] = v._start[i]; } _finish = _start + v.size(); _endofstorage = _start + v.capacity(); } void swap(vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_endofstorage, v._endofstorage); } // v1 = v2 vector<T>& operator=(vector<T> v) { swap(v); return *this; } ~vector() { if (_start) { delete[] _start; _start = _finish = _endofstorage = nullptr; } } // void reserve(size_t n) { if (n > capacity()) { size_t sz = size(); T* tmp = new T[n]; 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; } } } void push_back(const T& x) { /*if (_finish == _endofstorage) { size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; reserve(newcapacity); } *_finish = x; ++_finish;*/ insert(end(), x); } void pop_back() { erase(--end()); } size_t capacity() const { return _endofstorage - _start; } size_t size() const { return _finish - _start; } T& operator[](size_t pos) { assert(pos < size()); return _start[pos]; } iterator insert(iterator pos, const T& x) { assert(pos >= _start && 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 = x; ++_finish; return pos; } iterator erase(iterator pos) { assert(pos >= _start && pos < _finish); iterator it = pos + 1; while (it != _finish) { *(it - 1) = *it; ++it; } --_finish; return pos; } private: iterator _start = nullptr; iterator _finish = nullptr; iterator _endofstorage = nullptr; };

11.最后

vector这一章看似是在学习一个STL容器。但是实际上学习的是:

一个管理动态资源的类应该如何设计。

vector最重要的不是:

push_back() erase() insert()

这些接口。真正重要的是:

vector ↓ 动态开辟空间 ↓ 扩容 ↓ 资源迁移 ↓ 拷贝问题 ↓ 浅拷贝 ↓ 深拷贝 ↓ 对象管理资源
memcpy

这个问题一定要理解。对于内置类型memcpy没有问题。但是对于:

string vector

这种管理资源的对象:不要用memcpy

不要认为能复制内存就一定能复制对象。

C++中真正安全的拷贝,应该让对象自己完成资源管理。

这也是我们学习vector模拟实现最大的意义。后面会学习list,这里很考验对迭代器的理解,学了list后会对类模板以及封装有另一层意境的理解。

感谢大家的观看阅读,如果这篇文章能够给你们带来哪怕一点点收获和解决困惑,那对我而言都是一种动力和成就感。如果文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 支持一下!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询