【STL学习之路】vector的模拟实现

news2024/9/24 9:19:27

文章目录

  • 一、接口总览
  • 二、vector成员变量
  • 三、默认成员函数
    • 构造函数① -- 默认无参构造
    • 构造函数② -- 迭代器区间构造
    • 构造函数③ -- n个val构造
    • 拷贝构造函数
    • 赋值运算符重载
    • 析构函数
  • 四、迭代器
  • 六、容量以及元素访问的相关接口
    • empty
    • size和capacity
    • reserve
    • resize
  • 七、增删查改等接口
    • push_back
    • pop_back
    • insert
    • erase
    • operator[]
    • swap
    • front 和 back


一、接口总览

namespace ky
{
	template<typename T>
	class vector
	{
	public:
		//迭代器
		typedef T* iterator;
		typedef const T* const_iterator;
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;
        //默认成员函数
		vector();//默认构造
        vector(size_t n,const T& value =T());//n个val构造
        //迭代器区间构造
        template<typename inputIterator>
		vector(inputIterator first,inputIterator last);
        ector(const vector<T>& v);//拷贝构造
		vector<T>& operator=(vector<T> v);//赋值重载
		~vector();//析构
		
		//容量相关
		size_t size() const;
		size_t capacity() constbool empty() const;
		void reserve(size_t n);
		void resize(size_t n,const T& value= T());
	
		//增删查改
		void push_back(const T& x);//尾插
        void pop_back();//尾删
		iterator insert(iterator pos, const T& x);//插入
		iterator erase(iterator pos);//删除
        void swap(vector<T>& v);//交换函数
		T& operator[](size_t pos);
		const T& operator[] (size_t pos) const;
		T& front();
		T& back();
	private:
		iterator _start;		//指向数据块开始
		iterator _finish;		//指向有效数据块末尾
		iterator _end_of_storage;	//指向存储容量的尾
	};
}

二、vector成员变量

image-20221231174354220

  • _start指向容器的头部
  • _finish指向有效数据的尾部(下一个即将放入数据的地方)
  • _end_of_storage指向整个容器的尾(容量的末尾)

三、默认成员函数

构造函数① – 默认无参构造

首先要有一个无参的默认构造,只需要把三个成员变量初始化为空指针即可

//直接初始化列表初始化
vector() 
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr) 
   {}

构造函数② – 迭代器区间构造

迭代器区间构造:利用某一段迭代器区间构造初始化vector。

注意:需要设置为模板函数,因为该迭代器区间可以是任意其他容器的迭代器区间。然后只需要把迭代器区间的值依次push_back到vector即可

template<typename inputIterator>
vector(inputIterator first,inputIterator last)
	:_start(nullptr)
    ,_finish(nullptr)
    ,_end_of_storage(nullptr)
{
    while (first != last)
    {
        push_back(*first);	//尾插
        ++first;
    }
}

构造函数③ – n个val构造

有时候想要用n个相同的值 val来构造一个vector,所以vector提供一个这样的构造函数。

先开辟n个空间,然后依次push_back来尾插n个val即可

vector(size_t n,const T& value =T())	
    :_start(nullptr)
    ,_finish(nullptr)
    ,_end_of_storage(nullptr)
{
     //开空间
     reserve(n);
     //开好空间后直接尾插n次
     for (size_t i = 0;i<n ; ++i)
     {
         push_back(value);
     }
}

注意

n个val构造必须加一个重载函数!

//n为int的重载函数
vector(int n, const T& value = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	//开空间
	reserve(n);
	//开好空间后直接尾插n次
	for (int i = 0; i < n; ++i)
	{
		push_back(value);
	}
}

为什么呢?

因为对于vector<int> v(10,1)这个代码来说,我们想用10个1构造一个vector,但是因为10和1显然会被编译器识别为同一种类型,他认为(10,1)更匹配的是一个迭代器区间,而n个val的构造函数中,n是size_t类型,编译器会选择参数最匹配的函数进行调用,因此实际上这个代码片段调用的是构造函数②,就会发生 非法的间接寻址错误

拷贝构造函数

拷贝构造就要涉及到深拷贝问题了,因为vector中的元素类型,既可能是普通的内置类型,也有可能是自定义类型,对于自定义类型,如果只是浅拷贝的话,那么可能在析构的时候发生同一块空间析构两次的情况从而导致崩溃。

写法1:传统写法

传统写法就是,自己去开空间,然后把要复制的对象中的元素一个一个赋值拷贝过来

vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	_start = new T[v.size()];		//开_capacity个大小也可以
	for (size_t i = 0; i < v.size(); ++i)
    {
        _start[i] = v._start[i];
    }
	_finish = _start + v.size();		//更新结尾和容量
	_end_of_storage = _start + v.size();
}

注意:在拷贝的时候不可以使用memcpy!因为memcpy是一种浅拷贝,当vector中的元素是int等内置类型不会发生错误,但是当vector中的元素是需要进行深拷贝类型的时候,使用memcpy就是存在问题的。例如:对<vector<vector<int>>进行拷贝

比如:vector中的元素 都是一个vector<int>类型,每一个vector<int>中都指向一块连续的空间

image-20221231181956770

如果使用memcpy对vector进行拷贝,那么所拷贝出来的vector的空间是新开的空间,但是所拷贝的vector中每一个vector的内容都是和原来一摸一样的,也就是说,指针都完全一样,指向同一块连续空间。那么势必会导致析构两次而崩溃的问题,如下图所示:

image-20221231183119116

正确的写法就是,对于新开辟的空间中的每一个元素,使用 = 运算符依次拷贝过来,因为赋值重载实现的是深拷贝
正确的拷贝结果应该是这样:
在这里插入图片描述

所以,对于memcpy千万要慎用!对于内置类型或者不需要开辟动态数组的一些类型,可以使用,但是如果像string,vector等类型,就不能使用memcpy!

写法2

直接先开辟好空间,然后依次把要拷贝的尾插进来即可

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

写法3:工具人写法

创建一个临时对象,用迭代器区间构造要拷贝的对象,然后交换一下内部的三个指针即可!同时交换后,临时变量会自动在函数退出的时候销毁。

vector(const vector<T>& v)
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{
	vector<T> tmp(v.begin(), v.end());	//迭代器区间构造
	swap(tmp);							//然后让此对象和tmp交换
}

赋值运算符重载

赋值运算符和拷贝构造类似,也涉及到深拷贝问题

传统写法

  1. 首先判断是不是自己给自己赋值,如果是就不用操作
  2. 如果不是,首先释放旧的空间
  3. 开辟新空间
  4. 利用赋值运算符 一个个把数据拷贝过来(不能使用memcpy!)
  5. 更新_finish和 _end_of_storage的值
vector<T>& operator=(const vector<T>& v)
{
	if (this != &v)
	{
		//1.删除
		//2.开空间
		//3.拷贝
		delete[] _start;
		_start = new T[v.size()];//开空间
		for (size_t i = 0; i < v.size(); ++i)
		{
			_start[i] = v._start[i];
		}
		_finish = _start + v.size();//更新_finish和 _end_of_storage
		_end_of_storage = _start + v.size();
	}
	return *this;
}

现代写法

和拷贝构造的第三种方法类似,形参直接不用引用,把形参作为临时对象,在传参的时候就把要拷贝的对象拷贝构造给形参了,然后让本对象和临时对象交换,最后返回*this即可

//v(v1)   传参的时候 v1就拷贝构造给了tmp
vector<T>& operator=(vector<T> tmp)//值形参作为tmp
{
	swap(tmp);//传参的时候,拷贝构造给了形参v
	return *this;
}	

析构函数

析构的时候,直接就释放_start所指向的连续空间

然后把三个成员变量赋值为nullptr即可

~vector()
{
	delete[] _start;	//都是指向的同一块内存,只释放开头即可
	_start = _finish = _end_of_storage = nullptr;
}

四、迭代器

vector中,底层的实现也是数组,是连续的,所以迭代器就是原生指针

迭代器的声明

typedef T* iterator;
typedef const T* const_iterator;

begin和end

iterator begin()
{
    return _start;//返回容器的第一个元素的迭代器
}
iterator end()
{
    return _finish;//返回最后一个元素的下一个位置的迭代器
}
//下面是针对const容器来调用的,返回之后只能读不能修改!
const_iterator begin() const
{
    return _start;
}
const_iterator end() const
{
    return _finish;	
}

此时再回看迭代器:实际上就是利用指针访问数组

int arr[] = {1,3,5,7,9};
vector<int> v(arr,arr+sizeof(arr)/sizeof(int));
vector<int>::iterator it = v.begin();
while(it!=v.end())
{
    cout << *it << " ";
}
cout << endl;

支持了迭代器,自动就支持了范围for,因为范围for就是傻瓜式的替换,底层就直接把上面的迭代器替换成了下面的范围for,如果你把begin函数改成Begin都会发生报错!(真·傻瓜式替换)

int arr[] = {1,3,5,7,9};
vector<int> v(arr,arr+sizeof(arr)/sizeof(int));
for(const auto& i : v)
{
    cout << i <<" ";
}
cout << endl;

六、容量以及元素访问的相关接口

empty

判断是否为空,显然当_start 和 _finish相等的时候vector为空

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

size和capacity

size就是有效数据的个数,等于 _finish - _start(指针-指针得到中间元素的个数)

capacity就是总的容器容量,等于 _end_of_storage - _start

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

reserve

reserve(n)

  1. 当n大于当前的capacity的时候,将capacity扩容到n
  2. 当n<=capacity的时候,什么都不做

实现:首先判断n是否大于capacity,如果大于,就开辟大小为n的空间,然后,把原来的数据拷贝到新开辟的空间,最后释放旧空间即可。

void reserve(size_t n)
{
	//如果大于当前容量,才会扩容
	if (n>capacity())
	{
		T* tmp = new T[n];	//create new
		size_t len = size();	//记录原长度
		//如果原空间不为空
		if (_start)
		{
 //拷贝 不可以用memecpy!!  (因为涉及到一些更深层次的拷贝)
			for (size_t i = 0; i < len; ++i)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;	//free old

		}
        //更新成员变量
		_start = tmp;	//new head
		_finish = _start + len;	//new finish
		_end_of_storage = _start + n;	//new end_of_storage
	}
}

注意:

1、需要先记录原来的长度,因为在扩容之后,原来的空间被释放了,新的 _start变成了新开辟的空间的首地址,而 _finish = _start+size_end_of_storage = _start + capacity

所以需要提前记住原来的size才可以进行更新。否则如果在 _finish没有更新的情况下 利用size()函数通过 _finish-_start的方式计算有效数据个数,得到的就是一个随机值!

2、 拷贝的时候,需要把原空间的每一个元素利用 赋值 =来拷贝每一个元素,必须一个一个拷贝,不可以用memcpy。(如果使用memcpy进行拷贝,那么就是浅拷贝,如果是深拷贝类型,那么就会出现两个指针指向同一块空间的问题),而在reserve的时候,是需要及时释放掉旧空间的,这样旧空间释放了,而拷贝出来的每一个元素中的指针也都指向了一块被释放的空间,再进行访问就是非法野指针访问了!

image-20221231213732963

正确的拷贝应该是这样的:

image-20221231213937882

resize

resize(size_t n,const T& val = T())

注意,C++中对内置类型进行了升级,内置类型也有了默认构造,如int()为0,double为 0.0

1、如果n>当前的size,就用val把size扩大到n。如果val没有给,那么默认用vector存储的元素类型的默认构造进行初始化,如:int就用0,string就用空字符。如果val给了,就用所给的val扩大。

2、 如果n == 当前size,什么也不做

3、 如果n < 当前size,就删除元素,使得size变成n

resize的时候,我们还需要进行判断n是否大于当前的capacity。如果大于当前的capacity,优先进行扩容!

void resize(size_t n,const T& value= T())
{
	//1.n >= capacity
	//2. size <= n < capacity
	//3. n < size
	if (n > capacity())		//扩容+初始化
	{
		reserve(n);			
	}

	if (n > size())		//初始化
	{
		while (_finish < _start + n)
		{
			*_finish = value;
			++_finish;
		}
	}
	else		//删除元素
	{
		_finish = _start + n;		//n作为size()
	}
}

七、增删查改等接口

push_back

既然要插入数据,首先判断是否需要扩容。然后把数据插入到 _finish位置,++ _finish即可

void push_back(const T& x)
{
	//check capacity
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : 2 * capacity());
	}
	*_finish = x;	//添加数据
	++_finish;
}

pop_back

删除数据,首先检查是否为空,如果为空就不需要删除了

如果不为空,直接 – _finish即可!

void pop_back()
{
    assert(_finish > _start);
    --_finish;
}

insert

insert(pos,val):在所给迭代器位置插入一个元素val

1、首先检查是否需要扩容,和pos的合法性。

2、要提前把size记录下来,因为如果需要发生扩容,pos这个迭代器就被释放了,就找不到该位置了!

3、然后把依次把包括pos往后的元素都向后挪动一格,给pos位置腾出位置,插入val即可

注意:还要有返回值,返回新插入元素位置的迭代器

iterator insert(iterator pos, const T& x)
{
    assert(pos >= _start);
    assert(pos <= _finish);
    size_t len = pos - _start;

    //检查扩容	
    if (_finish == _end_of_storage)
    {
        reserve(capacity() == 0 ? 4 : 2 * capacity());
        //如果扩容,可能导致原pos指向的位置已经被释放了,所以需要重新计算pos
        pos = _start + len;	//pos等于新的_start+len
    }

    //挪动数据
    iterator end = _finish;
    while (end > pos)
    {
        *end = *(end - 1);
        --end;
    }

    //插入数据
    *pos = x;
    ++_finish;	//末尾+1
    return pos;
}

erase

erase(pos):删除迭代器pos位置的元素

1、首先检查pos的合法性

2、然后依次把pos位置后面的元素向前挪动一格,进行覆盖删除

注意:返回删除位置的下一个位置的迭代器(还是pos)

iterator erase(iterator pos)
{
    assert(pos >= _start);
    assert(pos < _finish);
    //挪动数据
    iterator begin = pos+1;
    while (begin < _finish)
    {
        *(begin - 1) = *begin;
        ++begin;
    }
    --_finish;
    return pos;		//STL中就是返回删除元素的后面一个元素
}

operator[]

operator[]保证了vector可以像数组一样访问,其实就是指针的解引用

T& operator[](size_t pos)
{
    assert(pos < size());
    return *(_start + pos);
}
//const对象用的:
const T& operator[] (size_t pos) const
{
    assert(pos < size());
    return *(_start + pos);
}

swap

交换函数:交换vector的每一个成员变量

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

::swap指的是在全局范围内找swap,其实就是调用了std中的进行内置类型变量的交换,因为我们包了头文件,已经展开了~,所以不用写std::,直接写 :: 即可

front 和 back

front返回第一个元素,back返回最后一个元素

T& front()
{
    return *_start;
}
T& back()
{
    return *(_finish - 1);
}

注意:返回引用,这样函数返回值可以直接进行修改~

vector<int> v(5,3);
v.front()*=10;
for(auto i : v)
    cout << i <<" ";
cout << endl;

//输出:30 3 3 3 3 

在这里插入图片描述

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

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

相关文章

async await 的基础使用和实现原理

async await 使用基础原理 async/await用法 其实你要实现一个东西之前&#xff0c;最好是先搞清楚这两样东西 这个东西有什么用&#xff1f; 这个东西是怎么用的&#xff1f; 有什么用&#xff1f; async/await的用处就是&#xff1a;用同步方式&#xff0c;执行异步操作&…

商会机构源码模板系统包含了信息管理、新闻管理、广告管理、系统管理等功能 v3.9

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 XYCMS商会机构源码模板系统是以aspaccess进行开发的商会网站源码&#xff0c;包含了信息管理、新闻管理、广告管理、系统管理等功能。 XYCMS商会机构源码模板系统功能简述&#xff1a; 商…

大数据面试题Spark篇(1)

1.spark数据倾斜 数据倾斜俩大直接致命后果&#xff1a;Out Of Memory&#xff0c;运行速度慢。这主要是发生在Shuffle阶段。同样Key的数据条数太多了。导致了某个key所在的Task数据量太大了&#xff0c;远远超过其他Task所处理的数据量。 数据倾斜一般会发生在shuffle过程中…

使用Eclipse开发第一个Java程序

虽然在《使用记事本编写运行Java程序》一节中已经开发过一个 Java 程序&#xff0c;但是那毕竟是通过记事本创建的。在上一节《Java Eclipse下载安装教程》中&#xff0c;我们已经安装了 Eclipse 工具&#xff0c;因此本节将介绍如何通过 Eclipse 开发 Java 程序。 在 Eclipse …

SD存储卡介绍

SD存储卡简介 SD存储卡是一种基于半导体快闪记忆器的新一代记忆设备&#xff0c;由于它体积小、数据传输速度快、可热插拔等优良的特性&#xff0c;被广泛地于便携式装置上使用&#xff0c;例如数码相机、平板电脑、多媒体播放器等。 SD存储卡实物图 SD存储卡特点 1、高存储容…

痞子衡嵌入式:Farewell, 我的写博故事2022

-- 题图&#xff1a;苏州荷塘月色 2022 年的最后一天&#xff0c;写个年终总结。困扰大家三年之久的新冠疫情终于在 12 月全面放开了&#xff0c;痞子衡暂时还没有阳&#xff0c;计划坚持到总决赛。对于 2023 年&#xff0c;痞子衡还是充满期待的&#xff0c;慢慢要恢复到 2019…

Codewars 你虐我千百遍,我待你如初恋

本人最近接触了Codewars啥的&#xff0c;没什么见识哈哈哈哈&#xff0c;刚开始看不懂啥的&#xff0c;到后面看多了其实也还好。我是小白轻点喷&#xff01;&#xff01;&#xff01;接下来就让我展示第一次写文章的历程吧&#xff0c;showTime&#xff1a; 第一位嘉宾A出场 …

2022年终总结:生活就像一道过山车

又到了年末&#xff0c;今年的年终总结我考虑了很久&#xff0c;到底要不要写&#xff1f;可以写些什么&#xff1f; 今年过得十分匆忙&#xff0c;我一直在赶路&#xff0c;但事实上今年内做完的&#xff0c;能说出口的事可以说没有。 回顾下去年对今年的期望&#xff1a; 有…

魔幻2022,2023涅槃重生!

前言&#xff1a; 大家好&#xff0c;按照惯例&#xff0c;每年的年尾&#xff0c;这个时候我都会进行复盘&#xff0c;这是自己第4个年头进行年度复盘&#xff1a;总结2019&#xff0c;展望2020&#xff01;不管过去如何&#xff0c;未来我们都要奋力前行&#xff01;复盘2021…

01月份图形化三级打卡试题

活动时间 从2023年 1月1日至1月21日&#xff0c;每天一道编程题。 本次打卡的规则如下&#xff1a; &#xff08;1&#xff09;小朋友每天利用10~15分钟做一道编程题&#xff0c;遇到问题就来群内讨论&#xff0c;我来给大家答疑。 &#xff08;2&#xff09;小朋友做完题目后&…

一个词语总结2022,你的是什么? | 2022 年度总结

一个词语总结2022&#xff0c;你的是什么&#xff1f; | 2022 年度总结前言回顾2022蓄力Unity 可寻址系统Unity 发布微信小游戏Unity 发布抖音小游戏Unity Mac AppStore内购Unity 后期处理蓄势付费专栏联袂推荐签约作者年度回忆展望20232023flag前言 2022马上即将过去。近期各…

机器学习理论和定理

在机器学习中&#xff0c; 有一些非常有名的理论或定理&#xff0c; 对理解机器学习的内在特性非常有帮助&#xff0e; 1. PAC学习理论 当使用机器学习方法来解决某个特定问题时&#xff0c; 通常靠经验或者多次试验来选择合适的模型、 训练样本数量以及学习算法收敛的速度等…

【MyBatis】如何使用“注解”来获取参数,以及其他方式?(底层原理解释)

目录 一、获取参数办法 1.1、多个参数时&#xff0c;直接传参&#xff0c;通过arg0...或param1...实现 1.2、通过传入map实现 1.3、实体化类对象传参 二、使用注解获取参数&#xff08;最实用&#xff01;建议经常使用&#xff09; 三、建议&#xff01;&#xff01;&…

算法设计与分析复习01:主方法求递归算法时间复杂度

算法设计与分析复习01&#xff1a;主方法求递归算法时间复杂度 文章目录算法设计与分析复习01&#xff1a;主方法求递归算法时间复杂度复习重点算法复杂度分析——主方法例题1&#xff1a;例题2&#xff1a;例题3&#xff1a;复习重点 算法复杂度分析——主方法 T(n)aT(nb)f(n)…

【Kotlin 协程】协程中的多路复用技术 ② ( select 函数原型 | SelectClauseN 事件 | 查看挂起函数是否支持 select )

文章目录一、select 函数原型二、Select clause 事件1、SelectClause0 事件代码示例2、SelectClause2 事件代码示例三、查看挂起函数是否支持 select一、select 函数原型 在上一篇博客 【Kotlin 协程】协程中的多路复用技术 ① ( 多路复用技术 | await 协程多路复用 | Channel …

Qt之线程运行指定函数(含源码+注释,优化速率)

一、线程示例图 下图包含三种不同方式启动线程的示例图和各自运行速率的对比&#xff1b;C线程的启动方式包括阻塞运行和异步运行&#xff0c;可以从C线程启动按钮看出两者区别&#xff0c;异步启动时按钮文本立即更新&#xff0c;当阻塞启动时按钮文本在线程运行完成后更新&a…

mybatis处理返回结果集

结果处理 1 简单类型输出映射 返回简单基本类型 //查询管理员总数 int adminCount(); <select id"adminCount" resultType"int">select count(*) from admin </select> 返回结果需要定义后才能使用简称 eg&#xff1a;resultType"Adm…

个人信息保护合规建设桔皮书

声明 本文是学习个人信息保护合规建设桔皮书. 下载地址而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 个人信息保护建设思考 识别监管要求聚焦安全保护重点 1. 个人信息处理原则合规 《个人信息保护法》总则中规定了个人信息处理的合法、正当、必要…

应用torchinfo计算网络的参数量

1 问题定义好一个VGG11网络模型后&#xff0c;我们需要验证一下我们的模型是否按需求准确无误的写出&#xff0c;这时可以用torchinfo库中的summary来打印一下模型各层的参数状况。这时发现表中有一个param以及在经过两个卷积后参数量&#xff08;param&#xff09;没变&#x…

从socket开始讲解网络模式(epoll)

从socket开始讲解网络模式 windows采用IOCP网络模型&#xff0c;而linux采用epoll网络模型&#xff08;Linux得以实现高并发&#xff0c;并被作为服务器首选的重要原因&#xff09;&#xff0c;接下来讲下epoll模型对网络编程高并发的作用 简单的socket连接 socket连接交互的…