c++-vector

news2025/1/11 14:03:45

文章目录

  • 前言
  • 一、vector介绍
  • 二、vector使用
    • 1、构造函数
    • 2、vector 元素访问
    • 3、vector iterator 的使用
    • 4、vector 空间增长问题
    • 5、vector 增删查改
    • 6、理解vector<vector< int >>
    • 7、电话号码的字母组合练习题
  • 三、模拟实现vector
    • 1、查看STL库源码中怎样实现的vector
    • 2、实现vector
    • 3、vector深浅拷贝问题


前言


一、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有下面的几种构造函数。并且还有一个构造函数模板。
在这里插入图片描述

void test01()
{

	//创建一个存储int类型的vector容器
	//调用的是explicit vector(const allocator_type& alloc = allocator_type())函数
	//此时v1里面没有内容
	vector<int> v1;
	cout << v1.capacity() << endl;
	
	//调用的是explicit vector(size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type())函数
	//将vector中初始化为10个1。
	vector<int> v2(10, 1);
	cout << v2.size() << endl;
	cout << v2.capacity() << endl;

	//还可以使用迭代器来进行构造,此时就会使用
	//template<class InputIterator>
	//vector(InputIterator first,InputIterator last, const allocator_type& alloc = allocator_type())模板生成对应的迭代器构造函数。
	//此时v3的内容就是v2里面的内容
	vector<int> v3(v2.begin(), v2.end());
	cout << v3.size() << endl;
	cout << v3.capacity() << endl;
	for (auto n : v3)
	{
		cout << n << " ";
	}
	cout << endl;


	//还可以使用其它类类型对象的迭代器来进行构造。
	string s("hello world");
	//此时v4中都为int元素,所以保存的是每个字符对应的ascii码值。
	vector<int> v4(s.begin(), s.end());
	cout << v4.size() << endl;
	cout << v4.capacity() << endl;
	for (auto n : v4)
	{
		cout << n << " ";
	}
	cout << endl;

	//使用拷贝构造函数来进行初始化
	vector<int> v5(v4);
	cout << v5.size() << endl;
	cout << v5.capacity() << endl;
	for (auto n : v5)
	{
		cout << n << " ";
	}
	cout << endl;

}

2、vector 元素访问

在这里插入图片描述

void test06()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	//使用[]访问v1的元素
	cout << v1[0] << endl;

	//使用at访问v1的元素
	cout << v1.at(0) << endl;

	//front返回v1的头元素
	cout << v1.front() << endl;

	//back返回v1的尾元素
	cout << v1.back() << endl;

	//返回存储v1的数据的地址
	cout << v1.data()[0] << endl;

}

3、vector iterator 的使用

在这里插入图片描述
在这里插入图片描述

void test02()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	//想要变量vector容器内的元素,可以使用下标来遍历
	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
	cout << endl;

	//也可以使用范围for来遍历
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//当然也可以使用迭代器来进行遍历
	//下面为使用正向迭代器正序遍历v1里面的元素,
	vector<int>::iterator it = v1.begin();
	while (it != v1.end())
	{
		//因为该迭代器没有被const修饰,所以也可以更改v1的内容
		*it = 2 * (*it);
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//下面为使用反向迭代器倒序遍历v1里面的元素
	vector<int>::reverse_iterator rit = v1.rbegin();
	while (rit != v1.rend())
	{
		//因为该迭代器没有被const修饰,所以也可以更改v1的内容
		*rit = 2 * (*rit);
		cout << *rit << " ";
		++rit;
	}
	cout << endl;

	//下面为使用const修饰的正向迭代器正序遍历v1里面的元素
	vector<int>::const_iterator cit = v1.cbegin();
	while (cit != v1.cend())
	{
		//因为该迭代器被const修饰了,所以只能读取v1数据,不能修改v1的数据
		//*cit = 2 * (*cit);   //错误,不能修改v1的元素的值
		cout << *cit << " ";
		++cit;
	}
	cout << endl;

	//下面为使用const修饰的反向迭代器倒序遍历v1里面的元素
	vector<int>::const_reverse_iterator crit = v1.crbegin();
	while (crit != v1.crend())
	{
		//因为该迭代器被const修饰了,所以只能读取v1数据,不能修改v1的数据
		//*crit = 2 * (*crit);   //错误,不能修改v1的元素的值
		cout << *crit << " ";
		++crit;
	}
	cout << endl;
}

4、vector 空间增长问题

在这里插入图片描述

void test03()
{
	vector<int> v1(20, 1);
	vector<int> v2;
	//size()获取v1的数据个数
	cout << v1.size() << endl;

	//max_size()获取vector存储的最大元素个数
	//因为int型元素占4个字节,所以只能存10亿多个元素,而char类型元素占4个字节,所以存40亿多个元素
	cout << v1.max_size() << endl;

	//capacity()获取v1的容量大小
	cout << v1.capacity() << endl;

	//empty()判断v1、v2是否为空
	cout << v1.empty() << endl;
	cout << v2.empty() << endl;

	//shrink_to_fit()缩容函数,将v3的capacity缩容。
	vector<int> v3(100);
	cout << v3.capacity() << endl;
	v3.resize(10);
	cout << v3.capacity() << endl;
	//会将v3的capacity缩容到合size一样的大小
	v3.shrink_to_fit();
	cout << v3.capacity() << endl;
}

vector容器中的resize()函数和reserve()函数的区别和string类中的两个函数的区别类似。
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector频繁增容的代价缺陷问题。
resize在开空间的同时还会进行初始化,影响size。
在这里插入图片描述
在这里插入图片描述

void test04()
{
	//使用reserve开辟空间,只会改变capacity的值,不会进行初始化,所以不会改变size的值。
	vector<int> v1;
	v1.reserve(10);
	cout << v1.size() << endl;
	cout << v1.capacity() << endl;

	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	//当使用reserve开辟的空间没有原来的容量大时,就不会做任何处理
	v1.reserve(2);
	cout << v1.size() << endl;
	cout << v1.capacity() << endl;
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	
	//使用resize开辟空间,第二个参数为新空间要初始化的值
	vector<int> v2;
	v2.resize(10,1);
	cout << v2.size() << endl;
	cout << v2.capacity() << endl;
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	

	//使用resize开辟空间,如果不传第二个参数,默认将新空间的值初始化为0。
	vector<int> v3;
	v3.resize(10);
	cout << v3.size() << endl;
	cout << v3.capacity() << endl;
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	//使用resize减少数据
	//当使用resize(n)开辟的空间没有当前的容量大时,resize会将容器内的元素删除到为n,此时size也会被改为n
	vector<int> v4(10, 1);
	v4.resize(5);
	cout << v4.size() << endl;
	cout << v4.capacity() << endl;
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;
}

5、vector 增删查改

在这里插入图片描述

void test05()
{
	//assign为重新向容器中分配值
	vector<int> v1(5, 1);
	cout << v1.size() << endl;
	cout << v1.capacity() << endl;
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	//使用assign(5,2)后会将v1原来的数据给清除,然后重新分配新的数据进去。
	v1.assign(5, 2);
	cout << v1.size() << endl;
	cout << v1.capacity() << endl;
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	//只有当assign传入的大小大于原来的容量时,才会进行扩容。
	v1.assign(10, 3);
	cout << v1.size() << endl;
	cout << v1.capacity() << endl;
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//push_back为尾插一个元素
	v1.push_back(4);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//pop_back为尾删一个元素
	v1.pop_back();
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//insert为在任意位置插入一个元素,使用迭代器确定位置
	//在v1的第三个位置之后插入5。
	v1.insert(v1.begin()+3, 5);
	//在v1的第五个位置之后插入3个6
	v1.insert(v1.begin() + 5, 3, 6);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//erase为删除任意位置的数据,使用迭代器确定位置
	//如果不传入结束的位置,则就会将v1的全部数据删除。
	//v1.erase(v1.begin());
	//将v1的第3个位置之后,包括第5个位置的数据删除。即(3,5]的数据删除。
	v1.erase(v1.begin() + 3, v1.begin() + 5);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;


	//swap为交换两个vector容器的数据
	vector<int> v2(10, 1);
	vector<int> v3(10, 2);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
	v2.swap(v3);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;


	//clear为将vector容器中的数据都清除
	vector<int> v4(10, 1);
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;
	v4.clear();
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;


	//
}

vector中没有find函数,但是可以通过算法模块实现。
在这里插入图片描述

//find函数为查找,这个是算法模块实现,不是vector的成员接口
	vector<int> v5;
	v5.push_back(1);
	v5.push_back(2);
	v5.push_back(3);
	v5.push_back(4);
	//使用这个find,当找到元素时会返回指向该元素的迭代器
	vector<int>::iterator ret01 = find(v5.begin(),v5.end(),3);
	cout << *ret01 << endl;

6、理解vector<vector< int >>

当我们写下面的题时,发现使用c++写时,题目中给了我们一个vector<vector< int >>的返回值。
在这里插入图片描述
vector<vector< int >> vv 表示的就是vv中的每个元素都是vector< int >类型的。
在这里插入图片描述

7、电话号码的字母组合练习题

题目链接
在这里插入图片描述

三、模拟实现vector

1、查看STL库源码中怎样实现的vector

我们可以在DevC++的文件目录下找到stl库的源码文件。具体路径如下:

D:\Dev-Cpp\MinGW64\lib\gcc\x86_64-w64-mingw32\4.9.2\include\c++\bits

在bits这个文件夹下面我们看到stl库的源码。
在这里插入图片描述
在linux系统下,我们可以在/usr/include/c++/4.8.2/bits目录下看到stl库的源码。gcc使用的是SGI版本的STL库。

cd /usr/include/c++/4.8.2/bits

在这里插入图片描述
我们看到stl的源码中有三个成员变量分别为:
在这里插入图片描述
stl的源码中就是使用这三个成员变量来求size和capacity等一些值。

2、实现vector

因为查看源码时看到源码的vector使用了start和finish和end _ of _ storage这三个指针。所以我们模拟实现vector也靠这些指针。
template< class T >为一个模板,因为vector容器里面可以存任意类型的数据,可以是内置类型,也可以是自定义类型,所以我们使用模板来实现。T就相当以后vector里面的数据类型,可能是int、char、string类、Date类或者vector< int >等类型。
下面就是我们模拟实现的vector的刚开始的模板。

在这里插入图片描述
我们先实现push_back尾插函数。

在这里插入图片描述
但是因为push_back插入元素要考虑扩容的问题,所以我们要先实现reserve函数。又因为reserve函数需要用到size和capacity,所以我们要先实现size和capacity函数。下面为size和capacity函数的实现。
在这里插入图片描述
在这里插入图片描述
然后我们再来实现reserve函数。
在这里插入图片描述
接下来我们实现vector的[]操作符的重载函数。

在这里插入图片描述
然后我们进行测试时会发现出现了异常,
在这里插入图片描述
我们调试后发现在reserve函数中,当申请了一片新空间后,_start和_end_of_storage的值都改变了,而_finish的值还是nullptr,所以在push_back函数中解引用_finish时才出现了空指针解引用的异常。这个异常是因为我们在reserve中求_finish时调用了size函数,而此时_start已经变为了tmp,此时_finish = tmp + _finish - tmp,所以_finish还是为nullptr,这才出现了异常。想要解决这个异常有两种方法。

在这里插入图片描述
在这里插入图片描述
(1). 交换语句顺序。(不推荐,以后不好维护代码,顺序反了程序就崩溃)

在这里插入图片描述
(2). 提前将size的值算出来。(推荐)
在这里插入图片描述

接下来我们就实现vector的迭代器中的begin和end。
在这里插入图片描述
当实现了begin和end后,就可以使用迭代器和范围for来遍历vector的元素了。
在这里插入图片描述
但是此时我们发现如果是const修饰的vector的对象,此时没有办法调用[]操作符重载函数,也没有办法调用迭代器的begin和end函数等。
在这里插入图片描述
所以我们还需要写一个const修饰的[]操作符重载函数和迭代器。
在这里插入图片描述
在这里插入图片描述

然后我们再来实现pop_back方法了。在实现pop_back函数时,因为可能会遇到vector为空的情况,所以我们需要写一个empty函数来判断vector是否为空。并且在pop_back函数中使用assert断言vector是否为空。

在这里插入图片描述
如果不判断vector是否为空,就会出现如下的错误。使用迭代器遍历vector的元素时会一直循环下去,这时因为此时_start在_finish的后面了,所以会一直向后访问下去。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

下面再来实现resize函数。我们看到官方库中的resize函数的第二个参数为缺省参数value_type val = value_type()。因为vector中可以存放任意类型的数据,例如内置类型中的int、char、double等,自定义类型的Date、string类等。所以就不能给缺省参数赋值时给一个定值,而value_type val = value_type()为创建一个匿名对象,在创建匿名对象时会调用他的默认构造函数,然后给val的缺省值就为这个匿名对象的值,这样来实现给val初始化。
在这里插入图片描述
那么我们的内置类型int、double、char有构造函数吗?
因为有了模板的存在,所以这些内置类型也有构造函数。但是指针类型不支持这样显示的调用构造函数。
在这里插入图片描述
在这里插入图片描述
但是指针类型还是支持使用模板调用它的构造函数。例如下面T为int * 时,int * = int * (),使用模板就可以进行int类型的构造函数。
在这里插入图片描述
所以我们自己实现resize函数时,将resize的第二个参数也写为T val = T()的形式。然后我们进行测试也没有出现问题。
在这里插入图片描述
在这里插入图片描述
接下来我们实现insert函数。
在这里插入图片描述
当我们测试时我们发现当vector中有4个元素时,此时再向vector中插入元素会出现错误,而当vector中有5个元素时,此时再向vector中插入元素不会出错。这其实是因为发生了迭代器失效问题。
在这里插入图片描述
我们可以看到当vector中有4个元素后,此时再向vector中插入元素会调用reserve函数来进行扩容。而当扩容后我们看到_start、_finish、_end_of_storage的值都发生了变化,即指向了扩容后的空间的地址,而pos的值没有变化,即pos还指向了原来的空间的地址。此时pos指向的空间已经被释放,所以pos此时就相当于一个野指针,指向已经被释放的空间。而我们使用
pos=val修改的是原来空间的值,而新的空间并没有插入val,但是_finish已经+1向后移动一位了,所以vector的最后一个元素为随机值。上述的这种情况为最常见的迭代器失效问题,类似于野指针问题。
在这里插入图片描述

在这里插入图片描述
没扩容之前_start、_finish、_end_of_storage、pos都指向同一片空间。
在这里插入图片描述
扩容之后_start、_finish、_end_of_storage指向新的空间,而_pos还指向原来的空间。
在这里插入图片描述
当执行*pos=val后,原来空间中的值变了,而指向新空间的_finish+1向后移动了一位。
在这里插入图片描述
解决办法:更新pos。
即先求出pos和_start之间的距离,当扩容后,更新pos的指向,即让pos也指向新的空间。
在这里插入图片描述

当我们将代码修改了后,可以解决上面的问题,但是又会出现下面的两种情况的迭代器失效问题。
(1) insert里面没有发生扩容,但是pos指向了新插入的元素,而不是原来的3了,是迭代器失效。
我们在测试代码中使用find函数找到vector中3元素的位置,因为find函数为std模板生成的函数,所以返回的是一个迭代器,即返回的是指向3的一个迭代器。但是当我们向pos位置插入30元素后,此时再(*pos)++,会发现是新插入的元素30++变为31了,而不是3++变为4。这也是迭代器失效问题,因为pos指向的是元素3,但是(*pos)++后3没有变为4,而是30变为31了。
在这里插入图片描述
(2) insert里面发生了扩容,insert里面的pos发生了改变,但是测试代码里面的pos没有发生改变。也是迭代器失效。
我们发现当在insert里面发生了扩容之后,在insert里面已经更新了pos,但是测试代码里面的pos还没有变。因为我们调用insert时是值传递,所以insert中修改的为pos的拷贝,而测试代码中的pos并没有改变。所以(*pos)++是将pos指向的原来的空间里的数据++了。

在这里插入图片描述
那么我们可以将insert修改为传引用传参。

在这里插入图片描述
在这里插入图片描述
传引用传参虽然解决了上面的问题,但是当我们直接向insert中传入begin和end时又会出错,这是因为begin中是传值返回,传值返回会发生拷贝,而拷贝生成的临时变量具有常性。所以不能使用传引用来修改。

在这里插入图片描述

在这里插入图片描述
我们查看st文档可以看到insert函数有一个iterator的返回值,即库里面的解决办法是传回新的pos迭代器。当使用完insert后,将测试代码的pos赋值为insert的返回值即可更新pos。所以我们也使用这样的方法。std库里面的迭代器在insert后,如果在insert里面进行了扩容,而没有将pos接收insert的返回值从而更新pos迭代器时,下面再使用pos也会出现错误。这时就需要使用pos = v1.insert(pos, 30);更新pos迭代器,然后才不会出错。但是insert以后,我们就认为pos失效了,不能再使用。
在这里插入图片描述
在这里插入图片描述

下面我们再来进行erase函数的实现。
在这里插入图片描述
在这里插入图片描述
我们实现的erase,在erase之后,pos迭代器没有失效,但是std库里面的pos在erase之后会失效。即在windows下的VS中会中止程序。
在这里插入图片描述
在这里插入图片描述
但是同样的代码在Linux下的g++中不会中止程序。erase g++的实现和我们的实现类似。
在这里插入图片描述
在这里插入图片描述

那么erase之后,我们认为pos失效吗?
下面为删除的是最后一个元素。VS会报错。
在这里插入图片描述
在这里插入图片描述
但是在Linux下的g++中还是可以运行,但是此时pos迭代器指向的位置已经越界了,是不应该被访问的。
在这里插入图片描述
在这里插入图片描述

结论:erase之后,pos失效了,不要访问,行为结果未定义。因为不同编译器下的结果不同。
所以insert之后pos不要访问,因为pos可能为野指针。erase之后pos也不要访问,因为可能pos指向的位置可能越界。

下面我们使用erase来进行一个练习,删除所有的偶数。
在window下的VS中,我们使用下面的代码来进行删除所有的偶数,在使用erase之后,VS编译器会强制检查,如果it没有更新则就会出错。
在这里插入图片描述
在这里插入图片描述

同样的代码在Linux下的g++中运行的情况如下。
当最后一个数为奇数,但是没有进行it迭代器更新时,在g++中可以正常执行,因为g++中不会进行强制检查。
在linux中使用g++编译时,如果代码中使用了c++11的语法,就要在编译时加上-std=c++11,即按照c++11的语法编译。

-std=c++11

在这里插入图片描述
在这里插入图片描述
但是当最后一个数为偶数时,程序会出现段错误。
在这里插入图片描述
在这里插入图片描述
我们经过下面的分析后发现是因为我们的删除逻辑不对。
在这里插入图片描述
那么我们将判断条件改为it<v1.end()。it就为图中的pos,v1.end()就为图中的finish。此时发现程序没有出现错误。但是这样的改法并不能真正的解决问题。
在这里插入图片描述
在这里插入图片描述
如果我们有连续的偶数在一起时,此时发现有的偶数没有被删除。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们知道上面的问题都是因为rease之后迭代器失效问题和我们的删除逻辑有问题,所以我们在使用erase之后需要将迭代器更新,而c++的文档中也写了erase函数返回的就是最新的迭代器,所以我们在每次使用完erase之后需要进行迭代器更新。
在这里插入图片描述
当我们将代码改为这样时,就可以实现删除偶数了。
下面为在VS下可以正常删除偶数了。
在这里插入图片描述
此时在linux下的g++中程序也正常运行。
在这里插入图片描述
在这里插入图片描述
所以我们将自己写的erase也改为将新的pos返回。
在这里插入图片描述
然后我们实现vector的析构函数。
在这里插入图片描述

然后我们实现构造函数中的将n个元素初始化为val的构造函数。我们知道第二个缺省参数val为一个匿名对象的引用,但是我们之前说过匿名对象生命周期只在这一行,那么在函数中val引用的匿名对象不就销毁了吗?

在这里插入图片描述
我们看到匿名对象A()的生命周期只在那一行。
这是因为当这行之后没有人会使用这个匿名对象了,所以这个匿名对象的生命周期只在当前一行。
在这里插入图片描述
当使用const修饰的引用指向这个匿名对象时,这个匿名对象就不会在这一行之后销毁,会随着xx的销毁而销毁。
const引用会延长匿名对象的生命周期到引用对象域结束,因为以后使用xx就代表匿名对象。
在这里插入图片描述
所以我们将下面的构造函数这样写。并且在写这个构造函数时记得初始化,因为此时this里面的值都为随机值,如果不初始化,那么在reserve中求的capacity和size都是不对的,所以要记得将_start、_finish、_end_of_storage初始化。
在这里插入图片描述

我们看到在c++文档中还有一个这样的构造函数模板。这个相当于一个迭代器区间初始化的模板。这里面为什么不使用iterator而使用InputIterator,是因为这个迭代器区间不是必须要用vector的迭代器区间,它可以使用一个string类类型对象的迭代器区间来进行初始化。可以看到可用s1的迭代器区间来初始化v1,所以不能写iterator,因为iterator只是vector里面的类型的迭代器。
在这里插入图片描述
在这里插入图片描述
所以我们自己实现迭代器区间初始化的构造函数时也写一个模板。然后我们将迭代器区间的内容都push_back到vector中。
在这里插入图片描述
当我们写完这个模板后,测试时发现出现了下面的非法的间接寻址错误。这是因为当没有template < class InputIterator >模板时,vector< int > v1(10,5)会去匹配vector(size_t n, const T& val = T())这个构造函数,然后此时int类型的10会进行类型转换变为size_t类型。而当有了template < class InputIterator >模板后,vector< int > v1(10,5)会去匹配template < class InputIterator >模板生成的构造函数,因为这个函数不会进行类型转换,编译器认为是最匹配的。所以就会出现将int类型的变量当作地址来进行寻址,然后就出现了非法的间接寻址错误。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以使用下面的两种方法来解决这个错误。
解决办法1:在调用vector(size_t n, const T& val = T())构造函数时,使用vector< int > v(10u, 5);这样的形式调用,因为加上u后10就表示无符号整数,就不会发生类型转换,而当有一个匹配的函数时,编译器就不会再根据template < class InputIterator >模板再生成函数了。

在这里插入图片描述
解决办法2:提供一份更合适的vector(size_t n, const T& val = T())构造函数的重载版本。这样vector< int > v(10, 5);就会调用vector(int n, const T& val = T())构造函数了。
在这里插入图片描述
在这里插入图片描述
当我们实现了template < class InputIterator >这个模板之后,我们也可以使用其它类型的迭代器区间来初始化vector的内容。
在这里插入图片描述

其实不只是在vector中使用了template < class InputIterator >这样的迭代器区间初始化模板,在sort中也使用了类似的方法来实现可以接收任意类型的迭代器来调用sort方法。
在这里插入图片描述
在这里插入图片描述
sort函数默认是升序,如果不传第三个参数默认就是升序。当我们创建一个greater< int >类型的对象当作第三个参数传入sort时,此时sort为降序排序。
在这里插入图片描述
在这里插入图片描述

3、vector深浅拷贝问题

当实现了vector上面的一些功能后,我们再来看看拷贝构造函数,我们看到编译器自动生成的拷贝构造函数为浅拷贝,即将v1和v2指向了同一片空间,这样会出现两次析构函数的调用,所以会出现错误。
在这里插入图片描述
我们可以自己写拷贝构造函数。
在这里插入图片描述
在这里插入图片描述
我们上面写的拷贝构造函数可以将vector< int >类型的对象进行正确的拷贝,但是当遇到vector< std::string >类型的对象时,就会出现错误。这是因为vector里面的string类类型的对象拷贝时也涉及到深浅拷贝问题,而使用memcpy(_start,v._start,sizeof(T)*v.size())对于string类类型对象来说是浅拷贝,所以此时v3和v4的里面存的是同一个string类类型对象。可以看到下面v3和v4中的第一个元素都是string类类型的对象,这两个string类类型的对象中_Ptr相等,即这两个string类类型对象指向了同一个字符串。所以v3和v4中的每个string类类型对象会调用两次析构函数,所以会出现错误。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
出现这个错误的原因是我们在拷贝构造函数中使用的memcpy函数来进行的拷贝,所以我们需要将memcpy浅拷贝换成深拷贝函数,所以我们使用自定义类型的赋值运算符重载函数,因为string类的赋值运算符重载函数是深拷贝,它会新开辟一片空间然后复制原来的字符串。
在这里插入图片描述
在这里插入图片描述

当我们向v4中插入元素,让v4进行扩容时,此时又会出现错误,因为reserve扩容函数中使用的数据拷贝也是memcpy,所以我们也需要将reserve中的memcpy浅拷贝函数换为自定义类型的赋值运算符重载函数,因为赋值运算符重载函数是深拷贝。此时新的空间中的string类类型对象的_str还指向已经释放的空间里面的内容,所以会出现错误。

在这里插入图片描述
在这里插入图片描述
我们将reserve函数里面的memcpy函数也改变后,就不会出现错误了。
在这里插入图片描述
在这里插入图片描述

但是此时我们测试杨辉三角类时,又出现了错误。即我们创建一个vector< vector< int > >用来接收Solution类的成员函数generate返回的一个vector< vector< int > >数组。这是因为我们在拷贝时使用了赋值运算符重载函数,但是我们并没有重写vector的赋值运算符重载函数,所以vector使用默认生成的赋值运算符重载函数,默认生成的为浅拷贝,所以在执行< vector < vector< int > > ret = Solution().generate(5)时,虽然ret和vv的_start、_finish、_end_of_storage指向了不同的空间,但是因为将ret[0] = vv[0]时发生了浅拷贝,所以ret[0]中的vector< int >的_start、_finish、_end_of_storage和vv[0]的vector< int >的_start、_finish、_end_of_storage相同,即ret中的vecto< int >和vv中的vector< int >都两两指向了同一片空间,而vv的空间当出了generate函数就调用析构函数被销毁了,而ret中的vector< int >中的_start、_finish、_end_of_storage还指向了这些空间,所以才会出错。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们需要重写vector的赋值运算符重载函数,将该函数写为深拷贝就可以解决问题了。
在实现vector的赋值运算符重载函数时,我们先实现swap函数,然后复用swap函数来完成赋值运算符重载函数。
当我们执行 v1 = v2时,赋值运算符重载函数的实参就为v2,而该函数为传值传参,所以当进入赋值运算符重载函数时,会调用拷贝构造函数创建一个临时对象v,而因为我们的拷贝构造函数使用new来申请空间,所以这个临时对象v被创建在堆区中,并且该临时对象v里的内容都和v2相同,这个临时对象v就相当于v2的副本,此时我们将这个临时对象v传入swap函数中,将v1的_start、_finish、_end_of_storage和这个临时对象v的_start、_finish、_end_of_storage进行交换,然后此时v1指向的_start、_finish、_end_of_storage中的内容和v2的内容相同,当执行完swap函数后将此时的v1返回。然后退出赋值运算符重载函数,因为临时对象v的作用域在赋值运算符重载函数中,所以退出赋值运算符重载函数时,临时对象v就会调用自己的析构函数将_start、_finish、_end_of_storage的内容释放,这样对象v1的原来的内存就被释放了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

(四)激光线扫描-光平面标定

在上一章节,已经实现了对激光线条的中心线提取,并且在最开始已经实现了对相机的标定,那么相机标定的作用是什么呢? 就是将图像二维点和空间三维点之间进行互相转换。 1. 什么是光平面 激光发射器投射出一条线,形成的一个扇形区域平面就是光平面,也叫光刀面,与物体相交…

linux下查找文件的相关命令

linux下查找文件的相关命令 运行环境&#xff1a;centos7 参考来源&#xff1a;man、鸟哥入门书籍 一、脚本文件查找&#xff1a;which/type 1. which man手册描述&#xff1a; 返回当前环境可以被执行的文件&#xff08;或链接&#xff09;的路径。搜索PATH变量匹配参数中…

vuejs开发环境搭建

Vuejs是一个前端页面应用开发框架&#xff0c;它基于标准 HTML、CSS 和JavaScript 构建&#xff0c;支持不同的JavaScript开发规范&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面&#xff0c;本文主要描述Vuejs开发环境的搭建。Vuej…

大规模语言模型--训练成本

目前&#xff0c;基于 Transformers 架构的大型语言模型 (LLM)&#xff0c;如 GPT、T5 和 BERT&#xff0c;已经在各种自然语言处理 (NLP) 任务中取得了 SOTA 结果。将预训练好的语言模型(LM) 在下游任务上进行微调已成为处理 NLP 任务的一种 范式。与使用开箱即用的预训练 LLM…

基于SSM的视频点播系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

C++语言GDAL批量裁剪多波段栅格图像:基于像元个数裁剪

本文介绍基于C 语言的GDAL模块&#xff0c;按照给定的像元行数与列数&#xff0c;批量裁剪大量多波段栅格遥感影像文件&#xff0c;并将所得到的裁剪后新的多波段遥感影像文件保存在指定路径中的方法。 在之前的文章中&#xff0c;我们多次介绍了在不同平台&#xff0c;或基于不…

TouchGFX之后端通信

在大多数应用中&#xff0c;UI需以某种方式连接到系统的其余部分&#xff0c;并发送和接收数据。 它可能会与硬件外设&#xff08;传感器数据、模数转换和串行通信等&#xff09;或其他软件模块进行交互通讯。 Model类​ 所有TouchGFX应用都有Model类&#xff0c;Model类除了存…

色彩一致性自动处理方法在遥感图像中的应用

前言 在获取卫星遥感影像时&#xff0c;由于受不均匀的光照、不同的大气条件和不同的传感器设备等因素的影响&#xff0c;遥感影像中会存在局部亮度和色彩分布不均匀的现象&#xff0c;下面是在BigMap地图下载器中收集的几幅谷歌卫星影像&#xff0c;像下面这种都是拼接好的影像…

从零开始的C++(四)

上篇链接&#xff1a;http://t.csdnimg.cn/3nyT9 1.拷贝构造函数&#xff1a; 上篇中介绍了析构函数&#xff0c;即在对象销毁时自动调用的函数&#xff0c;常用于含有malloc、fopen等成员变量的对象。然而&#xff0c;在将对象做函数实参进行值传递的时候&#xff0c;可能会…

Unity宣布自2024年起将根据游戏安装量收费,你对此有何看法?

文章目录 每日一句正能量前言Unity的来历Unity的应用对于收费的看法个人角度&#xff1a;公司角度&#xff1a; 后记 每日一句正能量 水与水之间有距离&#xff0c;但地心下直相牵&#xff0c;人与人之间有距离&#xff0c;但心里时刻挂念&#xff0c;发条短信道声晚安&#xf…

【强化学习】05 —— 基于无模型的强化学习(Prediction)

文章目录 简介蒙特卡洛算法时序差分方法Example1 MC和TD的对比偏差&#xff08;Bias&#xff09;/方差&#xff08;Variance&#xff09;的权衡Example2 Random WalkExample3 AB 反向传播(backup)Monte-Carlo BackupTemporal-Difference BackupDynamic Programming Backup Boot…

深入浅出,SpringBoot整合Quartz实现定时任务与Redis健康检测(一)

目录 前言 环境配置 Quartz 什么是Quartz&#xff1f; 应用场景 核心组件 Job JobDetail Trigger CronTrigger SimpleTrigger Scheduler 任务存储 RAM JDBC 导入依赖 定时任务 销量统计 Redis检测 使用 ​编辑 注意事项 前言 在悦享校园1.0中引入了Quart…

番外3:下载+安装VMware(前期准备)

step1: 查看自己笔记本电脑配置&#xff1b; step2: 下载并安装VMware&#xff08;下载地址www..kkx.net/soft/16841.html&#xff09;这里选择本地普通下载&#xff1b; step3: 安装VMware过程中需要填写密钥&#xff08;本人用的最后一个&#xff09;; #UU54R-FVD91-488PP-7N…

国庆day4

运算符重载代码 #include <iostream> using namespace std; class Num { private:int num1; //实部int num2; //虚部 public:Num(){}; //无参构造Num(int n1,int n2):num1(n1),num2(n2){}; //有参构造~Num(){}; //析构函数const Num operator(const Num &other)cons…

微服务技术栈-Ribbon负载均衡和Nacos注册中心

文章目录 前言一、Ribbon负载均衡1.LoadBalancerInterceptor&#xff08;负载均衡拦截器&#xff09;2.负载均衡策略IRule 二、Nacos注册中心1.Nacos简介2.搭建Nacos注册中心3.服务分级存储模型4.环境隔离5.Nacos与Eureka的区别 总结 前言 在上面那个文章中介绍了微服务架构的…

【C语言】函数的定义、传参与调用(一)

目录 导读&#xff1a; 1. 为什么要用函数 2. C语言中函数的分类 2.1 库函数 2.1.1 什么是库函数 2.1.2 C语言常用的库函数 2.2 自定义函数 2.2.1 什么是自定义函数 2.2.2 定义函数的方法 2.2.3 举例 3. 函数的参数 3.1 传参不同的对比 3.2 形式参数&#xff08;形…

以太网基础学习(一)——以太网概述

一、以太网概述 以太网(Ethernet)指的是由 Xerox公司创建并由Xerox、Intel和 DEC公司联合开发的基带局域网规范&#xff0c;通用的以太网标准于1980年9月30日出台&#xff0c;是当今现有局域网采用的最通用的通信协议标准&#xff08;是局域网的一种&#xff09;。 以太网是一种…

libevent源码学习笔记

libevent源码学习笔记 libevent安装libevent源码解析&#xff08;1&#xff09;事件对象&#xff08;2&#xff09;事件操作&#xff08;3&#xff09;事件循环&#xff08;4&#xff09;事件处理 常用指令问题记录问题一&#xff1a;长连接的管理问题二&#xff1a;连接关闭问…

WebSocket实战之三遇上PAC

一、前言 前两天销售数据实时刷新功能开发测试完成&#xff0c;开开心心部署到生产环境&#xff0c;然后直接懵逼傻眼了&#xff0c;竟然连接不上WebSocket服务端&#xff0c;浏览器端请求头报 Provisional headers are shown 信息&#xff0c;然后采用一系列操作排查问题。 …

DS线性表之链表

前言 我们上一期介绍了顺序表&#xff0c;它的底层就是数组&#xff0c;我们也分别对顺序表的动态版本和静态版本进行了实现&#xff01;并且分析了顺序表的优缺点&#xff0c;优点是&#xff1a;尾插、尾删效率很高&#xff0c;其时间复杂度是O(1)&#xff1b;缺点是&#xff…