【C++】vector使用与模拟实现

目录

一、vector的介绍

二、vector的常用接口

1、vector的构造

2、vector的容量操作

2.1、reserve

2.2、resize

3、vector的增删查改

3.1、find

3.2、insert

三、vector模拟实现

1、 begin 与 end

2、capacity 与 size

3、operator[]

4、operator=

5、reserve

6、push_back

7、pop_back

8、resize​

9、迭代器失效

10、insert

11、erase

12、构造函数

1、构造函数 二

2、构造函数 三

13、拷贝构造函数


一、vector的介绍

 vector 是一个类模板,可以根据不同的模板参数实例化出存储不同数据的类。 vector 类可以用来管理数组,与 string 类不同的是,string只能管理 char 类型的数组,而vector可以管理任意类型的数组。

  1. vector是表示可变大小数组的序列容器。
  2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
  3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
  5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
  6. 与其它动态序列容器相比(deque, list and forward_list),vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。

二、vector的常用接口

1、vector的构造

(constructor)构造函数声明 接口说明
vector()(重点) 无参构造
vector(size_type n, const value_type& val = value_type()) 构造并初始化n个val
vector (const vector& x); (重点) 拷贝构造
vector (InputIterator first, InputIterator last); 使用迭代器进行初始化构造

 举例说明:

void test1()
{
	//无参构造
	vector<int> v1;

	//构造并初始化n个val
	vector<int> v2(10, 1); 
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	//拷贝构造
	vector<int> v3(v2);
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	//使用迭代器进行初始化构造
	vector<int> v4(v2.begin(), v2.end());
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
}

 结果如下:

2、vector的容量操作

容量空间 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize(重点) 改变vector的size
reserve (重点) 改变vector的capacity

2.1、reserve

我们可以通过以下代码来更加直观的体会到vector的扩容操作:

void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v 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';
		}
	}
}

 现象如下:

 可以看到在 vs 编译器下,vector的扩容是以每次 1.5 倍进行扩容的。如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够,就可以避免边插入边扩容导致效率低下的问题了:

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';
		}
	}
}

2.2、resize

resize可以改变 size 的大小。可以在开辟空间的同时进行初始化:

3、vector的增删查改

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

3.1、find

 因为算法里的 find 是通用的,可以给所有的容器使用,所以就没有在 vector 里单独提供一个 find 接口。

使用方法如下:

3.2、insert

 在指定下标位置插入数据:

三、vector模拟实现

我们先来搭建一个vector最基本的框架,其三个成员变量皆为指针:

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

		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{}

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

1、 begin 与 end

 const 非const 函数构成重载,以满足不同情况时的调用需求。

2、capacity 与 size

3、operator[]

  const 非const 函数构成重载,以满足不同情况时的调用需求。 

4、operator=

实现赋值运算符重载,需要借助拷贝构造,拷贝构造下面一点再讲,这里先拿来用。

具体实现如下:

 红框框起的部分使用的是传值传参,而不是传引用传参,这是因为我们本来就希望这里发生拷贝,拷贝出一个新的对象,并在函数体中完成交换。

5、reserve

 扩容函数 reserve 中有一个需要注意的地方:

 因为函数 size() 是通过返回 _finsih - _start 来实现的,如果直接写成 _finish = _start + size() ,会造成该行语句变为 _finish = _start + _finsih - _start ,最终 _finish 还是指向原来的位置,造成错误。因此,我们需要事先设置一个变量 sz 来保存 size() 的值,最后计算的时候加 sz 就可以了。

 注意:因为使用的函数是 memcpy ,以上写法会造成浅拷贝的问题,如果vector里存放的数据类型不是内置类型的话,编译器就会报错,为了解决这个问题,我们应该写成深拷贝的形式:

6、push_back

7、pop_back

8、resize

9、迭代器失效

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

 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。

 在某些情况下,迭代器会失效。其中最经典的迭代器失效就是扩容造成的野指针问题导致的。

具体原因如下:

 异地扩容之后, _start _finish  _end_of_storage 都指向了新的位置,而上一次获取的迭代器 pos 却不会更新指向的位置,由此导致 pos 变为野指针。

 除此之外,指定位置元素的删除操作也会导致迭代器失效,在下面会进行讲解。

10、insert

我们先来看一个错误的写法

 这种写法的错误之处在于,在进行 insert 时,也需要判断是否需要扩容,有了扩容,就可能存在迭代器失效的问题,所以需要更新一下 pos 的指向:


 补充知识 insert 函数的参数 pos 不能使用传引用传参,因为以下原因:

 begin() 使用的是传值传参,所有的传值传参都会借用临时变量来传递,而临时变量具有常性,不可被改变,因此如果在 insert 函数中的参数 pos 使用传引用传参,就会造成权限放大的问题,这是不被允许的。


 库中vector的 insert 函数是拥有返回值的,这样做是为了在调用完 insert 函数后,仍然可以找到 pos 的位置:

11、erase

 erase删除 pos 位置元素后, pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果 pos 刚好是最后一个元素,删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么 pos 就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了

 为了解决迭代器失效的问题,库中的erase函数是具有返回值的,返回删除数据的下一个位置:

12、构造函数

以下是三个构造函数:

vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{}

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);
	}
}

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

1、构造函数 二

对于第二个构造函数,这里有几点说明:

 该函数参数使用了一个匿名对象作为缺省参数。之前我们曾经讲过,匿名对象的生命周期仅在本行,这其实是不全面的,匿名对象的声明周期仅存在于本行,是因为在此之后没有人再使用它。而在以下场景下,在本行之后仍然有人使用该对象:

 对象 xx 是匿名对象的别名,因此,匿名对象的生命周期被延长为与 xx 相同,(因为匿名对象与临时变量一样,都具有常性,所以需要使用关键字 const 来修饰)。

这种构造函数可以实现直接初始化:

为了防止和下面的第三个构造函数的使用造成冲突,这里再重载一个 int 类型参数的函数:

2、构造函数 三

 第三个构造函数使用了函数模板,以便各种类型的迭代器都可以使用:

13、拷贝构造函数

我们现在看一种错误的写法:

观察现象: 

 

 这种拷贝构造的写法在如上场景下确实没什么问题,但是如果是以下场景的话,就会出现报错:

原因如下:

  1.  memcpy 是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是自定义类型的元素, memcpy 既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为 memcpy 的拷贝实际是浅拷贝。

 为了解决这个问题,我们需要实现深拷贝:

为了简便,我们也可以使用现代写法来实现:


关于vector的使用与底层实现的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_74078718/article/details/129896589