【C++】:vector容器的底层模拟实现&&迭代器失效&&隐藏的浅拷贝

2024-06-04 7785阅读

目录

  • 💡前言
  • 一,构造函数
    • 1 . 强制编译器生成默认构造
    • 2 . 拷贝构造
    • 3. 用迭代器区间初始化
    • 4. 用n个val值构造
    • 5. initializer_list 的构造
    • 二,析构函数
    • 三,关于迭代器
    • 四,有关数据个数与容量
    • 五,交换函数swap
    • 六,赋值拷贝
    • 七,[ ]运算符
    • 八,预留空间(扩容)
      • 8.1 使用memcpy拷贝问题(重点)
      • 九,尾插和尾删数据
      • 十,插入数据 insert
      • 十一,删除数据 erase
      • 十二,insert和erase的迭代器失效问题(重点)
        • 1. 什么是迭代器失效?
        • 2. insert 的迭代器失效
          • 2.1 insert 内部pos位置的失效
          • 2.2 insert 以后外部的实参失效
          • 3. erase 以后的迭代器失效

            💡前言

            点击跳转到文章:vector容器的基本使用

            上篇文章已经介绍了vector容器的基本使用,这篇文章主要选择vector中一些核心的,基本的接口进行模拟实现。

            注意:由于我们模拟实现时使用了类模板,所以不建议进行文件分离,不然会产生链接错误。所以我们把函数都写在.h文件中,在Test.cpp文件中进行测试。

            首先我们先给出vector类:

            #include 
            #include 
            #include 
            using namespace std;
            template
            class vector
            {
            public:
            	// Vector的迭代器是一个原生指针
            	typedef T* iterator;
            	typedef T* const_iterator;
            	
            	//......
            	
            private:
            	iterator _start = nullptr;//指向开始位置的指针
            	iterator _finish = nullptr;//指向最后一个位置的下一个位置的指针
            	iterator _end_of_storage = nullptr;//指向存储容量的尾
            };
            

            一,构造函数

            在vector文档中,构造函数分为好几个类型,下面分别进行介绍:

            1 . 强制编译器生成默认构造

            无参构造

            vector() = default;
            

            2 . 拷贝构造

            //v2(v1);
            vector(const vector& v)
            {
            	//提前预开空间,避免边尾插边扩容
            	reserve(v.capacity());
            	for (auto e : v)
            	{
            		//this->push_back(e);
            		push_back(e);
            	}
            }
            

            3. 用迭代器区间初始化

            (1) 一个类模版的成员函数可以写成函数模版。

            (2) 若使用iterator做迭代器,会导致初始化的迭代器区间[first,last)只能是vector的迭代器,重新声明迭代器,迭代器区间[first,last)可以是任意容器的迭代器。

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

            4. 用n个val值构造

            vector(size_t n, const T& val = T())
            {
            	reserve(n);
            	for (size_t i = 0; i  
            

            具体使用:

            //初始化3个空
            vector v1(3);
            //初始化为3个xx
            vector v2(3, "xx");
            //初始化为3个1
            vector v3(3, 1);//err 非法的间接寻址.参数匹配问题
            vector v3(3u, 1);//ok
            //如果非要这样传参数,就需要再重载一个构造
            vector v3(3, 1);
            

            (1) 为什么要重载两个类似的构造?

            理论上将,提供了vector(size_t n, const T& value = T())之后vector(int n, const T& value = T())就不需要提供了,但是对于:vector v(10, 5);

            编译器在编译时,认为T已经被实例化为int,而10和5编译器会默认其为int类型, 就不会走vector(size_t n, const T& value = T())这个构造方法, 最终选择的是:vector(InputIterator first, InputIterator last)。因为编译器觉得区间构造两个参数类型一致,因此编译器就会InputIterator实例化为int,但是10和5根本不是一个区间,编译时就报错了,故需要增加该构造方法。

            (2) T()是什么?

            T()是缺省值,注意这里不能给0,因为T可能为自定义类型,当T为内置类型的时候,也ok。因为C++对内置类型进行了升级,也有构造,为了兼容模版。

            5. initializer_list 的构造

            (1) C++11新增的类模板initializer_list,方便用花括号初始化。

            (2) 这里不需要传引用&,因为不涉及拷贝问题,它的内部其实有两个指针,一个指向第一个值的位置,一个指向最后一个值的下一个位置,并且支持迭代器。

            vector(initializer_list il)
            {
            	reserve(il.size());
            	for (auto e:il)
            	{
            		push_back(e);
            	}
            }
            

            具体使用:

            支持被花括号括起的任意个数的值给 initializer_list

            auto il1 = { 1,3,4,5,6,7 };
            initializer_list il2 = { 1,2,3 };
            //这里的隐式类型转换不一样,参数个数不固定
            vector v4 = { 7,8,9,4,5 };//隐式类型转换
            vector v5({ 4,5,6 });//直接构造
            

            【C++】:vector容器的底层模拟实现&&迭代器失效&&隐藏的浅拷贝 第1张

            二,析构函数

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

            三,关于迭代器

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

            四,有关数据个数与容量

            //计算有效数据个数
            size_t size()const
            {
            	return _finish - _start;
            }
            //计算当前容量
            size_t capacity()const
            {
            	return _end_of_storage - _start;
            }
            

            五,交换函数swap

            与string类类似,依然调用库里的swap函数,进行指针的交换。

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

            六,赋值拷贝

            与string类的现代写法相同。

            //赋值拷贝
            //v3 = v1;
            vector& operator=(const vector v)
            {
            	swap(v);
            	return *this;
            }
            

            七,[ ]运算符

            //[]运算符
            T& operator[](size_t pos)
            {
            	//断言,避免下标越界
            	assert(pos  
            

            八,预留空间(扩容)

            void reserve(size_t n)
            {
            	//由于开空间后_start指向改变,所以要提前记录原来的有效数据个数
            	//以便在新空间中更新_finish的位置
            	size_t  old_size = size();
            	if (n > capacity())
            	{
            		T* tmp = new T[n];//开新空间
            		if (_start)
            		{
            			//memcpy(tmp, _start, sizeof(T) * old_size);
            			for (size_t i = 0; i  
            

            8.1 使用memcpy拷贝问题(重点)

            假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?

            int main()
            {
            	 bite::vector v;
            	 v.push_back("2222");
            	 v.push_back("2222");
            	 v.push_back("2222");
             
             	return 0;
            }
            

            问题分析:

            (1) memcpy是内存的二进制格式拷贝,按字节拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。

            (2) 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

            图解如下:

            当把原空间里的"2222"拷贝进新空间后,delete会先对原空间的每个对象调用析构函数,再把原空间销毁,但是此时tmp仍指向那块空间,变成野指针了!

            【C++】:vector容器的底层模拟实现&&迭代器失效&&隐藏的浅拷贝 第2张

            复用赋值进行修改后:

            此时这里的赋值调用的是string的赋值,肯定是深拷贝

            【C++】:vector容器的底层模拟实现&&迭代器失效&&隐藏的浅拷贝 第3张

            九,尾插和尾删数据

            //尾插数据
            void push_back(const T& x)
            {
            	if (_finish == _end_of_storage)
            	{
            		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
            		reserve(newcapacity);
            	}
            	*_finish = x;
            	++_finish;
            }
            //尾删
            void pop_back()
            {
            	//断言,确保有数据可删
            	assert(size() > 0);
            	--_finish;
            }
            

            十,插入数据 insert

            //void insert(iterator pos, const T& x)
            iterator insert(iterator pos, const T& x)
            {
            	//断言,判断下标的有效性
            	assert(pos >= _start && pos 
            		size_t len = pos - _start;
            		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
            		reserve(newcapacity);
            		//insert函数的内部迭代器失效问题:类似于野指针问题
            		//扩容后pos位置失效,需要重新计算pos
            		pos = _start + len;
            	}
            	//挪动数据再插入
            	iterator end = _finish - 1;
            	while (end = pos)
            	{
            		*(end + 1) = *end;
            		--end;
            	}
            	*(end + 1) = x;
            	++_finish;
            	//返回新插入那个位置的迭代器
            	return pos;
            }
            

            十一,删除数据 erase

            iterator erase(iterator pos)
            {
            	assert(pos >= _start && pos  
            

            十二,insert和erase的迭代器失效问题(重点)

            1. 什么是迭代器失效?

            迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

            2. insert 的迭代器失效

            2.1 insert 内部pos位置的失效

            当刚好执行了扩容操作这一步时,由于要开辟新空间,拷贝数据,释放空间,改变空间指向。但是此时pos位置还在原空间,这就使pos变成了野指针,如果对该位置进行访问,就会导致程序崩溃。

            所以在函数内部,扩容后我们要更新pos迭代器!

            【C++】:vector容器的底层模拟实现&&迭代器失效&&隐藏的浅拷贝 第4张

            2.2 insert 以后外部的实参失效

            演示代码如下:

            void vector_test2()
            {
            	vector v1;
            	v1.push_back(1);
            	v1.push_back(2);
            	v1.push_back(3);
            	v1.push_back(4);
            	for (size_t i = 0; i 

    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]