C++初阶:vector

news2025/1/9 14:46:38

文章目录

  • 1 vector介绍
  • 2 实现vector
    • 2.1 类的定义
    • 2.2 默认成员函数
      • 2.2.1 构造函数
      • 2.2.2 析构函数
      • 2.2.3 拷贝构造
      • 2.2.4 赋值重载
    • 2.3访问接口
    • 2.4 容量接口
    • 2.5 修改接口
      • 2.5.1 尾插尾删
      • 2.5.2 任意位置插入
      • 2.5.3 任意位置删除
    • 2.6 其他接口

1 vector介绍

1 vector是表示可变大小数组的序列容器
2 就像数组一样,vector也采用连续存储空间的方式来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组为了增加存储空间需要被重新分配大小。其做法是,分配一个新的数组,然后将全部元素移到这个数组。

2 实现vector

在这里插入图片描述

可以看出,vector使用start指向数组的起始位置,finsh指向数组有效元素的下一个位置,end_of_storage指向区间的容量

2.1 类的定义

namespace zbt
{
	template <class T>
		class vector
	{
	public:
			typedef T* iterator;
			typedef const T* const_iterator;
	 private:
		iterator _start;//指向数组的起始位置
		iterator _finsh;//指向数组有效元素的下一个位置
		iterator _end_of_storage;//指向区间的容量

	};

和之前实现string不同的是,把指向元素个数和区间容量的值改为用相应的迭代器实现,但本质是一样的。我们可以看到,vector的迭代器底层实现依旧是指针。

之所以定义为类模板,是因为vector里面不仅可以存int,char,double这样的内置类型,同样也可以存储string,甚至vector这样的自定义类型

2.2 默认成员函数

2.2.1 构造函数

                vector()
				:_start(nullptr)
				,_finsh(nullptr)
				,_end_of_storage(nullptr)
			{

			}

无参的构造函数,直接把三个指针初始化为nullptr。

在这里插入图片描述

除了使用无参的构造函数,我们看到C++的官方文库还支持使用迭代器区间进行构造的方式

//函数模板
			template <class InputIterator>
			vector(InputIterator first, InputIterator last)//给定一段迭代器区间
				:_start(nullptr)
				,_finsh(nullptr)
				,_end_of_storage(nullptr)
            {
				while (first != last)
				{
					push_back(*first);//从头开始将元素一个接一个尾插到vector里面
					first++;
				}

			}

从中可以看出,在类模板里面同样也可以套用函数模板,将此函数实现为模板函数,这样任意类型的迭代器都可以用来构造vector
当然,实现迭代器区间构造的方式前提还需要实现push_back函数

example:

            std::string s("hello");
			//使用迭代器区间构造v
			vector<char>v(s.begin(), s.end());
			for (auto e : v)
			{
				cout << e << " ";
			}
			cout << endl;

用string的迭代器去构造vector

2.2.2 析构函数

 ~vector()
			{
				delete[]_start;//将动态开辟的空间释放掉
				_start = _finsh = _end_of_storage = nullptr;
			}

2.2.3 拷贝构造

//现代写法
			void swap(vector<T>& tmp)
			{
				::swap(_start, tmp._start);
				::swap(_finsh, tmp._finsh);
				::swap(_end_of_storage, tmp._end_of_storage);
			}
			//v2(v3)
			vector(const vector<T>& v)
				:_start(nullptr)
				, _finsh(nullptr)
				, _end_of_storage(nullptr)
			{
				vector<T>tmp(v.begin(), v.end())//利用迭代器区间构造一个tmp,里面存储的值就是对应v的值;
				swap(tmp);//将v2和tmp进行交换

			}

2.2.4 赋值重载

	//v2=v3;
			vector<T>& operator=(vector<T> v)
			{
				//v就是v3的拷贝,然后将v和v2进行交换
				swap(v);
				return *this;
			}

2.3访问接口

访问方式可以是operator[ ]或者迭代器

            iterator begin()
			{
				return _start;
			}
			iterator end()
			{
				return _finsh;
			}
			const_iterator begin()const
			{
				return _start;
			}
			const_iterator end()const
			{
				return _finsh;
			}
			T& operator[](size_t pos)
			{
				assert(pos < size());
				return _start[pos];
			}
			const T& operator[](size_t pos)const
			{
				assert(pos < size());
				return _start[pos];
			}

2.4 容量接口

resize

void resize(size_t n,const T& val = T())
				//匿名对象调用默认构造,
			{
				if (n > capacity())//需要的空间比原有的空间大,先扩容
				{
					reserve(n);
				}
				if (n > size())//需要的空间大于有效数据的个数,在原有有效数据的后面插入val
				{
					while (_finsh != _start + n)
					{
						*(_finsh) = val;
						_finsh++;
					}
				}
				else//需要的空间比原有的空间小,需要删除数据
				{
					_finsh = _start + n;
				}
			}

缺省值采用T()的形式,T()调用的是T类型的默认构造函数,初始化出一个匿名对象,得到的是T类型的空值

reserve

void reserve(size_t n)
			{
				if (n > capacity())
				{
					size_t sz = size();
					T* tmp = new T [n];//动态开辟一块新的空间,大小为n
					if (_start)//如果原来的vector有数据,将数据拷贝到新的空间去
					{
						//memcpy(tmp, _start,sizeof(T)* sz);
						for (size_t i = 0; i < sz; i++)
						{
							tmp[i] = _start[i];
						}
						delete[]_start;
					}
					//更新成员变量的值
					_start = tmp;
					_finsh = _start + sz;
					_end_of_storage = _start + n;
				}
			}

需要注意的是,在拷贝数据的时候,我们并没有使用memcpy直接将数据拷贝过去,而是采用一个一个的赋值,这是为什么呢?这里便涉及更深层次的深拷贝问题

如果vector里面存储的是int,double等内置类型,用memcpy进行浅拷贝是完全没有问题的。但是vector里面也可以存储自定义类型的数据,例如string,vector等,这时如果粗暴的将数据进行浅拷贝,那么原来数组中的数据和和此时新拷贝的数据便会指向同一块空间,析构函数对同一块空间释放两次,程序便会崩溃

在这里插入图片描述

所以在拷贝数据的时候,应使用深拷贝。这里我们采用的是赋值重载,即一个一个赋值。

2.5 修改接口

2.5.1 尾插尾删

void push_back(const T& x)//尾插
			{
				if (_finsh == _end_of_storage)
				{
					reserve(capacity() == 0 ? 4 : capacity() * 2);//满了就扩容
                 }
				*(_finsh) = x;
				_finsh++;
			}
			void pop_back()//尾删
			{
				assert(_finsh > _start);
				_finsh--;
			}

2.5.2 任意位置插入

iterator  insert(iterator pos, const T& x)
			{
				assert(pos >= _start);
				assert(pos <= _finsh);
				if (_finsh == _end_of_storage)//满了扩容
				{
					size_t len = pos - _start;
					reserve(capacity() == 0 ? 4 : capacity() * 2);
					//扩容完之后,_start的地址会发生变化,所以要更新pos地址
					pos = _start + len;
				}
				iterator end = _finsh - 1;
				while (end >= pos)//[pos~_finsh)位置的元素后移一位
				{
					*(end+1) =*end ;
					end--;
				}
				*pos = x;
				_finsh++;
				return pos;

			}

注意:
① 因为扩容会重新开辟一块空间,_start会指向一块新的空间,但pos还是指向之前的位置,没有更新,这时迭代器就会失效,所以在扩容之后要更新pos的值,指向新的位置。
② insert函数内的iterator失效问题解决了,但是函数外的pos并没有改变,仍然指向之前的位置,所以最后要将更新后的pos值作为返回值返回。

2.5.3 任意位置删除

iterator erase(iterator pos)
			{
				assert(pos >= _start);
				assert(pos < _finsh);
				iterator begin = pos + 1;
				while (begin < _finsh)//pos位置之后的元素统一向前移动
				{
					*(begin - 1) = *begin;
					begin++;
				}
				_finsh--;
				return pos;
			}

任意位置删除是否有迭代器失效的问题呢?

答案是有的,举个栗子
删除数组元素中的偶数

第一种写法

vector<int>v3;
			v3.push_back(1);
			v3.push_back(2);
			v3.push_back(4);
		     v3.push_back(3);
			v3.push_back(4);
			v3.push_back(5);
			vector<int> ::iterator it = v3.begin();
			while (it != v3.end())
			{
				if (*it % 2 == 0)
				{
					 v3.erase(it);
				}
				it++;
			}
			for (auto e : v3)
			{
				cout << e << " ";
			}
			cout << endl;

在这里插入图片描述

很显然,答案错误,偶数并没有删除完,这种写法是有bug的

在这里插入图片描述

在删除元素2之后,由于2后面的元素前移,所以此时pos指向的是4,pos++刚好越过了4,所以没有删除掉4这个元素。此时迭代器便失效

第二种写法

vector<int>v3;
			v3.push_back(1);
			v3.push_back(2);
			v3.push_back(4);
			v3.push_back(3);
			v3.push_back(4);
			v3.push_back(5);
			vector<int> ::iterator it = v3.begin();
			while (it != v3.end())
			{
				if (*it% 2 == 0)
				{
					it = v3.erase(it);
				}
				else
				{
					it++;
				}
			}
			for (auto e : v3)
			{
				cout << e << " ";
			}
			cout << endl;

在这里插入图片描述

答案正确,在删除完元素后返回当前的pos,保证pos的位置正确(有些STL版本可能会缩容,导致pos为野指针),并且删除之后不能++pos的值。

综上所述,insert/erase pos位置的值后不要直接访问pos,因为此时迭代器已经失效。

2.6 其他接口

size_t capacity()const//返回容量大小
			{
				return _end_of_storage - _start;
			}
size_t size()const//返回有效元素的个数
			{
				return _finsh - _start;
			}

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

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

相关文章

10+编程语言实现云笔记

目标 为编程初学者打造入门学习项目&#xff0c;使用各种主流编程语言来实现。让想学编程的&#xff0c;一个都不落下。 上述基本涵盖了当前编程开发所有主流语言。 左侧为前端版本&#xff1a;安卓、iOS、鸿蒙、Flutter、Vue、uni-app。 右侧为服务器端版本&#xff1a;Jav…

代码随想录算法训练营三期 day 27 - 回溯 (3) (补)

39. 组合总和 题目链接&#xff1a;39. 组合总和 原文链接&#xff1a;39. 组合总和 视频链接&#xff1a;39. 组合总和 本题和 77.组合 &#xff0c;216.组合总和III 的区别是&#xff1a;本题没有数量要求&#xff0c;可以无限重复&#xff0c;但是有总和的限制。 树形结构&…

【axios】axios的基础知识和使用

一、基础知识概念Axios 是专注于网络数据请求的库,只负责发请求、拿数据&#xff0c;不能操作DOM元素。相比于原生的 XMLHttpRequest 对象&#xff0c;axios 简单易用。相比于 jQuery&#xff0c;axios 更加轻量化&#xff0c;不能操作DOM元素&#xff0c;只专注于网络数据请求…

cubeIDE开发, stm32人工智能开发应用实践(Cube.AI).篇二

一、事有蹊跷 接篇一&#xff0c;前面提到在使用cube.AI生成的c语言神经网络模型API调用时&#xff0c;输入数据数量是24&#xff0c;输出数据数量是4&#xff0c;但上文设想采集了三轴加速度传感器的x/y/z三个各数据&#xff0c;按Jogging(慢跑),Walking(走了)两种态势采集了两…

Java链表OJ题

目录1. 删除链表中等于给定值val的所有结点2. 逆置单链表3. 链表的中间结点4. 链表中倒数第k个结点5. 将两个有序链表合并为一个新的有序链表6. 以给定值x为基准将链表分割成两部分7. 判断是否为回文链表8. 两个链表的第一个公共结点9. 判断链表中是否有环10. 链表开始入环的第…

【Linux】目录权限和默认权限

上期介绍了Linux的文件权限&#xff0c;这期我们仔细来说说Linux环境下目录权限和默认权限一、目录权限1.1 进入目录所需的权限我们在进入目录时需要什么样的权限呢&#xff1f;是r、w还是x呢&#xff1f;下面我们一起来验证一下&#xff1a;&#x1f4cb;如下我门拥有全部目录…

Day11 AOP介绍

1 前言AOP&#xff0c;Aspect Oriented Programming&#xff0c;面向切面编程&#xff0c;是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象&#xff0c;一个对象包括静态的属性信息&#xff0c;包括动态的方法信息等。而AOP是横向的对不同事物的抽象&#xff0c;属性与属…

【Python从入门到精通】第一阶段

文章目录前言python的起源打印hello world注释变量变量基本概念类型类型转换运算符字符串拓展字符串的三种定义方法字符串拼接字符串格式化数据输入input比较布尔类型和比较运算符if判断if elseif elif else嵌套循环while循环while循环嵌套for循环range()的使用函数的使用函数的…

3小时精通opencv(五) 利用TrackBar进行颜色检测

3小时精通opencv(五) 利用TrackBar进行颜色检测 参考视频资源:3h精通Opencv-Python 本章内容介绍如何利用TrackBar调节色域, 手动提取到我们需要的颜色 文章目录3小时精通opencv(五) 利用TrackBar进行颜色检测创建Trackbar色彩检测创建Trackbar 在opencv中使用createTrackbar函…

C语言:数组

往期文章 C语言&#xff1a;初识C语言C语言&#xff1a;分支语句和循环语句C语言&#xff1a;函数 目录往期文章前言1. 一维数组的创建和初始化1.1 数组的创建1.2 数组的初始化2. 一维数组的使用3. 一维数组在内存中的存储4. 二维数组的创建和初始化4.1 二维数组的创建4.2 二维…

大数据技术架构(组件)7——Hive:Filter PushDown Cases And Outer Join Behavior

1.2、Filter PushDown Cases And Outer Join Behavior前提:关闭优化器set hive.auto.convertjoinfalse; set hive.cbo.enablefalse;Inner Join:1、Join On中的谓词: 左表下推、右表下推2、Where谓词:左表下推、右表下推-- 第一种情况: join on 谓词 selectt1.user_id,t2.user_i…

C++函数定义和调用介绍

C函数定义和调用介绍 函数的意义&#xff1a;利用率高&#xff0c;可读性强&#xff0c;利于移植。 一个C程序中主函数有且只有一个&#xff0c;是程序的入口&#xff0c;而函数&#xff08;或称子函数&#xff09;可以有很多。 每个 C 程序都至少有一个函数&#xff0c;即主…

2021 XV6 8:locks

实验有两个任务&#xff0c;都是为了减少锁的竞争从而提高运行效率。Memory allocator一开始我们是有个双向链表用来存储空闲的内存块&#xff0c;如果很多个进程要竞争这一个链表&#xff0c;就会把效率降低很多。所以我们把链表拆成每个CPU一个&#xff0c;在申请内存的时候就…

栈和队列的应用

一、栈在括号匹配中的应用 数据结构之栈_迷茫中的小伙的博客-CSDN博客_数据结构之栈栈括号和队列的应用 二、栈在表达式求值中的应用 中缀转 ->后缀 &#xff1a; 左右先 (左边能先算,先算左边,因为这样可以保证确定性,即计算机运算的方式) 后缀转->中缀 &#xff1a…

王者荣耀入门技能树-解答

前言 前段时间写了一篇关于王者荣耀入门技能树的习题&#xff0c;今天来给大家解答一下。 职业 以下哪个不属于王者荣耀中的职业&#xff1a; 射手法师辅助亚瑟 这道题选&#xff1a;亚瑟 王者荣耀中有6大职业分类&#xff0c;分别是&#xff1a;坦克、战士、刺客、法师、…

如何好好说话-第12章 理清楚问题就是答案

生活中该不该积极主动与别人展开社交活动&#xff1f;有些时候社交活动并不开心&#xff0c;仅仅只是无聊的闲才。但他确实能拉拢人际关系&#xff0c;帮我们获得近身套路。而且有一种观点认为不善于社交的人是不成功的。注意以上说的这些都是偏见。当我们站在一个更高的维度认…

Jetpack架构组件库:Hilt

Hilt Hilt 是基于 Dagger2 的依赖注入框架&#xff0c;Google团队将其专门为Android开发打造了一种纯注解的使用方式&#xff0c;相比 Dagger2 而言使用起来更加简单。 依赖注入框架的主要作用就是控制反转&#xff08;IOC, Inversion of Control&#xff09;, 那么什么是控制…

表格相关的一些标签

<!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>表格相关的标一些签</title> </head> <body> <!-- 需求 1&#xff1a;做一个四行&#xff0c;三…

Golang进阶

"白昼会边长&#xff0c;照亮心脏,让万物生长。"一、Golang进阶我们对golang的语法进行了一定的了解后&#xff0c;也算是入门了。本节的进阶篇围绕三个方向展开,Goroutine 、 Channel 、Sync。如何理解并行与并发&#xff1f;并行是指“并排行走”或“同时实行或实施…

用数组实现链表、栈和队列

目录前言一、用数组实现链表1.1 单链表1.2 双链表二、用数组实现栈三、用数组实现队列前言 众所周知&#xff0c;链表可以用结构体和指针来实现&#xff0c;而栈和队列可以直接调用STL&#xff0c;那为什么还要费尽心思用数组来实现这三种数据结构呢&#xff1f; 首先&#x…