第六讲 | 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
可以跟其他算法进行配合,例如sort
和initializer_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迭代器都会失效,这个迭代器失效指的是:
- 第一种,迭代器失效像野指针一样,即便后面list里的迭代器不是原生指针,但也是用类去封装指针,其实就认为迭代器失效底层就类似野指针。例如,erase在有些平台会缩容。但是不能认为迭代器就等价于指针。
- 第二种就是逻辑上的迭代器失效。比如,迭代器指向数据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;
}