C++STL详解(四)—— vector模拟实现

news2024/11/24 9:25:32

文章目录

  • vector内置成员变量
  • 默认成员函数
      • 初始化列表构造
      • 迭代器区间构造函数
      • 赋个数赋值构造函数
      • 赋值构造的相关问题
      • 拷贝构造函数
      • 赋值运算符重载函数
      • 析构函数
  • 迭代器及迭代器相关函数
      • begin和end
      • 范围for
  • 容量与扩容相关函数
      • size和capacity
      • reserve
      • resize
      • empty
  • vector中的增删查改:
      • push_back
      • pop_back
      • insert
      • erase
      • swap
  • 访问容器函数
      • operator[]
      • front和back

vector内置成员变量

_start: 容器的初始位置。

_finish: 容器最后一个数据的下一个位置。

_endofstorage: 指向容器的尾部,可比作容量。

这三个内置成员都代表有关vector位置的地址。
在这里插入图片描述

默认成员函数

初始化列表构造

vector()
			:_start(nullptr)  //初始值为nullptr
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
		}

迭代器区间构造函数

1: 在类模板里面又创建一个函数模板(为了能传递迭代器区间),这个模板最好使用除iterator之外的迭代器名字,因为可以传入多种容器数据类型,而不是只能像iterator 一样只能用vector容器数据类型。

2: 在迭代器区间构造本质是将其他容器的数据给定区间范围去构造一个新的vector,所以将该容器数据存放在vector之前一定要
将vector进行初始化,以防在push_back扩容时delete _start为
野指针。

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

赋个数赋值构造函数

C++对于内置类型也具有构造函数,它的默认构造函数的缺省值为0,val = T()就像相当于让内置类型的匿名对象去作缺省值,
在这里插入图片描述

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

赋值构造的相关问题

1:如果我们对vector 传入10时,同过int()作为缺省值可推断
出插入了10个0;
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
2:如果我们用10个1去构造vector v时,却发现程序报错。

3:如果我们用10个char类型时,却发现程序运行成功!

在这里插入图片描述
原因分析
情况1和情况3编译器调用构造匹配的时半缺省构造函数,只用传一个参数进行构造。

在这里插入图片描述
原因分析
情况2是因为我们传的实参为int,int类型,但是函数形参为unsign int ,int类型,编译器此时不会匹配半缺省值的构造
函数,而是会选择迭代器区间构造(实参类型相同),在传入的过程中模板参数
Inputlerator就会实例化为int,int 类型,最后在push_back中对
int类型解引用造成编译错误。
在这里插入图片描述
解决办法
我们可以增加一个与实参类型相匹配的函数重载,那么在调用时编译器就会调用相匹配的函数。

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

拷贝构造函数

传统写法
复用push_back()并选择范围for将v中的数据循环放入到新构造的vector中。

vector(const vector <T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			reserve(v.size());
			for (auto e : v)
			{
				push_back(e);
			}

现代写法
新开辟一块空间,将数据依次赋值到新拷贝的vector容器中(深拷贝),但是我们不能用memcpy进行拷贝(浅拷贝),因为对于内置类型,深拷贝和浅 拷贝没有什么问题。但是如果vector存的类型为自定义类型(例如vector)的时候,此时选择浅拷贝进而将该自定义类型依次拷贝到vector容器中,此时vector容器中的自定义类型中的_start以及被拷贝对象的_start都指向同一块空间,当程序运行完毕的时候,编译器分别调用两次析构函数,此时,同一块空间被销毁两次进而造成程序错误。所以,对于内置类型编译器调用值拷贝,对于内置类型赋值(为深拷贝)

//传统写法:
		vector(const vector<T>& v)
		{
			_start = new T[v.size()];
			for (int i = 0; i < v.size(); ++i)
        {
			_start[i] = v[i];//将vv中的vector<int>依次赋值给ret中,赋值为深拷贝。
			}
			_finish = _start + v.size();
			_end_of_storage = _start + v.size();
		
		}

赋值运算符重载函数

传统写法
首先要判断能不能给自己赋值,如果是给自己赋值则不需要操作,如果不是给自己赋值,则需要将原来的空间删除,重新开辟与vector 容器相同的空间,再将容器vector v 的数据一个个拷贝出来(深拷贝),最后更新vector中的内置成员即可。

vector<T>& operator=( const vector<T>& v)
{
        if( this != &v )
        {
           delete[] _start;
           _str = new T[v.capacity()];  //开辟一个和v同样大小的空间。
           for( size_t i = 0; i < v.size();++i)
           {
           //如果是内置类型就为值拷贝,如果是自定义类型就为深拷贝。
                    _start[i] = v[i];
           }
           _finish = _start + v.size(); //最后一个数据的下一个的下一个位置。
           _endofstorage = _start + v.capacity(); //容器的末尾。
        }=
           return *this;            //返回被赋值的类,并且能够支持连续赋值。
}

现代写法一
传值传参
现代写法在传参中没有引用传参,而是将实参传递的形参嗲用拷贝构造作为 ”中间人“ ,在函数调用结束结束时编译器会调用这个”中间人“的析构函数进行析构。

vector<T>& operator=( vector<int> v ) //传参时编译器调用拷贝构造函数,此时v为实参的拷贝。
{
          swap(v);
          return *thsi //支持连续赋值。
}

现代写法二
引用传参
现代写法中引用传参,我们还是需要这个"中间人" ,所以我们可以采用迭代器区间构造先构造一个与v相同的容器tmp,
然后再将tmp与自己交换就行了。

vector ( const vector<T>& v)
{
     //先迭代器区间构造。
     vector<T> tmp( v.begin(),v.end());
     swap(tmp);
}

析构函数

对容器进行析构时,我们首先要判断该容器是否为空,如果为空的话,我们就不需要删除,如果不为空,则需要先释放该容器空间的,再将该容器的内置成员设为nullptr即可。

~vector()
{
   //判断_start是否为空,防止对空指针进行释放。
   if( _start )
    {
         delete[] _start;
         _start = nullptr;
         _finish = nullptr;
         _endofstorage = nullptr;
    }
}

迭代器及迭代器相关函数

迭代器实际就是容器中指向数据类型的指针,在vector中分为普通迭代器和const 迭代器。(原因下面内容详解)

typedef T* iterator;
typedef const T* const_iterator;

begin和end

begin()和end()函数分别获得vector的首地址和最后一个数据的下一个位置的地址。

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

另外,因为普通迭代器iterator返回值既能读又能写,而vector容器中还规定了返回值只能读不能写的迭代器,为了让普通迭代器调用普通begin()和end(),让const迭代器调用const修饰的begin()和end().

const_iterator begin() const   //返回值不能被修改。
{
        return _start;
}
const_iterator end() const    //返回值不能被修改。
{ 
        return _finish;
}

范围for

当vector支持迭代器时,我们就可以使用范围for遍历容器了,
因为此时编译器会主动将范围for形式编译成迭代器形式。

//迭代器形式
vector<int> v( 6,6);
auto it = v.begin();
whil( it != v.end() )
{
     cout << *it <<" ";
}
cout <<endl;
//范围for形式
vector<int> v( 6,6);
for( e : v )
{
     cout <<e <<" ";
 }
 cout<<endl;

容量与扩容相关函数

size和capacity

在这里插入图片描述
由图可得,数据个数size可有_finish - _start可得,由于返回类型为size_t 类型的,编译器会主动将_fiish— _start的地址差转换为个数size。容量capacity由_endofstorage - _start获取。

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

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

reserve

当n实参n大于该对象当前的容量capacity时,将容量扩大到n或者大于n.
当传递的实参n小于对象的capacity时,不进行扩容。
reserve函数实现思路
扩容前首先要判断n与capacity的关系,当n>capacity,并且为空容器,则开辟新的空间,将原来的空间进行释放,更新内置成员。
当n > capacity,并且不为空容器,则需要对数据进行赋值拷贝,否则不用。然后将原来的内存空间进行释放,更新vector内置成员就行。

void reserve(size_t n)
{
     if ( n > capacity() )
     {
           size_t sz = size();
           //开辟新空间。
           T* tmp = new T[n];
           if( _start )                 //如果为空容器,则不用将旧数据转移到新空间中。
           {
               for ( int i = 0; i < size(); ++i )
               {
               //将就空间数据一个个赋值给新空间。
                    tmp[i] = _start[i];
               }
               //将就空间删除。
               delete[] _start;
           }
           //更新内置成员
           _start = tmp;
           _finish = _start + sz;
           _endofstorage = _start + n;
           
     }
}

注意
空指针问题
(1)在扩容之前应该提前保存当前容器数据的个数:
如果不提前保存,当vector容器为空调用push_back第一次扩容时,因为_start = tmp,
_finish = tmp + size(); 而size = _finish - _start;
经过计算的,此时的_finish = _finish; 而空容器的_finish = nullptr. 在之后的push_back解引用时会造成解用nullptr造成程序崩溃。
所以,我们必须保存原来的size(),不能让计算过程中size发生改变。

vector存储自定义而类型浅拷贝问题
在reserve过程中,拷贝旧数据也有可能发生自定义类型的浅拷贝,所以我们不能用memcpy函数进行拷贝,而应该采用赋值拷贝,此为深拷贝。

erserve后[]问题
使用reserve之后最好不能直接使用[]访问,因为resize开辟空间不会改变size,如果直接调用的话,很有可能会因为assert断言造成错误。

resize

规则
1: 如果n > capacity , 需要扩容 + 初始化。

2: 如果n > size 且 n <= capacity ,只需要在最后一个数据的下一个数据开始初始化。

3:如果 n < size, 删除数据。

void resize( size_t n, const T& val = T())
{
        if ( n > capacity() )
        {
              reserve(n);
        }
        //只需要初始化
        if( n > size())
        { 
             //从_finish开始,_start + n结束
             while( _finish < _start + n )
             {
                     *finish = val;
                     ++finish
             }
        }
//以上两个if包含了resize两种情况。
        else
        //只需要调整_finish就可以了,循环遍历的时候就访问不到了。
        {
            _finish = _start + n;
        }
}

empty

如果vector容器中_start 等于 _finish就说明容器为空。

bool empty() const
{
      return _start==_finish;
}

vector中的增删查改:

push_back

尾插首先要判断容器是否已满,如果没满则只需要将数据尾插,如果满了得先扩容再将数据进行尾插。

void push_back( const T& x )
{
       if( _finish == _endofstorage )
       {
            //如果容量为null,则容量直接为4,如果不为空,则直接扩容两倍。
             size_t newcapacity = capacity() == 0? 4:2*capacity();
             reserve(newcapacity);
       }
       *_finish = x;
       ++_finish;
}

pop_back

尾删数据之前必须得判断容器是否为空,如果为空则断言,如果不为空则_finish–让这个数据无法访问即可。

void pop_back() 
{
//容器的数据的个数不能为0;
     assert( !empty());
     --_finish;
{

insert

inser函数可以指定位置插入数据,在插入之前一定要考虑插入的范围插入并且一定要判断是否需要扩容,pos位置后面的数据(包括pos)统一向后移动一位,最后将数据插入到pos位置就可以了。

 iterator insert( itderator pos, const T& x)
 {
         //范围一定要在_start和_finish之间,左闭右闭区间。
         assert(pos >=_start);
         assert(pos <= _finish);
         if( _finish == _end_of_storage )
         {
                size_t len = pos - _start;
                reserve( capacity() ==0? 4:2*capacity()*2);
                pos = _start + len;
         }
         iterator end = _finish;
         while( end >= pos )
         {
               *end = *(end-1);
               end--;
         }
         *pos = x;
         ++_finish;
         return pos;
 }

erase

erase函数可以删除pos位置的数据,在删除时判断vector容器必须有数据并且删除位置必须合法。然后将数据从pos+1位置开始,将前一个数据前移删除的,最后更新以下_finish就行。

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

swap

为了vector中拷贝构造现代写法中的交换自定义类型,我们们可以复用std中的vector写出合适的交换函数,对于自定义类型交换,最后不要直接用std库里面的。

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

访问容器函数

operator[]

vector容器支持用户使用下标进行访问,返回值是对访问数据的引用,如果是const修饰的容器,则调用返回值只可读不可以写的操作符,如果是普通容器,则说明它的数据既可以读也可以写。


```cpp
T& operator[](size_t i)
{
	assert(i < size());              //访问下标应该小于size下标。 

	return _start[i]; 
}
const T& operator[](size_t i)const
{
	assert(i < size());             //访问下标应该小于size下标。 
	return _start[i]; 
}


front和back

在C++11中,vector容器还支持获取迭vector容器中的头元素和尾元素支持可读可写,前提是必须要有数据,当然如果返回的首尾数据可读而不可写秩序要加上const就行。

        T& front()
		{
			assert(size() > 0);
			return *_start;
		}
		T& back()
		{
			assert(size() > 0);
			return *(_finish - 1);
		}
//const v容器调用
      const T& front() const
		{
			assert(size() > 0);
			return *_start;
		}
		const T& back()  const
		{
			assert(size() > 0);
			return *(_finish - 1);
		}

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

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

相关文章

「攻略手册」:ShardingSphere 与 Java 应用性能优化

笔者曾经写过一篇文章&#xff0c;介绍 ShardingSphere 在具体代码细节上的优化案例&#xff0c;但文章中没有介绍如何发现代码优化点。本文将结合之前笔者在 ShardingSphere 相关性能问题排查、优化经验&#xff0c;简要地介绍 ShardingSphere 性能问题排查、优化应如何入手。…

解决Sql WorkBench中数据库不能重命名的问题

解决Sql WorkBench中数据库不能重命名的问题mysql不支持直接重命名数据库1. 连接到数据库2. 打开菜单&#xff0c;选择迁移向导3. 点击Start Migration4. 填写源数据库的相应参数5. 填写目标数据库的响应参数6. 稍等片刻&#xff0c;点击Next7. 选择你要迁移的数据库。8. 进入一…

B站依然面临巨大风险,盈利之路可能会更加艰难

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 哔哩哔哩(BILI)虽然得到了阿里巴巴(BABA)和腾讯(00700)的支持&#xff0c;在扩大和多样化用户数量方面也取得了巨大的成绩。但哔哩哔哩还在继续亏损&#xff0c;随着国家的监管环境朝着对游戏行业有利的方向变化&#xff0…

【案例教程】GAMS电力模型

本专题主要针对电力系统领域中比较典型的优化问题、优化方法及其在GAMS中的实现进行分析。共分为五个部分&#xff0c;包括电力系统机组组合专题、最优潮流专题、水电优化运行专题、鲁棒优化和多能源互补优化专题、分布鲁棒优化专题等&#xff0c;从基本模型到复杂模型逐步深入…

MySql数据类型都是字符串(varchar),数据类型一样,但是联表查询不走索引,是什么原因呢

大家都知道,如果联表查询中,数据类型不一样,是很有可能不走索引的,但是有时候数据类型一样也是有可能不走索引的,我们往下看1 数据准备我们准备两个表,用来模拟联表查询1.1 user表CREATE TABLE user (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键,phone varchar(20) DE…

华为OD机试用Python实现 -【几何平均值最大子数组】| 2023年3月被抽中

华为OD机试题 最近更新的博客华为 OD 机试 300 题大纲几何平均值最大子数组题目描述输入描述输出描述说明示例一输入输出说明示例二输入输出说明Python 代码实现核心逻辑最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单</

测试只能干到35岁?35岁+的测试就会失业?

互联网行业在很多年轻人的眼中&#xff0c;是高薪的象征。前几年的软件测试行业还是一个风口&#xff0c;随着不断地转行人员以及毕业的大学生疯狂地涌入软件测试行业&#xff0c;但是现在裁员潮涌现的时候&#xff0c;互联网行业首当其冲&#xff0c;互联网企业大量的裁员&…

当你开始学习 Python 时,这是一个简单的学习计划及当你初学 Python 时,这里有几个建议

当你开始学习 Python 时&#xff0c;这是一个简单的学习计划&#xff1a; 1.入门 安装Python环境并熟悉基本的Python语法 熟悉Python的基本数据类型&#xff0c;例如数字、字符串和列表 学习控制流程&#xff0c;例如条件语句和循环语句 掌握函数和模块的基本知识 2.进阶 …

Qt开发基本步骤示例:输入半径显示圆的面积

目录 1. 创建一个新项目 1.1 创建类的基类 1.2 main.cpp代码释义 2. 代码写在哪&#xff1f; 2.1 怎么找到我们需要的函数&#xff1f; 1. 创建一个新项目 点击创建项目&#xff0c;开始创建&#xff1a; 1.1 创建类的基类 QMainWindow&#xff1a;带菜单栏的窗口QWidge…

第一章——冯·诺伊曼结构计算机工作原理及层次结构分析

&#x1f3e1;个人主页 &#xff1a; 守夜人st &#x1f680;系列专栏&#xff1a;计算机组成原理 …持续更新中敬请关注… &#x1f649;博主简介&#xff1a;软件工程专业&#xff0c;在校学生&#xff0c;写博客是为了总结回顾一些所学知识点 目录第一章——冯诺伊曼结构计算…

深入浅出RPC框架-学习笔记

1 基本概念 1.1 本地函数调用 1.2 远程函数调用 1.3 RPC概念模型 5个模型组成&#xff1a;User、User-Stub、RPC-Runtime、Server-Stub、Server 1.4 一次RPC的完整过程 1.4.1 IDL (Interface description language)文件 IDL通过一种中立的方式来描述接口&#xff0c;使得在不…

JavaScript(1)

JavaScript简介 JavaScript是一门跨平台、面向对象的脚本语言&#xff0c;用来控制网页行为的&#xff0c;它能使网页可以交互。 JavaScript引入方式 1、内部脚本 将js代码定义在HTML页面中&#xff0c;在HTML中&#xff0c;JavaScript代码必须位于<script>与</scrip…

用C语言写一个自己的shell-Part Ⅱ--execute commands

Part Ⅱ–execute commands Exec This brings us to the exec family of functions. Namely, it has the following functions: execlexecvexecleexecveexeclpexecvp For our needs,we will use execvp whose signature looks like this int execvp(const char *file, cha…

【数据库专题】数据库Mongodb之深入认知云计算三种服务方式、mongodb特点、mongodb重要进程 mongod、mongo、其他进程区别

文章目录一、什么是云计算1. IaaS:基础设施即服务2. SaaS:软件即服务3. PaaS:平台即服务二、大数据与云计算关系三、什么是MongoDB四、大数据与MongoDB五、MongoDB特点六、安装MongoDB七、重要进程介绍7.1 mongod进程7.2 mongo进程7.3 其他进程7.3.1 mongodump重建数据库7.3.2 …

解决封号 Walmart最全申诉步骤

最近龙哥听说不少平台的账号都被封掉&#xff0c;登不上去了。所以龙哥赶紧就把这篇Walmart申诉教程提上日程&#xff0c;以防这个不时之需哈。当时Walmart的这个封号申诉规则和社交平台的还是有很大区别的&#xff0c;今天龙哥就从封号原因和申诉流程两方面展开&#xff0c;让…

210 裸机程序烧录

一、驱动安装 1.1 dnw驱动安装 禁用win10驱动程序强制签名 设置 -> 更新和安全 -> 恢复 -> 立即重启 -> 疑难解答 -> 高级选项 -> 启动设置 -> 重启 -> 按提示输入“F7”硬件设备正常上电工作&#xff0c;插入USB线连接电脑&#xff0c;设备管理器识…

源表测试软件下载安装教程

软件&#xff1a;源表测试软件NS-SourceMeter 语言&#xff1a;简体中文 环境&#xff1a;NI-VISA 安装环境&#xff1a;Win10以上版本&#xff08;特殊需求请后台私信联系客服&#xff09; 硬件要求&#xff1a;CPU2GHz 内存4G(或更高&#xff09;硬盘500G(或更高&#xf…

BGP之BGP联邦综合实验

目录 BGP联邦综合实验 实验图 网段划分 基础配置 路由配置 启动AS2中的ospf--- IGP协议 检测IGP 启动AS之间的BGP 测试 发布R1路由 修改&#xff1a;下一跳解决上述问题 解决水平分割 发布R8路由 测试 AS2内部环回路由信息互相访问 配置空接口 发布静态路由 测试…

经典蓝牙Sniff Mode

文章目录IntroductionApplicationSniff Sub-ratingReferenceIntroduction Sniff mode为两个已连接的经典蓝牙设备提供了有效的降低功耗的方法。我们知道&#xff0c;当没有数据需要传输的时候&#xff0c;两个已连接的蓝牙设备之间也需要每两个slots完成一次POLL packet - NUL…

系列九、视图/存储过程/存储函数/触发器

一、视图 1.1、概述 视图&#xff08;View&#xff09;是一种虚拟存在的表。视图中的数据并不在数据库中实际存在&#xff0c;行和列数据来自定义视图的查询中使用的表&#xff0c;并且是在使用视图时动态生成的。 通俗的讲&#xff0c;视图只保存了查询的SQL逻辑&#xff0c;…