【C++入门】STL容器--vector底层数据结构剖析

news2024/11/13 15:31:38

 

目录

 前言

 1. vector的使用

      vector的构造

 vector迭代器

 vector空间相关的接口

 vector 功能型接口

 find

 swap

 insert

 erase

2. vector内部数据结构剖析

reserve

 push_back和pop_back

size、capacity、empty、operator[ ];

 insert和erase

resize

swap

 拷贝构造和赋值重载

构造函数补充

 迭代器区间构造

指定数值个数构造

总结


前言

         vector在C++中非常重要的容器,在刷题中也经常使用,它是一个动态的数组,提供了快速的随机访问和在尾部的插入和删除操作。vector的底层实现也是非常优秀的数据结构,值得我们去学习鉴赏,使用STL我们需要做到三个境界:能用,明理,能扩展;所以了解它的基本底层原理也是非常有必要的。

在这里插入图片描述

 1. vector的使用

      vector的构造

 常见的构造方式:

vector<int> v1;
vector<int> v2(10,1); // 使用10个1进行初始化
vector<int> v3(v2);

 迭代器区间构造:

vector<int> v = { 1,2,3,4,5 };
vector<int> vv(v.begin(),v.end());

 除此之外我们还可以传变量:

vector<int> v(n + m, 0);//开一个数组大小为n+m初始化为0

 这样写也是可以的,在一些刷题常见可能会遇到。

 vector迭代器

 

 vector和string迭代器的使用方法相同

vector<int> v{1,2,3,4,5,6};
vector<int>::iterator it = v.begin();
while(it!=vv.end())
{
	cout<<*it;
	it++;
}

 vector空间相关的接口

 这些接口的功能与使用和string的很相似,不做过多的介绍。

  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
  • resize在开空间的同时还会进行初始化,影响size

 vector 功能型接口

 这里着重介绍一下find和swap

 find

 find属于算法库里的内容在头文件 algorithm 中

 函数返回时返回的也是迭代器类型

template<class InputIterator, class T>
  InputIterator find (InputIterator first, InputIterator last, const T& val)
{
  while (first!=last) {
    if (*first==val) return first;
    ++first;
  }
  return last;
}

 使用样例:

vector<int> v{1,2,3,4,5,6,7,8,9};
auto pos = find(v.begin(),v.end(),5);
cout<<*pos;
 swap

 细心的朋友可能会发现,vector、string中都有一个swap接口,它和算法库里的swap还有些不一样:

vector:

 算法库:

算法库里的swap要传两个参数,vector里只用传一个参数;并且它们的交换方式也有所不同;

 算法库中的交换就是简单的数值交换

template <class T> 
void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

 vector和string中的swap通过交换指针指向来实现交换的;

在调用vector的swap时,其实还存在一个this指针作为参数,交换指针指向的内容。

vector<int> v1 = {1, 2, 3};
vector<int> v2 = {4, 5, 6};

v1.swap(v2);
 insert
vector<int> v{1,2,3,4,5,6,7,8,9};
auto pos = find(v.begin(),v.end(),5);
v.insert(pos,10); // 在5前边插入10
 erase
vector<int> v{1,2,3,4,5,6,7,8,9,10};
auto pos = find(v.begin(),v.end(),10);
vv.erase(pos);
//删除一个区间
v.erase(v.begin()+2,v.end()-2);

2. vector内部数据结构剖析

vector的底层本质就是一个动态顺序表,然而它的设计方式又与我们之前的顺序表又有所不同:

template<class T>
class vector
{
public:
	typedef T* iterator;
    typedef const T* const_iterator;

private:
    // 给缺省值,避免每个构造函数都要写初始化列表
	iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _endofstorage = nullptr;
}

 它的底层是通过三个指针来控制的

 这样的设计也是为了迭代器的适配,有了这样的适配,在STL容器中,迭代器的使用都基本相同,也是为了去除不同容器在使用迭代器时的差异。

 模拟封装一个vector的类,首先我们要先实现它的构造函数和析构函数:

vector() // 注意:这里虽然什么都没有但是不能删,因为我们写的有构造函数
         // 编译器不会生产默认构造,如果删除无参构造时就会报错
{}

~vector()
{
	if (_start)
	{
		delete[] _start;
		//析构时要将所有的指针变量置为NULL
		_start = _finish = _endofstorage = nullptr;
	}
}

 构造函数用于初始化

reserve

 刚开始就要遇到一个大难题,迭代器失效的问题;

        reserve的主要功能就是开空间,而开空间时就可能会遇到空间转移的情况(开辟一块新的空间,将原空间的内容拷贝过去);空间一旦转移,就会导致原先的指针失效——迭代器失效。所以我们要先记录一下原空间的一些数据。

第二个问题,浅拷贝的问题:

        将数据拷贝到新的空间看似很简单,但是它存在浅拷贝的问题,在只用内置类型时不会出问题,一旦遇到涉及动态内存管理的自定义类型就直接会挂;

 memcpy将所有的数据都拷贝过去,这会导致更深一层的浅拷贝问题,两个_str指向同一块空间,原空间交换后就会被销毁,字符串也会被销毁,new的新空间中的_str就会是一个野指针。

所以看似简单的拷贝使用memcpy直接拷贝不行;

        可以使用一个更为直接的方式,就是直接赋值(利用自定义类型的operator=).

void reserve(size_t n)
{
	if (n > capacity())
	{
		//考虑到交换后地址空间转移,需要提前记录旧空间的数据大小
		size_t old = size();
		T* tmp = new T[n];
		if (_start)
		{
			// 自定义类型会造成迭代器失效的问题例如:string
			// memcpy(tmp, _start, n * sizeof(T)); // 这个属于浅拷贝
			for (int i = 0; i < old; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
		
		_start = tmp;
		_finish = _start + old;
		_endofstorage = _start + n;
	}
}

 push_back和pop_back

 有了reserve,尾插和尾删就简单多了:

void push_back(const T& x)
{
	if (_finish == _endofstorage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}
	*_finish = x;
	_finish++;
}

void pop_back()
{
	assert(size() > 0);
	--_finish;
}

size、capacity、empty、operator[ ];

size_t size() const
{
	return _finish - _start;
}

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

bool empty() const
{
	return (size()==0);
}

T& operator[] (size_t pos)
{
	assert(size() > pos);
	return _start[pos];
}

const T& operator[](size_t pos) const
{
	assert(pos < size());

	return _start[pos];
}

 insert和erase

         插入和删除操作,依然存在着迭代器失效的问题,删除和插入操作可能面临缩容和扩容问题,这都会导致操作后的pos与原先的pos数据不同导致迭代器失效(有些编译器会进行检查,认为erase之后的迭代器失效);insert 和 erase 之后形参pos都可能会失效,insert和erase之后的迭代器不要使用

iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);
	if (_finish == _endofstorage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}

	//memmove(pos + 1, pos, sizeof(T) * (_finish - pos));
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}

	*pos = x;
	++_finish;
	return pos;
}

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);
	iterator it = pos + 1;
	while (it < _finish)
	{
		*(it - 1) = *it;
		it++;
	}
	_finish--;

	return pos;
}

为了避免操作后的pos数据丢失,所以返回类型最好是迭代器类型(也就是指针,连续存储的空间可以被视为是一个天然的迭代器);

resize

resize的主要作用是改变容器中元素的数量(size为有效元素个数,capacity为容量)

 三种情况:

  • n > size ,n<capacity   

resize的参数值大于当前容器中size的大小,小于capacity,会在容器末尾插入新的元素,不进行扩容(插入的元素可指定,也可以默认使用默认构造)

  • n > capacity 

 resize的参数值大于当前容器的capacity大小,进行扩容,并插入新的元素

  • n < size

resize的参数值小于当前容器的size大小,那么会删除多余的元素

 它又可以分为两种情况,有删除操作和无删除操作;

// T()为匿名构造
void resize(size_t n, T val = T())
{
	if (n > size())
	{
        // 扩容时会判断传值是否大于它的容量
		reserve(n);
        // 插入新的数据
		while (_finish < _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
	else
	{
		_finish = _start + n;
	}
}

不管自定义类型还是内置类型匿名构造会调用它的默认构造函数(在C++中认为内置类型也有默认构造函数)

int i; // 默认初始化为0
double d; // 默认初始化为0.0
char c; // 默认初始化为'\0'
bool b; // 默认初始化为false
int* ptr; // 默认初始化为nullptr

swap

swap可以直接使用std里的swap进行封装:

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

 拷贝构造和赋值重载

 在学习时,我们使用的都是传统的写法,每次都是开空间然后将数据拷贝过去,这样其实很烦,于是小编上网查阅了一些资料,看到别人的代码,偷学来了一种新的写法,简单便捷;

在这里分享给大家:

 我们可以利用我们实现的一些接口来帮我们做,简单便捷;

vector(const vector<T>& v)
{
	reserve(v.capacity());
	for (const auto& e : v)
	{
		push_back(e);
	}
}

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

 最为精妙的莫过于operator=;之前我们写赋值运算符重载要写一大堆,这里只需要两行就可以完成;

什么原理?

 如上图,v1有数据,v2没有数据,现在将v1的值赋值给v2,调用赋值运算符重载(operator=);

注意这里是传值调用,传值调用会调用拷贝构造,创建一个临时变量(也就是图上的v);

调用swap函数,交换两个指针指向的内容;

函数执行结束,临时对象v就会连同v的内容一同被销毁;

这样的设计堪称精妙,值得我们学习和鉴赏;

构造函数补充

构造函数我们只实现了一个,构造函数还有迭代器区间构造,指定数值个数构造

 迭代器区间构造

在此之前我们要先来把我们实现的vector迭代器接口实现,方便遍历:

iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

         这里我们可以使用模板,传进来的迭代器的类型我们未知,可能是int类型、string类型、double类型...

template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

 利用push_back接口进行数据插入;

指定数值个数构造
vector(size_t n, const T& val = T())
{
	resize(n, val);
}

 我们直接调用resize来帮我做;但是这样存在缺陷,我们用10个0进行初始化就会报错;为什么?

        模板调用时,一定是选择最合适的,两个参数都是int类型,他会调用迭代器的那个函数模板(模板的两个参数类型相同),不会走vector(size_t n, const T& value = T())这个构造方法,到了*first,就会报错,一个int类型如何解引用?

 解决办法也很简单在实现一个构造函数给int类型:

vector(int n, const T& val = T())
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

 


总结

        在本文中,我们深入探讨了STL容器中的vector,以及它的底层数据结构。vector的底层设计也是比较好的,值得每位初学者借鉴学习;以上便是本文全部内容,希望对你有所帮助,最后感谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1398585.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

acwing讲解篇之92. 递归实现指数型枚举

文章目录 题目描述题解思路题解代码 题目描述 题解思路 本题相当于二叉树的深度优先遍历&#xff0c;树的第i层是第i个数选或不选 我们记录当前递归的深度deep 然后用state进行状态压缩&#xff0c;state第i位是1表示选第i个数&#xff0c;第i位是0表示不选第i个数 进行dfs 如…

【面试突击】硬件级别可见性问题面试实战(上)

&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308; 欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复…

算法训练 day24 | 77. 组合

77. 组合 题目链接:组合 视频讲解:带你学透回溯算法-组合问题 回溯其实和递归是密不可分的&#xff0c;解决回溯问题标准解法也是根据三部曲来进行的。 1、递归函数的返回值和参数 对于本题&#xff0c;我们需要用一个数组保存单个满足条件的组合&#xff0c;还需要另一个结果数…

qt初入门5:字体设置和元对象系统的练习

空闲时间&#xff0c;参考课本demo&#xff0c;做一下练习。 字体的颜色主要用QPalette类&#xff0c;调色板的作用&#xff0c;控制着窗口部件的颜色和外观&#xff0c;包括背景色、前景色、文本颜色、边框颜色等。 字体的显示样式主要用Font类&#xff0c;用于管理字体。 元…

【想要安利给所有人的开发工具】最强工具ChatGPT——分享一些使用经验

目录 &#x1f525;个人使用ChatGPT的经验 &#x1f525;如何使用ChatGPT 方法一 方法二 &#x1f525;&#x1f525;提问技巧分享 1、英语翻译员 2、面试官 3、javascript 控制台 4、Excel表格 5、作曲家 6、辩手 7、小说家 8、诗人 9、数学老师 10、网络安全…

【第七在线】利用大数据与AI,智能商品计划的未来已来

随着科技的快速发展&#xff0c;大数据和人工智能(AI)已经成为各行各业变革的重要驱动力。在服装行业&#xff0c;这两大技术的结合正在深刻改变着传统的商品计划方式&#xff0c;引领着智能商品计划的未来。 一、大数据与AI在智能商品计划中的角色 大数据为智能商品计划提供了…

实用干货:最全的Loading动画合集网站!复制即用

大家好&#xff0c;我是大澈&#xff01; 本文约1000字&#xff0c;整篇阅读大约需要2分钟。 感谢关注微信公众号&#xff1a;“程序员大澈”&#xff0c;免费领取"面试礼包"一份&#xff0c;然后免费加入问答群&#xff0c;从此让解决问题的你不再孤单&#xff01…

心跳检测与服务剔除

社保中心的忧桑 今天社保中心来了一位钉子户&#xff0c;90多岁的王大爷又兴高采烈的来给自己快120岁的老父亲领社保了! 工作人员这一-想&#xff0c;好像哪里不对啊&#xff0c;这老父亲120岁的年纪都可以上吉尼斯世界纪录了&#xff0c;要不咱帮老爷子去申请一下?王大爷一听…

Java 实际开发中,实现微信小程序/微信公众号的微信注册登录

1.功能   实际开发中&#xff0c;实现微信小程序/微信公众号的微信注册登录 2.前置条件   这里只关注注册登录逻辑&#xff0c;所以前提是先对接好微信授权的相关接口。比如&#xff1a;      1. 获取微信公众号/小程序token接口      2. 获取微信公众号/小程序授…

Springboot常见报错及解决方案

1、多模块项目无法启动&#xff0c;报错Failed to execute goal on project*: Could not resolve dependencies for project 2、报错找不到符号&#xff08;在多moudle调用的时候&#xff0c;公共模块新增了东西的时候发生&#xff09; Rebuild项目

【实战】SpringBoot自定义 starter及使用

文章目录 前言技术积累SpringBoot starter简介starter的开发步骤 实战演示自定义starter的使用写在最后 前言 各位大佬在使用springboot或者springcloud的时候都会根据需求引入各种starter&#xff0c;比如gateway、feign、web、test等等的插件。当然&#xff0c;在实际的业务…

C语言从入门到实战——文件操作

文件操作 前言一、 为什么使用文件二、 什么是文件2.1 程序文件2.2 数据文件2.3 文件名 三、 二进制文件和文本文件四、 文件的打开和关闭4.1 流和标准流4.1.1 流4.1.2 标准流 4.2 文件指针4.3 文件的打开和关闭4.4 文件的路径 五、 文件的顺序读写5.1 顺序读写函数介绍fgetcfp…

pearcmd文件包含漏洞

1.什么是pearcmd.php pecl是PHP中用于管理扩展而使用的命令行工具&#xff0c;而pear是pecl依赖的类库。在7.3及以前&#xff0c;pecl/pear是默认安装的&#xff1b;在7.4及以后&#xff0c;需要我们在编译PHP的时候指定--with-pear才会安装 不过&#xff0c;在Docker任意版本…

python 自动化模块 - pyautogui初探

python 自动化模块 - pyautogui 引言一、安装测试二、简单使用三、常用函数总结 引言 在画图软件中使用pyautogui拖动鼠标&#xff0c;画一个螺旋式的正方形 - (源码在下面) PyAutoGUI允许Python脚本控制鼠标和键盘&#xff0c;以自动化与其他应用程序的交互。API的设计非常简…

卡萨帝洗衣机:被模仿也是竞争力

如何用一句话形容某家企业的竞争力和领导地位&#xff1f;“某某一出手&#xff0c;就知有没有。”这句话相当匹配。如果再加一条&#xff0c;“被模仿”也恰到好处。 从顶流公司OpenAI&#xff0c;苹果Apple Vision Pro&#xff0c;再到卡萨帝洗衣机&#xff0c;被跟随、模仿…

thinkadmin表单上传单图,多图,单文件,多文件

{extend name="../../admin/view/main"}{block name=content} <form action="{:sysuri()}" class="layui-card layui-form" data-auto="tr

iPerf3 使用指南

文章目录 iPerf3 使用指南1 iPerf3 简介2 安装指令2.1 Windows2.2 Linux 3 入门用法4 进阶用法4.1 启动服务端4.2 TCP 带宽测试4.3 UDP 带宽测试 5 iPerf3 命令说明 iPerf3 使用指南 1 iPerf3 简介 iPerf3 是用于主动测试 IP 网络上最大可用带宽的工具。它支持时序、缓冲区、…

Linux编写简易shell

思路&#xff1a;​ ​ ​ 所以要写一个shell&#xff0c;需要循环以下过程:​ 获取命令行解析命令行建立一个子进程&#xff08;fork&#xff09;替换子进程&#xff08;execvp&#xff09;父进程等待子进程退出&#xff08;wait&#xff09; 实现代码&#xff1a;​ #inc…

AI量化交易案例

量化交易 案例介绍 1.1 案例说明 机器学习与人工智能在金融领域已有成熟的应用。用统计模型来预测股票等金融产品的价格并自动交易&#xff0c;这是其中的经典问题。价格预测的模型是这个应用场景中的核心问题&#xff0c;在预测价格变化的基础上&#xff0c;通过一定的交易规则…

给视频添加srt字幕,为你的创作加上心声

无论你是分享生活点滴、教学知识&#xff0c;还是传递某种情感&#xff0c;总会有那么一刹那&#xff0c;言语显得如此苍白无力。而srt字幕就像是一位翻译官&#xff0c;用最恰当、最直接的文字&#xff0c;把你所要表达的意思准确的传递给观众。 所需工具&#xff1a; 一个【…