vector底层实现详讲

目录

1. vector的介绍

1.1 vector构造函数的定义

1.2 vector iterator的使用

1.3 vector的空间增长问题

1.4 vector的增删查改

2. vector代码的实现

2.1 vector扩容

2.2 插入元素

2.3 删除元素

2.4 成员函数初始化

2.4.1 拷贝构造

2.4.2 赋值运算符重载

2.4.3 构造函数(迭代器区间初始化)

 2.4.4 构造函数(n个val初始化)

2.4.5 构造函数(常量数组初始化)

2.5 自定义类型使用memcpy扩容的问题

2.6 vector迭代器失效的问题

3. vector总代码

4. 杨辉三角


1. vector的介绍

  1. vector是表示可变大小数组的序列容器

  2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问(支持随机访问),和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。

  3. vector的文档介绍

1.1 vector构造函数的定义

这里先介绍5个重要的构造函数给大家看看,现在大家了解一下就可以了,后续会一一实现的。

构造函数声明 接口声明
vector()(重点) 无参构造
vector(size_type n, const value_type& val = value_type()) 构造并初始化n个val
vector (const vector& x); (重点) 拷贝构造

template <class InputIterator>

vector (InputIterator first, InputIterator last)

使用迭代器进行初始化构造
vector(initializer_list<T> il) 使用花括号进行初始化构造

1.2 vector iterator的使用

因为vector是类模板,所以iterator就是模板参数类型的指针

然后vector有三个成员变量

这三个成员变量的作用如下:

1.3 vector的空间增长问题

容量问题(这里介绍的是成员函数) 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize(重点) 改变vector的size
reserve 改变vector的capacity
  • capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。

  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以提前将空间设置足够用于缓解vector增容的代价缺陷问题。(在插入的数据的时候避免了边插入边扩容导致效率低下的问题了)

  • resize在开空间的同时还会进行初始化,影响size。

void TestVectorExpandOP()
{
	vector<int> v;
	size_t sz = v.capacity();
	v.reserve(100);   // 提前将容量设置好,可以避免一遍插入一遍扩容

	cout << "making bar grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

1.4 vector的增删查改

vector的增删查改 接口说明
push_back(重点) 尾插
pop_back(重点) 尾删
find 查找(注意这个是算法模块实现,不是vector的成员接口)
insert 在position之前插入val
erase 删除position位置的数据
swap 交换两个vector的数据空间
operator[] 想数组一样访问

2. vector代码的实现

2.1 vector扩容

在实现扩容前,我们首先要把vector最基本的成员函数搭建好,以便于后面我们的使用

成员函数包括:begin(),end(),size(),capacity()

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

		iterator begin()
		{
			return _start;
		}

		const_iterator begin() const
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator end() const
		{
			return _finish;
		}
        
        //这里获取的是容量大小
		size_t capacity()
		{
			return _end_of_storage - _start;
		}
        
        //指针-指针得到的是指针之间的元素个数
        //这里获取的是数据个数
		size_t size()
		{
			return _finish - _start;
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

实现完最基础的成员函数后,就要实现扩容了

#pragma once

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

		iterator begin()
		{
			return _start;
		}

		const_iterator begin() const
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator end() const
		{
			return _finish;
		}

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

		size_t size()
		{
			return _finish - _start;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				iterator tmp = new T[n];
				size_t oldsize = size();

				//不为空才需要拷贝一份代码,并且释放原本的旧空间
				if (_start)
				{
					//这种是一个字节一个字节进行拷贝
					//memcpy(tmp, _start, sizeof(T) * size());

					//下面这个方法也是以一个字节一个字节进行进行拷贝,但这种写法巨坑
					//一共要拷贝 sizeof(T) * size()个字符个数
					strncpy((char*)tmp, (char*)_start, sizeof(T) * size());
					delete[] _start;
				}
				_start = tmp;
				//此时size()里面的_start已经不是指向原先数组的起始地址
				//而是指向tmp所指向的数组的起始地址,所以
				//_finish = _start + _finish - _start;
				//求得:_finish = _finish 因此_finish是不变的
				//因此finish迭代器失效,因为指向的旧空间,并且旧空间等等会被释放
				//_finish = _start + size();

				//解决办法1:
				//把_finish放到_start = tmp前面
				//_finish = tmp + size();

				//方法1不推荐哈,因为有点依赖顺序,最好方法是提前记录好size()
				// 方法2
				_finish = tmp + oldsize;
				_end_of_storage = _start + n;
			}
		}

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

上面代码有非常多的细节需要说明

首先类里面定义的函数是没有顺序可言的,就算我们把reserve扩容函数写在size()函数上面,我们这里依旧是可以调用的!!!


这里我们需要注意的一个点就是如果_start原本指向就是为空,那就没有必要走拷贝步骤,更没必要对_start实施delete[]

重点:

我这边写了两个拷贝空间数据的函数,memcpy(tmp, _start, sizeof(T) * size())和strncpy(tmp, _start, sizeof(T) * size())。大家觉得哪个比较好,因该留哪个。

结论:

strncpy(tmp, _start, sizeof(T) * size()) 这个写法用在这里巨坑

这里我就先补充说明,就是一开始如果数组是没有空间的,那扩容一开始就会分配4个大小数组空间,所以这里一开始我们插入4个元素的时候是正常的,那我们在插入一个元素后就会产生扩容,那扩容我们本意是想把旧空间的元素拷贝到新空间中,在尾插一个5,可最终我们输出的时候不是1,2,3,4,5而是1,0,0,0,5

很明显,这是扩容出现的问题,问题就出在我们拷贝旧空间时使用错了方法

如果我们不知道strncpy的使用规则,我们可以看下面这张图

这里我们假设T是int类型,那我们就要从旧空间拷贝16个字节数据到新空间去

而strncpy有个问题就是如果我们提前遇到了‘\0’,但是我们还没有拷贝完,那就要在目的空间继续添加'\0',直到拷贝完(num等于0)

因为一开始拷贝元素是1,1在小端存储的时候是01 00 00 00,所以一开始先把01拷贝到新空间去,而接着遇到了00,00就是0,'\0'的ASCII也是0,由于提前遇到了'\0'了,而我们此时还没有拷贝完,所以就要继续追加num个'\0'

所以我说用strncpy这种拷贝方法巨坑,那为什么我一开始会用这种拷贝方法的原因是string自定义实现的时候就是用strncpy的拷贝方式,所以我把在string拷贝旧空间的方法移接到vector的时候就有问题(这个问题真的困扰我有一段时间)


解决了上面的问题,接着面临的问题就是扩容后迭代器失效

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值