【C/C++】STL——深度剖析vector容器

news2024/10/2 22:17:11

在这里插入图片描述

​👻内容专栏: C/C++编程
🐨本文概括:vector的介绍与使用、深度剖析及模拟实现。
🐼本文作者: 阿四啊
🐸发布时间:2023.10.8

一、vector的介绍与使用

1. vector的介绍

像string的学习一样,我们依旧得学会在cplusplus网站中学会查看文档。

关于vector的文档介绍
在这里插入图片描述

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

2. vector的使用

vector在实际中非常的重要,在实际中我们熟悉常见的接口就可以。我们一一介绍学习常见的vector接口。

2.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)使用迭代器进行初始化构造
//vector的构造
void test_vector1()
{
	vector<int> v1; // 无参初始化
	vector<int> v2(10, 1); //带参初始化
	
	//利用迭代区间进行初始化
	vector<int> v3(v1.begin(), v1.end());
	vector<int> v4(v2); //拷贝构造

	//也利用string的迭代区间也可以进行初始化
	string s1("hello world");

	vector<int> v5(s1.begin(), s1.end());

	vector<int>::iterator it = v5.begin();

	while (it != v5.end())
	{
		//打印字符所对应的ascii码值
		cout << *it << " ";
		it++;
	}
}

2.2 vector 迭代器的使用

iterator的使用接口说明
begin/cbegin + end/cend获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator

在这里插入图片描述

void PrintVector(const vector<int>& v)
{
	// const对象使用const迭代器进行遍历打印
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

void test_vector2()
{
	vector<int> vec(10,0);
	vector<int>::iterator it = vec.begin();

	while (it != vec.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	//使用迭代器进行修改
	it = vec.begin();

	int i = 1;

	while (it != vec.end())
	{
		*it += i;

		it++;
		i++;
	}

	//反向迭代器
	auto rit = vec.rbegin();
	while (rit != vec.rend())
	{
		cout << *rit << " ";
		rit++;
	}
	cout << endl;

	//迭代器遍历打印vec
	PrintVector(vec);
}

2.3 空间增长问题

容量空间接口说明
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。
// 测试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:运行结果:vs下使用的STL基本是按照1.5倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141

g++运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128
// 如果已经确定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.4 vector的增删查改

vector的增删查改接口说明
push_back尾插
pop_back尾删
insert在position位置之前插入val值
erase删除position位置的元素
swap交换两个vector的数据空间
operator[ ]像数组一样访问
clear删除容器的所有元素,将size置为0,但并不改变capacity的大小
// 尾插和尾删:push_back/pop_back
void test_vector3()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	auto it = v.begin();
	while (it != v.end()) 
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	v.pop_back();
	v.pop_back();

	it = v.begin();
	while (it != v.end()) 
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

// 任意位置插入:insert和erase,以及查找find
// 注意find不是vector自身提供的方法,是STL提供的算法
void test_vector4()
{
	// 使用列表方式初始化,C++11新语法
	vector<int> v{ 1, 2, 3, 4 };

	// 在指定位置前插入值为val的元素,比如:3之前插入30,如果没有则不插入
	// 1. 先使用find查找3所在位置
	// 注意:vector没有提供find方法,如果要查找只能使用STL提供的全局find
	auto pos = find(v.begin(), v.end(), 3);
	if (pos != v.end())
	{
		// 2. 在pos位置之前插入30
		v.insert(pos, 30);
	}

	vector<int>::iterator it = v.begin();
	while (it != v.end()) 
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	pos = find(v.begin(), v.end(), 3);
	// 删除pos位置的数据
	v.erase(pos);

	it = v.begin();
	while (it != v.end()) {
		cout << *it << " ";
		++it;
	}
	cout << endl;
	v.clear();
	
	//调用clear后,vector的size将变成0,但是它的容量capacity并未发生改变
}

二、vector的底层实现

1.说明和准备工作

创建一个vector的源文件,写一个vector的类模板,放入一个自己的MyVector的命名空间里面,以免与库里面的vector发生冲突。
在模拟vector时,我们并没有和string一样使用动态分配的指针_Ptr、_size、_capacity,在类中我们使用了三个iterator,贴近stl库里面的实现方式,其实本质就是原生指针。

_start: _start指向动态数组或容器的第一个元素的位置。它用于表示容器的起始位置。

_finish: _finish 也是一个指针,指向容器中当前元素的下一个位置。它表示容器中元素的结束位置。通常,_finish 处于有效元素的末尾,但它之后的内存可能已经分配,但未被使用。

_end_of_storage: _end_of_storage 指向容器内存分配的末尾位置。这个位置之后的内存是容器为将来添加更多元素而预留的。当容器的大小接近容量时,它可能需要重新分配内存,并将新的_end_of_storage更新为新的内存末尾。

namespace MyVector
{
	template<class T>
	class vector
	{
	public:
	typedef T* iterator;
	 

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

2.push_back操作

首选我们提前需要写好构造函数与析构函数,构造函数在初始化列表将三个iterator置为nullptr即可,析构函数进行释放资源与指针置空操作。
size()接口函数:表示vector的有效数据个数,即:_finish - _start
capacity()接口函数:表示vector的容量大小,即:_end_of_storage - _start

在push_back尾插之前,我们还需要进行判断是否要进行扩容,扩容机制我们在数据结构学习了很多,不作细致讲解,下面直接放代码:

namespace MyVector
{
	template<class T>
	class vector
	{
	public:
	typedef T* iterator;

	vector()
		: _start(nullptr)
		, _finish(nullptr)
		, _end_of_storage(nullptr)
	{}
	
	size_t capacity()
	{
		return _end_of_storage - _start;
	}
	size_t size()
	{
		return _finish - _start;
	}

	void push_back(const T& val)
	{
		//扩容
		if (_finish == _end_of_storage)
		{
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			T* tmp = new T[newcapacity];
		
			if (_start)
			{
				memcpy(tmp, _start, sizeof(T)* size());
				delete[] _start;
			}
			
			_start = tmp;
			_finish = _start + size();
			_end_of_storage = _start + newcapacity;

		}
		*_finish = val;
		_finish++;
	}
	//通过[]进行访问vector
	T& operator[] (size_t n)
	{
		assert(n < size());
		return *(_start + n);
	}

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

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

	void test_vector1()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

		for (size_t i = 0; i < v1.size(); i++)
		{
			cout << v1[i] << " ";
		}
		cout << endl;
	}
};

在main函数中我们调用MyVector::test_vector1(),将程序运行起来之后程序就出问题了:
在这里插入图片描述

报错说_finish是nullptr,什么原因呢?

我们将程序调试起来,观察发现vector发生了扩容,其_start与_end_of_storage均发生了改变,我们讲他俩相减等于16字节,1个int占4个字节,说明确实刚开始扩容了4个元素,但是我们细心观察发现_finish的值还是为空指针,原因其实就在于_finish = _start + size(),这里更新了_start,而size()里面的_finish还是指向原来的空间,也就是0x00000000,属于迭代器失效问题,解决办法就是在扩容之前,提前用sz变量记录好偏移量
在这里插入图片描述
解决方案:

void push_back(const T& val)
{

	if (_finish == _end_of_storage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		size_t sz = size();
		T* tmp = new T[newcapacity];
	
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T)* sz);
			delete[] _start;
		}
		
		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + newcapacity;

	}
	*_finish = val;
	_finish++;
}

3.vector的访问与遍历

第一种:[]下标访问遍历
第二种:将迭代器_start与_finish用begin与end方法进行封装为成员函数,利用iterator进行遍历。
第三种:一旦有了迭代器,就可以支持范围for语句,因为其底层就是迭代器。

namespace MyVector
{
	template<class T>
	class vector
	{
	public:
	typedef T* iterator;

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

	iterator begin()
	{
		return _start;
	}

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

	void push_back(const T& val)
	{

		if (_finish == _end_of_storage)
		{
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			size_t sz = size();
			T* tmp = new T[newcapacity];
		
			if (_start)
			{
				memcpy(tmp, _start, sizeof(T)* sz);
				delete[] _start;
			}
			
			_start = tmp;
			_finish = _start + sz;
			_end_of_storage = _start + newcapacity;

		}
		*_finish = val;
		_finish++;
	}

	T& operator[] (size_t n)
	{
		assert(n < size());
		return *(_start + n);
	}

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

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

	void test_vector1()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);

		for (size_t i = 0; i < v1.size(); i++)
		{
			cout << v1[i] << " ";
		}
		cout << endl;

		vector<int> v2;
		v2.push_back(10);
		v2.push_back(20);
		v2.push_back(30);
		v2.push_back(40);
		v2.push_back(50);

		vector<int>::iterator it = v2.begin();

		while (it != v2.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;
		
		for(auto e:v2)
		{
			cout << e << " ";
		}
		cout << endl;
	}
};

4.resize与reserve

resize接口函数:改变vector的size,可以增加或减少容器中的元素数量。
当使用 resize 减少size大小时,多余的元素会被移除,当使用 resize 增加size大小时,会发生扩容。
reserve接口函数:改变vector的容量大小,它能够预留足够的空间,使用 reserve 可以减少因为频繁扩容而带来的性能开销。
ps:reserve本身有检查扩容机制的意思,我们直接使用push_back写的扩容机制代码,然后用push_back复用reserve.

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

		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * sz);
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

//为什么这里的val要给匿名对象初始化,而不是0,
//因为这里写的是vector类模板,
//传进来的参数类型可能是int,double,也可能是vector<string>,vector<int>……
//C++泛型编程对内置类型也支持构造函数(匿名对象),不然C++模板很难用
//添加const说明匿名对象具有常属性,添加&可以延长匿名对象的生命周期
void resize(size_t n, const T& val = T())
{
	//分为三种情况
	//小于等于size =>缩容(多余元素被移除)
	// 大于size 小于capacity
	//大小capacity =>扩容
	
	if (n <= size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);

		while (_finish < _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
}
void push_back(const T& val)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	*_finish = val;
	_finish++;
}

测试:

void test_vector2()
{
	vector<int*> v1;
	v1.resize(5);
	vector<string> v2;
	v2.resize(10,"xxx");

	for (auto e: v1)
	{
		cout << e << " ";
	}
	cout << endl;

	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
}

5.insert和erase

5.1 insert插入操作

void insert(iterator pos,const T& val)
{
	assert(pos >= _start);
	assert(pos <= _finish);

	//判断是否需要扩容
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

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

测试:

void test_vector3()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);


	for (size_t i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
	
	//第一次在下标为2的位置插入一个元素30
	v1.insert(v1.begin() + 2, 30);

	for (size_t i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;

	//第二次头插一个元素8,此时会发生扩容,导致pos失效
	v1.insert(v1.begin(), 8);

	for (size_t i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
}

第一次我们在下标为2的位置插入一个元素30,程序能够正确运行,结果也是对的。
但是我们再次利用insert头插一个元素,此时正好是添加新的元素,需要进行扩容,最后程序发生了崩溃。
为何呢?是因为扩容的原因导致的吗?接下来,我们探究一下
在扩容之前,我们调试观察看到pos接收的地址的确和_start的地址一模一样,都是0x0122da28

在这里插入图片描述
一旦经过reseve扩容,我们发生三个iterator地址都发生了变化,唯独pos却纹丝不动,还是原来的地址空间,此时的问题貌似很眼熟,没错,就是在前面部分我们提到的_finish如出一辙,也属于迭代器失效问题,本质就是因为pos使用的是释放之前的空间,空间发生了扩容,pos在对以前已经释放的空间进行操作时,就会引起代码运行崩溃。
在这里插入图片描述
解决方案:在扩容之前,保存pos位置的偏移量,在扩容后更新pos位置。

void insert(iterator pos,const T& val)
{
	assert(pos >= _start);
	assert(pos <= _finish);

	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
}

5.2 erase删除操作

void erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	iterator begin = pos + 1;

	while (begin < _finish)
	{
		*(begin - 1) = *begin;
		begin++;
	}
	_finish--;
}

测试:

void test_vector4()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	v1.push_back(7);

	for (size_t i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;

	auto it = v1.begin();
	v1.erase(it);
	
	for (auto e:v1)
	{
		cout << e << " ";
	}
	cout << endl;

	v1.erase(it + 2);

	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

}

以上我们对头部和下标为2的元素进行了删除操作,代码的结果也能顺畅地跑出来,结果也是正确的,但是这里的it迭代器不会失效吗?答案并非如此,我们来看下面的场景:

我们给出三组样例数据,分别计算给出的样例中用erase删除偶数元素

第一组测试数据:1 2 3 4 5 6 7

//第一种情况
void test_vector5()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	v1.push_back(7);

	cout << "begin:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
 
	auto it = v1.begin();
	
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	cout << "after:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;	 
}

运行结果:结果正确

begin:1 2 3 4 5 6 7
after:1 3 5 7

第二种测试数据:1 2 3 4 5 6 7 8

//第二种情况
void test_vector6()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	v1.push_back(7);
	v1.push_back(8);


	cout << "begin:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v1.begin();

	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	cout << "after:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

运行结果:程序崩溃

begin:1 2 3 4 5 6 7 8
error运行崩溃(触发断言)

第三种测试数据:2 2 3 4 5 6 7

//第三种情况
void test_vector7()
{
	vector<int> v1;
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	v1.push_back(7);


	cout << "begin:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v1.begin();

	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	cout << "after:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

打印结果:结果错误

begin:2 2 3 4 5 6 7
after:2 3 5 7

分析:
注⚠️:
以上也是Linux下g++的编译器对迭代器的检测并不严格,处理没有vs编译器果断极端。
以上代码在vs下程序会出现崩溃,vs一些编译器会进行强制检查,认为erase之后it就失效了,访问就会报错。但是在Linux下,虽然可能可以运行,但是输出的结果是不对的。

在这里插入图片描述

那么对于以上it等迭代器失效问题,该如何解决呢?
其实erase有具体的返回值,返回的是一个iterator,指向被删除元素的下一个元素的位置。
在这里插入图片描述
👇修正erase的代码:

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	iterator begin = pos + 1;

	while (begin < _finish)
	{
		*(begin - 1) = *begin;
		begin++;
	}
	_finish--;

	return pos;
}	

//迭代器失效的解决方案
//在使用前,对迭代器进行重新赋值
void test_vector8()
{
	vector<int> v1;
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	v1.push_back(7);


	cout << "begin:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v1.begin();

	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			//在下次操作it之前,对迭代器进行重新赋值
			it = v1.erase(it);		
		}
		else
		{
			//不删除++即可
			it++;
		}
	}
	cout << "after:";
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}
	

结论:在使用了 inserterase 之后,迭代器失效了,不能再访问,需要谨慎处理。

6.关于具体迭代器失效问题的分析

关于剖析迭代器失效问题 博客==>关于迭代器失效问题

7.memcpy浅拷贝问题

使用memcpy拷贝的是内置类型,那么通常是高效又安全的,但是如果对于自定义类型,涉及动态资源管理,会导致浅拷贝问题,造成内存泄露等不可预知的结果!

//测试对于自定义类型,扩容时使用memcpy会导致浅拷贝问题
void test_vector9()
{
	vector<string> v1;
	v1.push_back("11111111111");
	v1.push_back("11111111111");
	v1.push_back("11111111111");
	v1.push_back("11111111111");
	v1.push_back("11111111111");
	
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

}

在这里插入图片描述
解决方案:
很简单,使用一个for循环,对每个字节进行赋值操作,对于内置类型是赋值,对于自定义类型就会调用自身的赋值重载函数!

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

		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * sz);
			for (size_t i = 0; i < sz; i++)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

8.vector的拷贝构造与赋值重载

//拷贝构造
//v2(v1)
vector(const vector<T>& x)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	//开辟和x一样大的空间
	reserve(x.capacity());

	for (size_t i = 0; i < x.size(); i++)
	{
		push_back(x[i]);
	}
}

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

//赋值重载
//v2 = v1
vector<T>& operator=(vector<T> tmp)
{
	swap(tmp);

	return *this;
}

测试赋值重载:

//测试赋值重载
	void test_vector10()
	{
		vector<int> v1;
		v1.push_back(10);
		v1.push_back(20);
		v1.push_back(30);
		v1.push_back(40);

		vector<int> v2;
		v2 = v1;

		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;
	}

9.迭代区间初始化与n个val初始化

类模板里面可以嵌套函数模板,可以传入任意类型的迭代区间初始化,在形参部分用InputIterator进行接收,具体细节可下面的测试用例。
对于n个val初始化,不能直接写成vector(size_t n, const T& val = T()),在编译器认为会优先去调用最匹配的,就会调用迭代区间的初始化,此时编译就会出错,那么我们就需要写一个更匹配的vector(int n, const T& val = T()),此时就能正确编译并执行了。

//利用迭代器区间进行初始化
//函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

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

	for (size_t i = 0; i < n; i++)
	{
		push_back(val); 
	}
}

vector(int n, const T& val = T())
{
	reserve(n);

	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

测试迭代器区间初始化与n个val初始化:

//测试迭代器区间初始化与n个val初始化
void test_vector11()
{
	//n个val初始化
	vector<int> v1(10, 0);


	//利用迭代器区间初始化
	string str("hello world");

	vector<int> v2(str.begin(), str.end());

	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
}

10.vector模拟实现源代码

vector的深度剖析及模拟实现

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

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

相关文章

华为数通方向HCIP-DataCom H12-831题库(单选题:241-259)

第241题 设备产生的信息可以向多个方向输出信息,为了便于各个方向信息的输出控制,信息中心定义了10条信息通道,使通道之间独立输出,缺省情况下,以下哪些通道对应的输出方向可以接收Trap信息? A、console通道 B、logbuffer通道 C、snmpagent通道 D、trapbuffer通道 答案:…

学习记忆——数学篇——案例——代数——均值不等式

文章目录 理解记忆法定义定义推导 重点记忆法用途记忆法使用前提做题应用及易错点两种用法 出题模式法模型识别 谐音记忆法一正二定三相等 秒杀方法 理解记忆法 定义 1.算术平均值&#xff1a;设有n个数 x 1 , x 2 , . . . , x n x_1,x_2,...,x_n x1​,x2​,...,xn​&#xf…

nodejs+vue 高校超市外卖系统elementui

根据现实需要&#xff0c;此系统我们设计出一下功能&#xff0c;主要有以下功能模板。 &#xff08;1&#xff09;高校超市外卖系统前台&#xff1a;首页、商品、公告、后台管理、购物车。 &#xff08;2&#xff09; &#xff08;2&#xff09;管理员功能&#xff1a;首页、个…

jvm--对象实例化及直接内存

文章目录 1. 创建对象2. 对象内存布局3. 对象的访问定位4. 直接内存&#xff08;Direct Memory&#xff09; 1. 创建对象 创建对象的方式&#xff1a; new最常见的方式、Xxx 的静态方法&#xff08;单例模式&#xff09;&#xff0c;XxxBuilder/XxxFactory 的静态方法Class 的…

记录一次springboot使用定时任务中@Async没有生效的场景

环境说明 jdk21springboot 3.0.11 springcloud 2022.0.0 spring-cloud-alibaba 2022.0.0.0 在开发一个定时触发的任务的时候&#xff0c;由于开发执行任务的函数比较耗费时间&#xff0c;所以采用异步解决问题。 发现并没有按照预期的触发 经询问后&#xff0c;发现当前类的…

allegro提示错误(SPMHDB-225) The maximum number of text sizes has been reached.的解决

被这个问题困扰了好几天&#xff01;网上根本找不到解决办法&#xff0c;无论是百度还是谷歌&#xff0c;bing都没有。 解决方法&#xff1a;菜单tools---->database check 全选上&#xff0c;点Check。 然后问题就解决了。

Docker 入门教程(简明易懂、零基础篇)

11.1 Docker 是什么&#xff1f; docker是一个开源的应用容器引擎。 21.2 容器是什么&#xff1f; 容器是一种轻量级的虚拟化技术 &#xff0c;它是一个由应用运行环境、容器基础镜像组成的集合。 以 Web 服务 Nginx 为例&#xff0c;如下图所示&#xff1a;Nginx 容器是由 …

代码随想录Day14 LeetCodeT110平衡二叉树 T257二叉树的所有路径 T404 左叶子之和

以下思路来自于: 代码随想录 (programmercarl.com) LeetCode T110 平衡二叉树 题目链接:110. 平衡二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目思路 前面我们说过了,求二叉树的深度我们应该使用前序遍历,求二叉树的高度我们应该使用后序遍历,因为后序遍历可以将子树的…

pdf文档内容提取pdfplumber、PyPDF2

测试pdfplumber识别效果好些&#xff1b;另外pdf这两个如果超过20多页就没法识别了&#xff0c;结果为空 1、pdfplumber 安装&#xff1a;pip install pdfplumber -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com代码&#xff1a; import pdfpl…

分布式锁如何实现

分布式是现在的比较主流的技术&#xff0c;常常和微服务一起出现。那么对于多个实例之间&#xff0c;如何证分布式系统中多个进程或线程同步访问共享资源呢&#xff1f;我们其实一想到的就是锁&#xff0c;我们在java里边有 synchronized, 在python里有lock&#xff0c;但是这个…

8路高速光栅尺磁栅尺编码器4倍频计数转Modbus TCP网络模块 YL99-RJ45

特点&#xff1a; ● 光栅尺磁栅尺解码转换成标准Modbus TCP协议 ● 高速光栅尺磁栅尺4倍频计数&#xff0c;频率可达5MHz ● 模块可以输出5V的电源给光栅尺或传感器供电 ● 支持8个光栅尺同时计数&#xff0c;可识别正反转 ● 可以设置作为16路独立DI高速计数器 ● 可网…

公园视频监控系统如何改造?人工智能又能提供哪些帮助?

近日合肥市骆岗公园宣布正式开园&#xff0c;作为目前世界最大的城市公园&#xff0c;占地12.7万平方公里&#xff0c;如此壮观宏伟的建设&#xff0c;也吸引到了不少市民进行参观打卡。不管大型小型&#xff0c;城市里的公园都是随处可见的&#xff0c;那么&#xff0c;公园安…

Android:自定义原生TimePickerDialog样式

效果图&#xff1a; 目标效果图&#xff1a; 原生效果&#xff1a; 实现&#xff1a; 首先是Dialog样式&#xff1a; <style name"TimePickerDialogStyle" parent"style/Theme.AppCompat.DayNight.Dialog.Alert"><item name"android:time…

数据结构 | (四) Queue

队列 &#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出 FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为 队尾&#xff08; Tail/Rear &#xff09; 出队列&#xff1a;进行删除操…

抖店商家体验分怎么提高|成都瀚网科技

在竞争激烈的电商行业&#xff0c;提供优质的商户体验对于吸引买家、增加销量至关重要。在抖店平台&#xff0c;商户体验评分是衡量商户服务质量的重要指标之一。本文将为您介绍提高抖店商家体验分的方法和技巧&#xff0c;帮助您提高服务质量&#xff0c;赢得更多买家的青睐。…

百度小程序制作源码 百度引流做关键词排名之技巧

百度作为国内最大的搜索引擎&#xff0c;对于关键词排名和流量获取的策略格外重要&#xff0c;下面给大家分享一个百度小程序制作源码和做百度引流、关键词排名的一些技巧。 移动设备的普及和微信小程序的火热&#xff0c;百度也推出了自己的小程序。百度小程序与微信小程序类…

云安全之下一代防火墙介绍

防火墙的概念 下一代防火墙&#xff08;Next Generation Firewall&#xff0c;NGFW&#xff09;是一种可以全面应对应用层威胁的高性能防火墙。通过深入洞察网络流量中的用户、应用和内容&#xff0c;并借助全新的高性能单路径异构并行处理引擎&#xff0c;NGFW能够为用户提供…

redis实战-实现用户签到UV统计

BitMap功能演示 我们针对签到功能完全可以通过mysql来完成&#xff0c;比如说以下这张表 用户一次签到&#xff0c;就是一条记录&#xff0c;假如有1000万用户&#xff0c;平均每人每年签到次数为10次&#xff0c;则这张表一年的数据量为 1亿条 每签到一次需要使用&#xff08…

遥感云大数据在灾害、水体与湿地领域典型案 例实践及 GPT 模型应用

近年来遥感技术得到了突飞猛进的发展&#xff0c;航天、航空、临近空间等多遥感平台不断增加&#xff0c;数据的空间、时间、光谱分辨率不断提高&#xff0c;数据量猛增&#xff0c;遥感数据已经越来越具有大数据特征。遥感大数据的出现为相关研究提供了前所未有的机遇&#xf…

el-tree中插入图标并且带提示信息

<template><div class"left"><!-- default-expanded-keys 默认展开 --><!-- expand-on-click-node 只有点击箭头才会展开树 --><el-tree :data"list" :props"defaultProps" node-click"handleNodeClick" :…