当前位置: 首页 > news >正文

第六讲 | vector的使用及其模拟实现

vector

  • 一、vector的使用
    • 1、vector是一个类模板
    • 2、constructor
    • 3、reserve
    • 4、insert和erase
    • 5、遍历
    • 6、关于vector为什么没有重载流插入、流提取运算符
    • 7、关于vector类模板的实例化
    • 8、vector 空间增长问题
    • 9、emplace——暂且别管
    • 10、vector与算法sort、initializer_list
    • 11、能不能用vector< char >代替string
  • 二、vector深度剖析及模拟实现
    • 1、vector的源代码
    • 2、vector的模拟实现
      • (1)、insert erase涉及迭代器失效
        • insert
        • erase
      • (2)、补充两个构造函数
      • (3)、resize
      • (4)、拷贝构造与赋值运算符重载
        • 拷贝构造
        • 赋值运算符重载
      • (5)、迭代器区间构造与n个val构造的联系
      • (6)、vector< string >类的更深层次的深拷贝问题
      • (7)、vector模拟实现代码

文档链接: https://legacy.cplusplus.com/reference/vector/vector/

接下来使用这里就挑选重点的讲解,很多部分会放在模拟实现详细讲解。

一、vector的使用

可以看出来相较于string类,vector的内容真的少了很多。在vector的使用这里,就不必像讲解string类一样花费太多时间,因为使用逻辑大体是一样的,只不过在底层各有不同,很多内容将会放在vector的模拟实现讲解。

vector就是顺序表,底层是动态的数组。存储的数据类型是不确定的,因此,vector也是类模板。使用vector时要包含头文件<vector>

1、vector是一个类模板

在这里插入图片描述
底层相当于:

template<class T>
class vector
{
private:
	T* _a;
	size_t _size;
	size_t _capacity;
};

vector是一个类模板,第一个模板参数是存储的数据类型,第二个模板参数有缺省值(不过这个缺省参数我们一般用不着,所以不用管)。

// 创建存储int类型的vector类对象,会调用默认构造,构造出空的容器
vector<int> v1;

allocator,就是内存池,STL六大组件中的空间配置器,STL的容器会高频得向系统申请空间,为了提高效率,内建了一个内存池供给容器用,这样就不是去堆上要空间,而是去找内存池要空间。内存池没有空间再去找堆要,内存池自己会去找堆。

所以这个参数需不需要我们管?不需要,因为有缺省值,就直接用缺省的空间配置器。

但是在这里写成模板的意思是?加入你在特殊场景下对STL的内存池的效率不满意,你还有更优的方案去设计内存池,那么你就可以按照规范的要求自己设计一个内存池,将类型传递给模板参数,(模板缺省参数与函数缺省参数类似:指定了用指定值,没指定用缺省值)这样就可以用自己建立的内存池啦。

2、constructor

在这里插入图片描述

const allocator_type& alloc = allocator_type(),模板参数都有内存池类型了,为什么还要传递内存池参数呢?因为模板参数只拿到了类型,里面的内存池只能调用默认构造。

比如,自己建立的内存池是需要传参的,但是只给vector类模板传递类型,也就只能调用内存池的类型的默认构造。

但是如果没有默认构造,必须是带参构造,那么就可以借助上面的全缺省默认构造。比如,你自己的内存池带参数。

要使用内存池的参数的机会就很少了:首先你要自己定义内存池,其次这个内存池还不支持默认构造。所以一般都不用管这些参数。

value_type()就是T


initializer_list在32位下大小是8个字节。

在这里插入图片描述

有begin()和end()两个迭代器的接口,这两个迭代器是用原生指针实现的,因为vector底层是个数组,指向数组的原生指针就是天然的迭代器。里面有迭代器就会支持范围for。

void test_vector3()
{
	vector<int> v1{ 1, 2, 3, 4, 5 };
	for (auto v : v1)
	{
		cout << v << " ";
	}
	cout << endl;
}

在这里插入图片描述

更多构造的内容讲解会在后面vector的模拟实现见到。

3、reserve

在这里插入图片描述

vector的reserve这里跟string类的有点不同,vector类里的reserve当n小于capacity时是不会做缩容的,而string类里的reserve是否缩容是依据平台的。

接口使用上没有什么特点,但是有跟string类不一样的3个地方:底层是如何扩容的,还有两个使用特点

4、insert和erase

在这里插入图片描述

insert和erase这里插入数据都没有使用下标,而是都使用的是迭代器。用insert实现头插、erase实现头删不建议多用,因为效率太低了。

void test_vector2()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	// 利用迭代器也可以指定位置插入/删除数据
	v1.insert(v1.begin() + 2, 0);
	for (auto v : v1)
	{
		cout << v << " ";
	}
	cout << endl;
	v1.erase(v1.end() - 1);
	for (auto v : v1)
	{
		cout << v << " ";
	}
	cout << endl;
}

在这里插入图片描述

5、遍历

#include <iostream>
#include <vector>
using namespace std;
void test_vector1()
{
	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 (size_t i = 0; i < v1.size(); ++i)
	{
		cout << ++v1[i] << " ";
	}
	cout << endl;

	// 迭代器
	vector<int>::iterator vit = v1.begin();
	while (vit != v1.end())
	{
		cout << (*vit)++ << " ";
		++vit;
	}
	cout << endl;
	vit = v1.begin();
	while (vit != v1.end())
	{
		cout << ++(*vit) << " ";
		++vit;
	}
	cout << endl;

	// 范围for,本质会转换成迭代器
	for (auto v : v1)
	{
		cout << v << " ";
	}
	cout << endl;

	for (auto v : v1)// 实际只改变了v的值,v1里的值没有被改变
	{
		cout << v + 5 << " ";
	}
	cout << endl;
}
int main()
{
	test_vector1();
	return 0;
}

在这里插入图片描述

6、关于vector为什么没有重载流插入、流提取运算符

与string类不同,vector没有重载流插入流提取运算符,因为string类是字符串,输入输出一个字符串都是很正常的,但是vector是个顺序表,怎么输出是不确定的。若是想让数据一个一个输出/修改,可以用前面遍历的3种方法。

7、关于vector类模板的实例化

vector类模板给编译器,编译器可以实例化出vector< int >、vector< double >、vector< string >等不同的类。

string类作为其他容器的数据类型是很正常的。vector< string >相当于是管理string的数组。

vector< int >是内置类型的数组,vector< string >是自定义类型的数组;vector<vector< int >>也是一个自定义类型的数组,只不过这个数组的不同之处在于,先是实例化出了一个vector< int >类型,再用vector< int >作为模板参数再实例化出一个类vector<vector< int >>。vector< int >是指向int数组的顺序表,vector<vector< int >>是指向vector< int >数组的顺序表。

用类类型vector<vector< int >>开一个数组,可以用resize开也可以用构造开。比如开一个10行5列的二维数组,数组中的每一个元素都为1。再比如杨辉三角有10行,第一行有1个数,第二行有2个数,这样怎么建立呢?

在这里插入图片描述

算法题“杨辉三角”就运用了这一知识。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	vector<vector<int>> vv(10, vector<int>());
	for (size_t i = 0; i < 10; ++i)
	{
		// 10行5列二维数组元素都初始化为1
		//vv[i].resize(5, 1);
		// 10行杨辉三角,首先都初始化为1
		vv[i].resize(i + 1, 1);
	}

	// 下标+[] 修改数组中的值
	vv[4][3] = 5;
	// 相当于转换成
	vv.operator[](4).operator[](3) = 5;

	// vector类模板实例化出vector<string>类型
	vector<string> v1;
	v1.push_back("张三");
	v1.push_back("李四");
	v1.push_back("王五");
	return 0;
}

在这里插入图片描述

8、vector 空间增长问题

capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。

reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。

切记不可用resize代替reserve,resize在开空间的同时还会进行初始化,影响size,而reserve是不会改变size和元素的。

// 测试vector的默认扩容机制
void TestVectorExpand()
{
	vector<int> foo;
	size_t sz = foo.capacity();
	cout << "making foo grow:\n";
	for (size_t i = 0; i < 100; ++i)
	{
		foo.push_back(i);
		if (sz != foo.capacity())
		{
			sz = foo.capacity();
			cout << "capacity changed: " << sz << endl;
		}
	}
}

在这里插入图片描述

vs下string和vector都是1.5倍扩容。但是string里是除了第一次是2倍扩容,其他都是1.5倍扩容,因为vs下string类里给了一个buff,为了防止在堆上频繁扩容,第一次的capacity显示的其实是buff的大小,buff数组满了,就会在堆上开一个相对大的31个空间,后续都是在堆上1.5倍扩容开空间扩容。string类扩容的逻辑不是完整的1.5倍是有特殊情况的,原因是通常定义的字符串都不长,基本都是小块空间,为了防止小块空间在堆上频繁开空间而直接使用内部的buff数组。

但是vector就没有这样的特殊情况。vector存储内容就会多一些,没必要在内部有buff,所以扩容逻辑基本都是1.5倍扩容。

如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够。就可以避免边插入边扩容导致效率低下的问题了。

void TestVectorExpand()
{
	vector<int> foo;
	size_t sz = foo.capacity();
	foo.reserve(100);// 提前将容量设置好,可以避免一遍插入一遍扩容
	cout << "making foo grow:\n";
	for (size_t i = 0; i < 100; ++i)
	{
		foo.push_back(i);
		if (sz != foo.capacity())
		{
			sz = foo.capacity();
			cout << "capacity changed: " << sz << endl;
		}
	}
}

在这里插入图片描述

9、emplace——暂且别管

涉及右值引用和模板的可变参数语法,后续学习“C++11”再讲解。遇到了,它的功能和insert是一样的,比insert更高效。emplace_back功能和push_back一样。功能是一样的,细节在“C++11”再讲解。

10、vector与算法sort、initializer_list

算法跟容器进行配合都是借助迭代器,这也是迭代器的重大意义。迭代器可以让我们很方便去访问、修改容器,同时也可以把算法写成泛型的。算法对于迭代器是有要求的,没有传递对迭代器的类型是会报错的,不过,当前阶段还讲解不了迭代器的各种类型,等到讲完list,迭代器的各种类型才会浮现,那时才会明白各个算法具体用什么类型的迭代器。注意:迭代器区间一定要传递左闭右开区间。

vector可以跟其他算法进行配合,例如sortinitializer_list

initializer_list:花括号里面有多少值都会给initializer_list,initializer_list都会开空间把他存起来,里面有两个指针指向开始和结束。C++11加的构造里面就是遍历initializer_list再push_back到对象里。

在这里插入图片描述

sort:使用时要加上头文件<algorithm>。默认是排升序(< 升序),若是想排降序就涉及到了参数Compare comp,是个访函数(在后续Stack Queue会讲解,这里就讲怎么用)。greater(> 降序)是C++算法库里的类模板 ,也是个访函数,要比较什么类型就实例化出什么类型(例如greater< int >类型),传递对象给参数Compare comp,最后结果就会排成降序的。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
	vector<int> v = { 5, 6, 9, 7, 1, 3 };
	// 默认 升序 <
	//sort(v.begin(), v.end());
	// [1, 5),下标1到4数据排序
	sort(v.begin() + 1, v.end() - 1);// sort(v.begin() + 1, v.begin() + 5);

	// 左闭右开,这里相当于没有值
	// sort(v.begin(), v.begin());

	vector<int> v1 = { 8, 6, 9, 4, 1, 9, 7 };
	/* > 降序 greater */ 
	// 定义成有名对象会写成两行
	//greater<int> gt;
	//sort(v1.begin(), v1.end(), gt);

	// 定义成匿名对象的好处:会写成一行,即用即销毁
	sort(v1.begin(), v1.end(), greater<int>());
	return 0;
}

在这里插入图片描述

11、能不能用vector< char >代替string

vector< char >string都表示char的数组,某种程度说都是char的顺序表,能不能用vector< char >代替string?——不能

第一,string里的数组后面一定是有'\0'的,而vector< char >后面没有’\0’。 string一直肩负着一个原则就是要兼容C语言,有了C++以后去控制字符串就不太用字符数组了,更多的是用string。C++兼容C语言,比如说调用某一个库,这个库可能给你的API接口是C的,MySQL的数据库就是只提供C版本的API;若是调用C语言的文件的一些接口时,文件名可能是用string来存储的,例如fopen(),第一个参数必须是const char*,那就必须调用c_str(底层后面有’\0’),如果存储在vector< char >里就达不到这样的效果。

int main()
{
	/* vector<char>不能代替string*/
	string s1;
	vector<char> s2;
	// string有'\0',vector<char>没有'\0'
	FILE* pf = fopen(s1.c_str(), "r");
	return 0;
}

第二,功能接口有很大差异。例如,插入字符和字符串、查找字符和字符串。 string可以 += 一个字符,也可以 += 一个字符串,而vector< char >没有 += 的概念。就算vector< char >有 += 的概念,但是每次只能 += 一个值,因为它代表的是顺序表,顺序表每次都是插入一个值。写整型字面量时,写的都是一个整型,也没有整型串的概念,只有字符串的概念。查找find这里,string有find一个字符、一个子串,而vector同样没有这个接口,因为它直接用算法库里的find,它只能查找一个值。同理,list也是调用算法库里的find。算法库里有的就直接复用算法库里的就行,这样就不用在vector里面实现。

在这里插入图片描述

二、vector深度剖析及模拟实现

1、vector的源代码

SGI版本是Linux下的版本,我们主要看这个版本。Windows版本不太适合看。

我们头文件包含的是< vector >,< vector >里面又包含了很多头文件,vector由这些头文件构成。

我们往往包库里的头文件包的只是一个壳子,真正的东西在其他地方包含着呢。

看源码很多都是有关联的,一定要直接切入重点。

如何看源代码?
1、了解功能。比如现在我们学习vector,那就要对vector的使用了解清楚。vector源码就是如何去实现vector的。
2、抓核心枝干。若有几十上百个类,梳理出来,二八原则,可能最核心的类就十几个,最核心的成员变量备注出来,核心成员函数,不要去看函数的实现。

在这里插入图片描述

接下来就简单看看源码的框架、枝干:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
vector的源码就先看到这里啦,涉及到的太多细节不要细看,不然容易走火入魔哦~

2、vector的模拟实现

与实现string类似,我们自己写一个是为了学习,不是为了写一个更好的。

typedef受到访问限定符的限制,需要typedef成公有的,要不然外面的访问不了。

模板的定义和声明可以分离,但是不能分离到两个文件。为了方便演示,干脆把函数定义在类模板里了。

写一个最简单的构造。不写析构内存泄漏。

然后为什么一上来就写push_back呢?只有push_back进去了一些数据才会好实现其他接口。

在这里插入图片描述

push_back里的newCapacity肯定大于之前的capacity,为什么还要在reserve里面判断呢?因为还有一些场景需要单独调用reserve,若单独调用reserve传递过来的n比capacity小,但是在vector中reserve只扩容不缩容,这样提前加上判断了reserve什么也不会做。

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

在这里插入图片描述

(1)、insert erase涉及迭代器失效

insert

迭代器失效就是插入数据前的扩容导致的。

insert这里相较于之前在string里实现的insert就好用,不用考虑太多东西。

在这里插入图片描述
假如插入数据不像这样写v1.insert(v1.begin() + 1, 8);。我不知道在第几个值之前插入数据,而是想调用算法库里的find查找一个值的位置,完成在这个位置指向的数据之前插入新数据。

在这里插入图片描述

erase

erase迭代器失效更多的是逻辑上的认为它失效了,因为不排除它可能会在一些平台上缩容或者像vs下进行了强制检查。

实现vector的erase不用像实现string类的erase时考虑边界问题。

在这里插入图片描述

给一组值,把里面的偶数删除掉。由于场景更复杂,先用库里的vector演示:

在这里插入图片描述

结论:这段代码在vs下一定会崩溃,因为迭代器失效了(erase后的迭代器虽然还是指向同一个位置,但是它不认为是指向之前的位置,即迭代器失效了)。vs下对失效的迭代器的检查很严格,也就是这里的it失效了,不能进行访问操作,例如不能it++、it不能进行关系运算符运算it != v.end()等。

在这里插入图片描述

vs下迭代器的实现比较复杂,都不是用原生指针实现的,是用自定义类型实现的而不是用内置类型实现的。

不要认为string、vector的迭代器就是原生指针。SGI3.0版本的vector的迭代器确实是原生指针,但不是所用的版本中的迭代器都是原生指针,也就是说迭代器有多种实现方式,但是具体使用哪一种方式是取决于平台的。

vs下这样实现迭代器的目的就是为了更严格的检查,erase后的迭代器虽然还是指向同一个位置,但是它不认为是指向之前的位置,也就是迭代器失效了。typeid(it).name()可以拿到真实的类型,下图所示这个迭代器是个自定义类型的,其实自定义类型的迭代器也是封装了原生指针(这个在list中会详细讲解,因为list的迭代器就是类似这样实现的,这里就不过度叙述),在这里就说一下本质就是erase里的迭代器实现删除操作,迭代器会失效,就不能it++。

在这里插入图片描述

那是不是所有的平台都是这样实现的呢?当然不是啦,这段代码及示例在Linux下运行得很好。说明库里的东西在不同的编译器下实现也是不同的。

在这里插入图片描述

但是这段代码本身写得就有问题,能在Linux下通过纯属是巧合。

在这里插入图片描述

在Linux g++下是没有强制检查的。若是给了有连续偶数的数组,例如std::vector<int> v = { 1, 2, 2, 3, 4, 5, 6, 7};,不会崩溃但是输出结果不对,会把第二个2跳过去。若最后一个数据是偶数,例如std::vector<int> v = { 1, 2, 2, 3, 4, 5, 6};,运行会直接崩溃。当然啦,这两种情况在vs下运行还是会崩,vs对失效的迭代器进行了严格检查。

在这里插入图片描述
修改代码,运行结果无误:

在这里插入图片描述

但是这个代码还是不对,因为不同平台实现不同,在Linux g++下无误,但是在vs下还是崩溃的,原因还是vs对失效的迭代器进行了强制检查,迭代器失效后就不能访问了。例如,下图中vs下认为迭代器it指向的2被erase后迭代器it失效了,失效迭代器被vs进行了强制检查,所以对迭代器it访问就会报错,例如进行it++、it进行关系运算符运算it != v.end()等都会报错。

在这里插入图片描述

不过vs下erase的实现会比其他平台更好一些,万一有些平台erase会缩容呢。例如,erase后剩下的数据只占总空间的一半,可能会另外开一块更小的空间,拷贝数据,原旧空间释放,那么这时迭代器就还是会指向已经释放的空间,就跟野指针似的了,迭代器失效了。

那么erase到底怎么实现呢?可以看文档里给的指示。库里的erase返回值是个迭代器,指向刚刚被函数调用删除的元素的下一个元素的新位置:

在这里插入图片描述

迭代器失效了就不能访问了,那么怎么样才能访问呢?—— 重新给迭代器it赋值。

在这里插入图片描述

vs下没问题,g++下就更没有问题了。

vector的迭代器的失效有可能是野指针的失效,也有可能是逻辑上的失效。

像上面的代码在哪个平台都能跑,因为没有去访问失效的迭代器,是不依赖于平台的底层实现。

顺序表一般都是扩容但不缩容,因为缩容是以时间换空间。

空间换时间:比时间换空间用的更多,消耗了更多的空间但是时间效率提升了。例如:斐波那契数列,两个变量进行不断的迭代或是数组不断迭代,若是写成递归的话肯定是更慢的。
时间换空间:缩容就是经典的时间换空间。因为缩容不能缩一小部分,只能另开小块空间,拷贝数据,释放原空间,就相当于释放了更多的空间但是消耗了更多的时间。

再把自己实现的erase改成与库里逻辑相似的,测试一下,也没问题:

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);
	iterator it = pos + 1;
	while (it < _finish)
	{
		*(it - 1) = *(it);
		++it;
	}
	--_finish;
	return pos;
}

在这里插入图片描述

总结:insert迭代器、erase迭代器都会失效,这个迭代器失效指的是:

  1. 第一种,迭代器失效像野指针一样,即便后面list里的迭代器不是原生指针,但也是用类去封装指针,其实就认为迭代器失效底层就类似野指针。例如,erase在有些平台会缩容。但是不能认为迭代器就等价于指针。
  2. 第二种就是逻辑上的迭代器失效。比如,迭代器指向数据2的位置,但是因为挪动数据迭代器就不是指向2了,就认为迭代器失效了,不能去访问了。

insert的迭代器失效不能更新pos,形参改变不影响实参,但是可以根据返回值解决,返回一个迭代器指向新插入的位置,这样就能拿到更新后的pos了。

在这里插入图片描述

iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;// 算出pos到_start的相对距离
		size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newCapacity);
		pos = _start + len;
	}
	iterator it = _finish;
	while (it > pos)
	{
		*it = *(it - 1);
		--it;
	}
	*pos = x;
	++_finish;
	return pos;
}
// 给一组值,删除里面的偶数
void test_vector4()
{
	// 场景有点复杂,先用库里的vector演示
	// 再测试一下自己实现的erase
	//bit::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
	zsy::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	//bit::vector<int>::iterator it = v.begin();
	zsy::vector<int>::iterator it = v.begin();
	cout << typeid(it).name() << endl;
	while (it != v.end())
	{
		if (*(it) % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			++it;
		}
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

(2)、补充两个构造函数

在这里插入图片描述

size_type就是size_t:
在这里插入图片描述

n个value构造,缺省值怎么给?缺省值一般都是给常量,模板类型怎么给缺省值?——匿名对象。匿名对象会调用对应的构造函数初始化对象。

// 匿名对象调用无参构造
int i = int();
double d = double();

// 匿名对象调用带参构造
int j = int(1);

// 初始化对象两种方式都可以
int k = 0;
int m(1);// 有名对象

T可能是自定义类型,也可能是内置类型。对于自定义类型会有对应的构造函数,但是对于内置类型如int、double,按理来说没有对应的构造函数,但是C++有了模板后被迫把内置类型升级了后也有构造了,编译器默认生成的构造会把int类型的对象初始化为0,同理,double的会初始化为0.0,指针初始化为空。

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

initializer_list里有迭代器就支持范围for,范围for里面一定要加引用,因为T可能是int、string、还可能是vector int,如果确定是int就不用加引用,这个主要是从效率方面考虑的,不加&会多拷贝一次数据,如果每个元素都是一个特别大的字符串,加引用就能减少拷贝了。

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

il通过原始视图查看,里面本质就是两个指针,一个指针指向数组的开始,一个指针指向数组的最后一个有效数据的下一个位置,传递多少都可以,都可以开空间存储这个数组,size()就是这两个指针相减。

在这里插入图片描述

(3)、resize

在这里插入图片描述

还是一样的,有3种情况。情况1,n比size大但比capacity小;情况2,n比capacity大。这两种情况都直接调用扩容reserve(n),因为扩容逻辑里还会对n再一次判断,大于capacity才会扩容,小于capacity也不会做什么事。情况3,n比size小,直接让_finish = _start + n;

void resize(size_t n, T val = T())
{
	if (n > size())
	{
		reserve(n);
		/*while (_finish < _start + n)
		{
			push_back(val);// 上面的reserve已经扩容了,push_back里还要判断是否扩容就很繁琐了
			++_finish;
		}*/
		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
	else
	{
		_finish = _start + n;
	}
}
void test_vector5()
{
	zsy::vector<int> v = { 1, 2, 3, 4, 5 };
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	v.resize(10);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	v.resize(15, 1);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述

(4)、拷贝构造与赋值运算符重载

拷贝构造

没有自己实现拷贝构造,默认生成的拷贝构造是浅拷贝,与没写string的拷贝构造一样两个指针会指向同一块空间。两个vector< int >对象里的3个指针分别会指向一样的位置,会导致同一块空间析构两次。

在这里插入图片描述

所以要自己实现拷贝构造,实现一个最简单的拷贝构造,与最简单的构造函数一样把三个成员变量初始化为nullptr,在初始化列表初始化还不如直接给缺省值。这样就简化了简单构造和简单拷贝构造的代码量并且初始化了对象(3个成员变量都为nullptr)。

template<class T>
class vector
{
public:
	// 构造函数
	vector()
	{}
	// 拷贝构造函数
	// v2(v1)
	vector(const vector<T>& v)
	{
		reserve(v.capacity());
		for (auto& e : v)
		{
			push_back(e);
		}
	}
private:
	iterator _start = nullptr;// 指向有效数据的开始
	iterator _finish = nullptr;// 指向最后一个有效数据的下一个位置
	iterator _end_of_storage = nullptr;// 指向空间结束的下一个位置
};

在这里插入图片描述

赋值运算符重载

同理,不自己实现还是有问题的:

在这里插入图片描述

现代写法很好用,无论深拷贝的类型如何复杂,例如:数组、链表、二叉树、哈希表。只要把拷贝构造实现好了,实现赋值运算符重载时都可以复用,借助拷贝构造函数完成深拷贝,借助析构函数帮助释放资源。最后再swap(vector里也有两个版本的swap函数)一下即可。

// 赋值运算符重载
// v1 = v3
vector<T>& operator=(vector<T> v)// 传值传参调用拷贝构造
{
	swap(v);
	return *this;
}
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}

在这里插入图片描述

v是局部对象,出了作用域调用析构释放原v1指向的空间及其数据:

在这里插入图片描述

如果与this一样就不交换了,还用在赋值运算符重载中加个判断吗?

在这里插入图片描述

void test_vector6()
{
	zsy::vector<int> v1 = { 1, 2, 2, 3, 4, 5, 6 };
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	zsy::vector<int> v2(v1);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	zsy::vector<int> v3 = { 10, 20, 30 };
	v1 = v3;
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述

(5)、迭代器区间构造与n个val构造的联系

在这里插入图片描述

类模板里的成员函数也可以是个函数模板,可以在这个函数模板里用类模板的模板参数T,并且函数模板自己如果还需要额外的参数就可以自己定义。

在这里为什么不用类模板中的迭代器iterator而是自己要定义个模板参数呢?

在这里插入图片描述

若写成iterator,那就只能用vector的迭代器去进行迭代器区间初始化。但是写成模板参数,就还可以用list/string的迭代器区间初始化。

// 迭代器区间构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

在这里插入图片描述

在这里插入图片描述

我们以为zsy::vector<int> v1(10, 1);会调用n个val构造,但是它实际上是调用迭代器区间构造。所以会有非法的解引用。

那么为什么会调用迭代器区间构造呢?因为int类型的两个实参与迭代器区间构造的参数类型更匹配,两个函数模板参数就都被实例化成int,但是int不能解引用,所以就会报错;n个val构造的参数n是unsigned int,有类型转换。

那么怎么解决这个问题?

可以把n个val构造中的参数n改成int类型,但是第一个问题就是库里的n是size_t类型;第二个问题,若改成了int,万一传递了2个无符号整型,那么还是会出现非法的解引用问题,会导致unsigned int不会去匹配int,因为涉及类型转换,还是会调用更匹配的迭代器区间构造。

在这里插入图片描述

我们可以看看库里是怎么做的?发现n的两种类型都留下了,两个n个val构造函数构成重载,这样就没有非法的解引用问题啦。

在这里插入图片描述

// 自己实现的两种n个val构造函数
vector(int n, const T& val = T())
{
	reserve(n);
	for (int i = 0; i < n; ++i)// n和i类型匹配
	{
		push_back(val);
	}
}
vector(size_t n, const T& val = T())
{
	reserve(n);
	for (size_t i = 0; i < n; ++i)// n和i类型匹配
	{
		push_back(val);
	}
}

再自测运行就没有问题啦~

void test_vector7()
{
	zsy::vector<int> v1 = { 1, 2, 2, 3, 4, 5, 6 };
	zsy::vector<int> v2(v1.begin(), v1.begin() + 5);
	// 用string的迭代器区间构造vector<int>类对象
	string s("hello world");
	zsy::vector<int> v3(s.begin(), s.end() - 1);// 因为T是int,char会转换成对应的ASCII
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
}

在这里插入图片描述

(6)、vector< string >类的更深层次的深拷贝问题

可以不用传string对象,直接传递const char*,常量字符串构造临时对象,单参数构造函数支持隐式类型转化。

在这里插入图片描述

第一个想到的就是扩容reserve可能出现了问题。

在这里插入图片描述

其实像这样的运行问题的排查也类似前面出现的编译错误,delete这里出问题了,但是这里的语法逻辑什么的都没有错误,那就可能是前面的越界、内存等问题。

以当前经验还不足以找到错误,这里就直接说结论,不一一排查了:

这里的错误就是memcpy()导致的,memcpy()是个值拷贝,把4个string依次拷贝给tmp的4个string,字符串故意写的比较长就存储在_str中,与buff无关,因为这种情况下才会出现问题。一个一个字节的拷贝,_str、_size、_capacity都会完成值拷贝。就会导致_start和tmp中的对应拷贝的两个string对象指向同一块空间,例如,查看第一个string对象的Ptr是一样的。

在这里插入图片描述

在这里插入图片描述

delete对_start指向的空间进行释放分两步。第一步先调用string的析构,string指向的空间被delete释放后会被设置成随机值,第二步再调用operator detele[]调用free()把_start指向的整块空间释放了。那么tmp里的_str就是野指针了,那么_str指向的空间置成随机值后为什么会出现乱码呢?随机值在编码下面会构成看到的例如“烫烫烫烫”“屯屯屯屯”等。

在这里插入图片描述

更深层次的深拷贝问题:memcpy()对内置类型没问题,但是对于需要深拷贝的自定义类型会出现浅拷贝的问题。

怎么解决?给vector< string >类做个深拷贝吗?但是vector的扩容逻辑是泛型的,不能保证T一定就是string,T到底是什么是不确定的,即便确定T就是string,那也不能确定vector< string >类的成员变量就叫_str、_capacity、_size,可能叫_Ptr还有可能带着buff。况且不同平台(Linux)下也不一样。所以不能访问对象指向的资源。

正确解决办法:一个一个赋值(两个已经存在的对象之间的赋值操作,赋值运算符重载),这样不管T是内置类型还是自定义类型的扩容逻辑就都没有问题啦。_start和tmp下标为0的string对象的Ptr也不一样,就说明是分别指向两块不同的空间。

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T) * old_size);
			for (size_t i = 0; i < old_size; ++i)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = _start + n;
	}
}

(7)、vector模拟实现代码

// vector.h
#include <assert.h>
using namespace std;
namespace zsy
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}
		size_t capacity() const
		{
			return _end_of_storage - _start;
		}
		size_t size() const
		{
			return _finish - _start;
		}
		vector()
		{}
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (int i = 0; i < n; ++i)// n和i类型匹配
			{
				push_back(val);
			}
		}
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)// n和i类型匹配
			{
				push_back(val);
			}
		}
		vector(initializer_list<T> il)
		{
			reserve(il.size());
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		// 迭代器区间构造
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		// 拷贝构造 
		// v2(v1)
		vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}
		// 赋值运算符重载
		// v1 = v3
		vector<T>& operator=(vector<T> v)// 传值传参调用拷贝构造
		{
			swap(v);
			return *this;
		}
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				if (_start)
				{
					//memcpy(tmp, _start, sizeof(T) * old_size);
					for (size_t i = 0; i < old_size; ++i)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = tmp + old_size;
				_end_of_storage = _start + n;
			}
		}
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(newCapacity);
			}
			*_finish = x;
			++_finish;
		}
		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];// _start数组首元素的地址
		}
		void pop_back()
		{
			assert(_finish > _start);
			--_finish;
		}
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;// 算出pos到_start的相对距离
				size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(newCapacity);
				pos = _start + len;
			}
			iterator it = _finish;
			while (it > pos)
			{
				*it = *(it - 1);
				--it;
			}
			*pos = x;
			++_finish;
			return pos;
		}
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos + 1;
			while (it < _finish)
			{
				*(it - 1) = *(it);
				++it;
			}
			--_finish;
			return pos;
		}
		void resize(size_t n, T val = T())
		{
			if (n > size())
			{
				reserve(n);
				/*while (_finish < _start + n)
				{
					push_back(val);// 上面的reserve已经扩容了,push_back里还要判断是否扩容就很繁琐了
					++_finish;
				}*/
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
			else
			{
				_finish = _start + n;
			}
		}
	private:
		iterator _start = nullptr;// 指向有效数据的开始
		iterator _finish = nullptr;// 指向最后一个有效数据的下一个位置
		iterator _end_of_storage = nullptr;// 指向空间结束的下一个位置
	};
}
// Test.cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include "vector.h"
void print(const zsy::vector<int>& v)
{
	for (auto e : v)
	{
		cout << ++e << " ";
	}
	cout << endl;
}
void test_vector1()
{
	zsy::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);

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

	zsy::vector<int>::iterator vit = v1.begin();
	while (vit != v1.end())
	{
		cout << *vit << " ";
		++vit;
	}
	cout << endl;
	//v1.insert(v1.begin() + 1, 8);
	int x = 2;
	cin >> x;
	auto p = find(v1.begin(), v1.end(), x);
	if (p != v1.end())
	{
		v1.insert(p, x * 10);
		// 这种场景下insert以后pos不能使用,pos可能会失效,迭代器失效类似野指针,不要访问这个迭代器
		cout << *p << endl;// 向6个数据中插入数据时,以为会打印出x * 10对应的值,但是实际不是。
	}
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	print(v1);
}
void test_vector2()
{
	zsy::vector<int> v1(10, 1);
	zsy::vector<size_t> v3(10u, 1u);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	zsy::vector<int> v2 = { 2, 5, 8, 1, 3, 7 };
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
}
// 简单测试自己实现的vector中的erase操作是没有问题的
void test_vector3()
{
	zsy::vector<int> v = { 1, 2, 3, 4, 5, 6 };
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	int x = 0;
	cin >> x;
	auto p = find(v.begin(), v.end(), x);
	if (p != v.end())
	{
		v.erase(p);
	}
	else {
		cout << "没有找到 " << x << endl;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}
// 给一组值,删除里面的偶数
void test_vector4()
{
	// 场景有点复杂,先用库里的vector演示
	// 再测试一下自己实现的erase
	//bit::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
	zsy::vector<int> v = { 1, 2, 3, 4, 5, 6, 7 };
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	//bit::vector<int>::iterator it = v.begin();
	zsy::vector<int>::iterator it = v.begin();
	cout << typeid(it).name() << endl;
	while (it != v.end())
	{
		if (*(it) % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			++it;
		}
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}
void test_vector5()
{
	zsy::vector<int> v = { 1, 2, 3, 4, 5 };
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	v.resize(10);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	v.resize(15, 1);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}
void test_vector6()
{
	zsy::vector<int> v1 = { 1, 2, 2, 3, 4, 5, 6 };
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	zsy::vector<int> v2(v1);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	zsy::vector<int> v3 = { 10, 20, 30 };
	v1 = v3;
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}
void test_vector7()
{
	zsy::vector<int> v1 = { 1, 2, 2, 3, 4, 5, 6 };
	zsy::vector<int> v2(v1.begin(), v1.begin() + 5);
	// 用string的迭代器区间构造vector<int>类对象
	string s("hello world");
	zsy::vector<int> v3(s.begin(), s.end() - 1);// 因为T是int,char会转换成对应的ASCII
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
}
void test_vector8()
{
	zsy::vector<string> v;
	v.push_back("1111111111111111111111111");
	v.push_back("1111111111111111111111111");
	v.push_back("1111111111111111111111111");
	v.push_back("1111111111111111111111111");
	v.push_back("1111111111111111111111111");
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}
int main()
{
	test_vector8();
	return 0;
}

在这里插入图片描述

相关文章:

  • 绿算轻舟系列FPGA加速卡:驱动数字化转型的核心动力
  • 敏感数据触发后怎么保障安全?
  • Windows10 微软五笔 造词造句
  • 矩阵求导 Ref 0
  • 跨境电商中的几种支付方式——T/T、L/C、D/P、D/A、O/A
  • 【新能源汽车压力采集与数据处理技术方案:从传感器到智能分析的硬核实战指南】
  • The first day of vue
  • openGauss新特性 | 自动参数化执行计划缓存
  • 三层架构与分层解耦:深入理解IOC与DI设计模式
  • 微信小程序实现table样式,自带合并行合并列
  • 网络中的基本概念
  • 虚幻引擎 Anim To Tex| RVT | RT
  • CTF web入门之文件上传
  • 【STL】set
  • 判断一棵树是不是另一棵树的子树
  • 容器实战高手课笔记 ----来源《极客时间》
  • 【C到Java的深度跃迁:从指针到对象,从过程到生态】第一模块·认知转型篇 —— 第二章 开发环境全景搭建:从gcc到JVM的范式迁移
  • 聊聊价值投资
  • 【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
  • Ubuntu24安装Docker详细教程
  • 人社部:对个人加大就业补贴支持,对企业加大扩岗支持
  • “天链”继续上新!长三乙火箭成功发射天链二号05星
  • 上海首个航空前置货站落户松江综合保税区,通关效率可提升30%
  • 加拿大温哥华一车辆冲撞人群,造成多人伤亡
  • 葛兰西的三位一体:重提并复兴欧洲共产主义的平民圣人
  • 王羲之《丧乱帖》在日本流传了1300年,将在大阪展23天