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

【C++闯关笔记】STL:vector的学习与使用

系列文章目录

第零篇:从C到C++入门:C++有而C语言没有的基础知识总结-CSDN博客

第一篇:【C++闯关笔记】封装①:类与对象-CSDN博客

第二篇:【C++闯关笔记】封装②:友元与模板-CSDN博客

第三篇:【C++闯关笔记】STL:string的学习和使用(万字精讲)-CSDN博客

第四篇:【C++闯关笔记】STL:vector的学习与使用-CSDN博客


目录

系列文章目录

前言

一、vector是什么?

1.vector介绍

2.vector使用

1)vector的定义:构造函数

2)vector iterator 迭代器的使用

迭代器失效原因

3)vector增删查改

4)vector空间相关函数

二、模拟实现vector

1.构造函数模拟

1)默认构造函数

2)构造函数

3)拷贝构造函数

4)析构函数

2.迭代器的模拟实现

3.vector的增删查改

1)push_back

2)pop_back

3)insert

4)erase

5)swap

6)operator[ ]

4.vector空间相关函数

1)size

2)capacity

3)empty

4)reserve

5)resize

总结



前言

数据结构的本质来讲,vector完全可以被理解为 C++ 版的、功能更强大的、高度自动化的顺序表。它们的核心特征都是:在内存中占用一段连续的存储空间,通过即索引在常数时间 O(1) 内访问任意元素。【数据结构】顺序表-CSDN博客

本文将按“能用、明理”两个维度介绍vector:首先是会用,详细介绍vector各种常见接口的使用方法,以求会用;然后再尝试模拟实现vector及其接口函数,以求理解vector的实际运行逻辑。


一、vector是什么?

C 语言的顺序表通常需要程序员手动管理,其基础形态就是一个结构体:

// C 风格顺序表
typedef struct {int* data;      // 指向动态数组的指针size_t size;     // 当前元素个数size_t capacity; // 总容量
} SeqList;

接下来的所有操作:初始化、插入、删除、扩容、销毁……都需要程序员亲力亲为。而vector将这些工作全部封装了起来,给你提供了一个开箱即用还安全高效的“超级顺序表”。

1.vector介绍

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

2)就像数组一样,vector采用的连续存储空间来存储元素。

这意味着可以用下标对vector的元素进行访问,和数组一样高效。但不同于数组的定长,它的大小是可以动态改变的。

3)vector使用动态分配数组来存储它的元素。

当插入新元素时,如果这个vector“数组”剩余空间不足,就需要被重新分配空间大小。其做法是:分配一个新的数组,然后将全部元素移到这个数组。就时间消耗而言,这是一个代价较高的任务,所以每次按原大小的二倍扩容,防止频繁扩容增加时间消耗。

4)由于vector本质是个可动态增长的数组,所以vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低(因为在数组前边或中间插入数据,需要将后边数据挪出一个空位)。

综合上述对vector的介绍,再结合C++类的封装思想,其实我们已经能猜出vector的底层大概是如何分配的。

	template<typename T>class vector{public:private:T* _date;size_t _size;size_t _capacity;};

对比上面顺序表的C语言结构体,本质上它们几乎相同,现在大家应该就能理解为什么说“vector完全可以被理解为 C++ 版的、功能更强大的、高度自动化的顺序表”了。

(注意:上述代码仅通过vector介绍推测,非实际实现代码!这里只为帮读者先对vector有个认识。实际代码详见下方模拟实现vector)

既然脑海中对vector有了个大概的概念,接下来让我们学习vector各种接口的使用方法。

2.vector使用

注意包含头文件#intclude<vector>,using namespace std;

1)vector的定义:构造函数

vector常用的构造函数

造函数声明接口说明
vector( )无参构造
vector(size_t n, const value_type val )构造并初始化n个val
vector (const vector& x); 拷贝构造
vector (InputIterator first, InputIterator last);使用迭代器进行初始化构造

示例

	vector<int> v;vector<char> v1(5, 'a');vector<char> v2(v1);vector<char>v3(v1.begin(), v2.end());

解释:vector使用了类模板,在实例化vector对象时加上<类型>就是告诉编译器,这个vector对象中存储什么类型的数据。

上面介绍了几种常用的构造方法,实际上vector的构造函数远不止这几种,有兴趣的同学可参看下图中描述的几种构造方法。

2)vector iterator 迭代器的使用

什么是迭代器呢?

这里可以将迭代器iterator 理解为封装过的指针,比如begin()返回的就是vector对象存储数据的首地址。

iterator的使用接口说明
begin( )与end( );begin( )获取第一个数据位置的iterator/const_iterator,end( ) 获取最后一个数据的下一个位置 的iterator/const_iterator
rbedin( )与rend( );rbegin( )获取最后一个数据位置的reverse_iterator,rend( )获取第一个数据前一个位置的 reverse_iterator

细节注意:

end( )指向的是vector数组最后一个数据的下一个位置;rend( )获取第一个数据前一个位置.

使用示例

	vector<int>v(5, 7);vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it;it++;}

迭代器失效原因

迭代器的主要作用就是让上层用户能够不用关心底层实现,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。所以迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,如果继续使用已经失效的迭代器, 程序可能会崩溃。

那么换句话说,引起其底层空间改变的操作,都有可能使得迭代器失效,比如:resize、reserve、insert、erase、 push_back等。

所以,在使用迭代器时最好养成即用即定义的习惯,不要使用之前定义好的迭代器,防止出现莫名bug。

3)vector增删查改

vector常用的几种增删查改函数

vector增删查改功能介绍
push_back(type val)在尾部插入数据
pop_back( )删除尾部数据
insert(iterator pos,type val)在pos位置之前插入val
erase(iterator pos)删除pos位置的数据
swap(另一个vector对象)交换两个vector对象中的数据
operator[ ]通过下标访问

使用示例

int main()
{vector<int>v;v.push_back(1);v.push_back(2);v.push_back(3);for (auto a : v)cout << a;cout << endl;v.pop_back();v.insert(v.begin(), 7);v.erase(v.end()-1);for (auto a : v)cout << a;cout << endl;vector<int> v1(3, 6);v1.swap(v);for (auto a : v)cout << a;cout << endl;for (int i = 0; i < v.size(); ++i)cout << v[i];return 0;
}

4)vector空间相关函数

经常使用的vector空间相关函数

函数名称功能介绍
size( )返回vector对象中数据个数(已经使用了的空间)
capacity( )返回vector对象所占有容量大小(总空间)
empty( )判断对象是否为空,是返回true,不是返回false
resize(size_t n)更改vector的size为n
reserve(size_t n)更改vector的reserve为n

细节注意:

①reserve的用法:如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够,这样就可以避免边插入边扩容导致效率低下的问题。

不同版本的STL中capacity增长速率不同。如vs下capacity是按1.5倍增长的,g++是按2倍增长的。

③resize的大小若超过vector的capacity大小,那么resize还会起到扩容的作用。

使用示例

int main()
{vector<int> v(5, 6);cout << v.size() << ' ' << v.capacity() << endl;for (int a : v)cout << a;cout << endl;v.resize(3);for (int a : v)cout << a;cout << endl;while (!v.empty())v.pop_back();for (int a : v)cout << a;cout << endl;return 0;
}

二、模拟实现vector

在vector介绍部分,我们推测vector类大概为:

	template<typename T>class vector{public:private:T* _date;size_t _size;size_t _capacity;};

而实际上,库中实现vector类代码如下:

	template<typename T>class vector{typedef T* iterator;public:private://指向首元素地址iterator _start;//指向最后一个元素的下一个地址iterator _finish;//指向分配空间的尾地址iterator _endofstorage;};

实际上它们两种设计都是有效的,选择取决于你的偏好和需求。三个指针的设计更符合标准库的实现方式,与迭代器体系集成更好,可能在某些情况下有微小的性能优势。

本文选择尽量贴近实际库中的实现方式,也就是三个指针的设计模拟实现vector。

1.构造函数模拟

1)默认构造函数

//如果成员全是原生指针,编译器生成的默认构造函数不会初始化它们,
//这是未定义行为。所以必须手动初始化,避免野指针。
vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr)
{ }

2)构造函数

    	vector(size_t n, const T& val = T()):_start(nullptr), _finish(nullptr), _endofstorage(nullptr){reserve(n);for (size_t i = 0; i < n; ++i){_start[i] = val;			}_finish = _start + n;}

通过迭代器赋值的构造函数

		vector(const iterator begin, const iterator end):_start(nullptr), _finish(nullptr), _endofstorage(nullptr){while (begin != end){push_back(*begin);begin++;}}

3)拷贝构造函数

		vector(const vector& p){int size = p.size();int capa = p.capacity();T* newca = new T[capa];for (int i = 0; i < size; ++i){newca[i] = p[i];}_start = newca;_finish = _start + size;_endofstorage = _start + capa;}

4)析构函数

		~vector(){delete[] _start;_start = _finish = _endofstorage = nullptr;}

2.迭代器的模拟实现

		typedef T* iterator;iterator begin(){return _start;}iterator end(){return _finish;}iterator begin()const {return _start;}iterator end()const{return _finish;}

3.vector的增删查改

1)push_back

		void push_back(const T& x){if (_finish == _endofstorage){size_t newca = size() * 2 + 1;reserve(newca);}*_finish = x;_finish++;}

2)pop_back

		void pop_back(){if(!empty())_finish--;}

3)insert

		iterator insert(iterator pos, const T& x){if (pos >= _start && pos < _finish){if (_finish == _endofstorage){int temp =(int)(pos - _start);size_t newca =  size() * 2;reserve(newca);pos = _start + temp;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;end--;}*pos = x;_finish++;}return pos;}

为什么insert函数需要返回一个迭代器?

当你向vector中插入或删除元素时,很可能会导致内存重新分配(扩容)或元素位置移动。这会使所有指向该vector的迭代器、指针和引用失效(包括你用来指定位置的迭代器)。

返回一个新的迭代器,就是为了在被操作“搅动”过的容器中,为你提供一个有效的、可以继续使用的“路标”。

4)erase

		iterator erase(iterator pos){if (pos >= _start && pos < _finish){iterator begin = pos+1;while (begin < _finish){*(begin - 1) = *begin;++begin;}				_finish--;}return pos;}

5)swap

		void swap(const vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endofstorage, v._endofstorage());}

直接交换两个vector对象的私有成员,简单高效。

6)operator[ ]

		T& operator[](int i){return _start[i];}T& operator[](int i)const{return _start[i];}

4.vector空间相关函数

1)size

		size_t size(){//指针减法的本质:直接返回元素个数,而不是字节数。//编译器自动考虑了指针所指向类型的大小。return _finish - _start;}size_t size()const{return _finish - _start;}

2)capacity

		size_t capacity(){return _endofstorage - _start;}size_t capacity()const{return _endofstorage - _start;}

3)empty

		bool empty(){//if (size() != 0)return false;//else return true;return _finish == _start;}

4)reserve

		void reserve(size_t n){if (n < capacity())return; size_t oldSize = size();size_t newca= oldSize == 0 ? 16 : oldSize * 2;while (newca < n){newca *= 2;}T* newvec = new T[newca];if (_start){for (int i = 0; i < oldSize; ++i){newvec[i] = _start[i];}delete[]_start;}_start = newvec;_finish = _start + oldSize;_endofstorage = _start + newca;}

5)resize

		void resize(size_t n, const T& x = T()){size_t _size = size(), capa = capacity();if (n <= _size){_finish = _start + n;}else{if(n>capa)reserve(n);iterator temp = _start + n;while (_finish < temp){*_finish = x;_finish++;}}}


总结

本文主要分为两阶段:

第一个阶段大致介绍了什么是vector类,然后说明相关类成员函数的用法,以及一些使用陷阱;

第二个阶段为模拟实现vector类。

作者水平有限,若文中出现缺漏的之处,还万望读者指出,共同进步。

读完点赞,手留余香~

http://www.xdnf.cn/news/1390249.html

相关文章:

  • Spring Security 传统 web 开发场景下开启 CSRF 防御原理与源码解析
  • CorrectNav:用错误数据反哺训练的视觉语言导航新突破
  • Apache服务器IP 自动跳转域名教程​
  • electron-vite 配合python
  • UPDF for mac PDF编辑器
  • JAVA:Spring Boot 集成 Easy Rules 实现规则引擎
  • 来自火山引擎的 MCP 安全授权新范式
  • 嵌入式Linux驱动开发:i.MX6ULL按键中断驱动(非阻塞IO)
  • PostgreSQL15——子查询
  • 基于SQL大型数据库的智能问答系统优化
  • Emacs 多个方便查看函数列表的功能
  • QML QQuickImage: Cannot open: qrc:/images/shrink.png(已解决)
  • 前端-初识Vue实例
  • Spring Boot Redis序列化全解析(7种策略)
  • 2024年06月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • leetcode 461 汉明距离
  • 如何在FastAPI中玩转全链路追踪,让分布式系统故障无处遁形?
  • 基于MCP工具的开发-部署-上线与维护全流程技术实现与应用研究
  • 北斗导航 | PPP-RTK算法核心原理与实现机制深度解析
  • AI助力PPT创作:秒出PPT与豆包AI谁更高效?
  • TypeScript:map和set函数
  • 【前端教程】从基础到专业:诗哩诗哩网HTML视频页面重构解析
  • Java试题-选择题(21)
  • new/delete 和 malloc/free 区别
  • 小杰机器视觉(five day)——直方图均衡化
  • linux系统学习(13.系统管理)
  • 基于orin系列的刷写支持笔记
  • 30分钟入门实战速成Cursor IDE(1)
  • 【拍摄学习记录】04-拍摄模式/曝光组合
  • Nginx的主要配置文件nginx.conf详细解读——及其不间断重启nginx服务等操作