目录
vector的成员函数:
at:
编辑
size:
assign:赋值
insert
find?
erase
swap
shrink_to_fit
编辑
vector的模拟实现:
vector的框架:
构造函数:
size和capacity
reserve函数:
begin和end
[]
尾插函数:
const修饰的begin和end
const修饰的[]
empty函数:
resize:
尾删
insert:
标准库里面的find函数:
at表示访问vector的第n个位置的数据。
我们进行实验:
#include
#include
#include
using namespace std;
void vector_test1()
{
vector v;
v.resize(10, 5);
for (int i = 0; i < v.size(); i++)
{
cout << v.at(i) << " ";
}
cout << endl;
}
int main()
{
vector_test1();
return 0;
}
我们使用resize对v进行初始化,初始化的结果是vector有是个整型元素,十个元素都是5,我们遍历v,使用at函数,打印vector的每一个元素。
size函数的返回值是vector中有效元素的个数。
void func(const vector&v)
{
v.size();
}
void test_vector2()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.size();
func(v);
}
int main()
{
/*vector_test1();*/
test_vector2();
return 0;
}
如图所示的函数,为什么func这里需要传引用呢?
答:因为我们的vector中存储的数据类型不仅可能是内置类型,也有可能是自定义类型,传值传参的过程本身就是拷贝,对于内置类型,拷贝的消耗不大,但是对于自定义类型,拷贝的消耗就很大,所以我们用传引用传参提高效率。
如图所示,为什么传参时需要加上const呢?
答:因为我们是传引用传参,我们得到v是为了读取到v上的size(),假如我们不加const,我们对v或v上的数据进行修改时,v本身也会做出相应的变化,所以我们加上const,表示我们的v是只读的,防止函数内部对v进行修改。
如图所示:这两个size函数的调用是同一个size吗?
答:如图所示:
是同一个size,因为我们的size只有一个接口函数,加上const的原因是我们的size是vector的基础属性,我们可以读取size,但是不能对size进行修改。我们在传参时,发生了权限的缩小,我们的v在test_vector2函数中是可读可写的,但是到了func函数中,我们的v变成了只读函数。
void func(const vector&v)
{
v[0];
v[0]++;
}
void test_vector2()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v[0];
v[0]++;
func(v);
}
int main()
{
/*vector_test1();*/
test_vector2();
return 0;
}
我们进行编译:
func的v[0]++出现错误:原因如下:
答:在test_vector2中,因为我们的v是可读可写的,所以我们的[]匹配的就是这个函数:
在func2中,因为我们的v被const修饰,所以我们的v是只读的,我们调用[]函数匹配的就是这个函数:
因为我们的[]被const修饰,所以我们只能读而不能修改。
总结:1:只读函数,const例如:size()
我们的size()函数本身就是只读的,所以我们用可读的对象或者可读可写的对象都可以调用size()函数。
2:只写函数,非const 例如:push_back()
我们的push_back函数必须要求调用函数的对象必须是可写的,也就是说const修饰的对象不能调用push_back函数。
3:可读可写的函数 例如:[]和at()函数
我们这些函数有两个接口函数,当我们调用函数的对象是可读可写的,我们就调用第一个函数,假如我们调用函数的对象是只读的,我们就调用第二个函数。
问题:at()和[]函数的区别在哪里?
我们先写一串代码:
void test_vector3()
{
vector v;
v.reserve(10);
for (size_t i = 0; i < 10; ++i)
{
v[i] = i;
}
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
我们进行运行会报错:
这里报的错误是断言错误,原因如下:我们虽然reserve开了10个空间,但是我们并没有resize,所以我们的size依旧是0,我们的[]内部的实现里有一个断言函数:要求[i]中的i要小于size,我们的size为0,所以不满足断言,所以会报错。
假如我们把这里的[]换成at呢?还会报错吗?
依旧会报错,但是这里报错的形式是抛异常。
总结:at和[]的不同点在于报错的形式:
at是通过抛异常的形式进行报错。
[]是通过断言的形式进行报错。
断言报错是有缺点的,因为assert只有在Debug版本下才能生效,在release版本下会失效。
assign有两种写法,第一种写法是以迭代器区间的形式进行赋值,第二种就是普通的赋值。
问题1:为什么这里需要用类摸板,为什么不直接用迭代器。
答:原因是我们的vector中不仅会有内置类型,也可能会有自定义类型,对于自定义类型的迭代器,我们可以用类模板实现。
我们对两种函数进行实验:
void test_vector3()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.assign(10, 1);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
assign表示把vector的前十个元素都赋值为1.
assign的第二种应用。
void test_vector3()
{
vector v,v1;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v1.assign(v.begin(), v.end());
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
assign这里表示把从迭代器v.begin()到v.end()的全部元素都赋值给v1.
我们进行调用。
接下来,我们尝试用string进行赋值。
void test_vector3()
{
/*vector v,v1;
v.push_back(1);
v.push_back(2);
v.push_back(3);*/
vector v;
string str("hello world");
v.assign(str.begin(), str.end());
//v1.assign(v.begin(), v.end());
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
我们把迭代器str.begin()到str.end()的元素赋值给v,我们进行打印。
之所以是数字:我们的vector内的数据类型是int,所以把string中的char类型强制转换成为了int,变成了数字。
我们的string的insert函数的参数使用的是下标,而我们的vecor的insert使用的都是迭代器。
我们在vector中不提供头插和头删,原因是头插和头删需要挪动数据,造成效率降低。
如果我们真的想要使用头插或者头删时,我们可以使用insert和erase来代替。
例如:
void test_vector3()
{
vector v;
v.reserve(10);
v.insert(v.begin(), 1);
for (int i = 0; i < v.size(); i++)
{
cout << v[i] << "";
}
cout << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
我们发现,vector的成员函数并没有find,为什么呢?
我们在std标准库找到了find函数
因为除了string容器之外,其他容器的find函数都是通过迭代器实现的,所以我们把这些容器的find函数直接写成一个,直接写在标准库中。
我们从迭代器first找到last,因为last是容器最后一个元素的下一个位置,所以last一定不是需要找的元素,所以当找到时,我们返回对应的迭代器,没有找到,我们返回last这个迭代器即可。
我们进行实验:
void test_vector3()
{
vector v;
v.resize(10);
for (size_t i = 0; i < v.size(); i++)
{
v[i] = i;
}
vector::iterator it = find(v.begin(), v.end(), 3);
if (it != v.end())
{
v.insert(it, 30);
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
我们的目的是从v中找到3的位置,在3的位置插入10。
问题:string为什么不使用标准库里面的find函数?
答:1:因为string出现的比较早,string出现的时候,标准库还没有完善。
2:因为string可能存在查找子串,我们用迭代器无法实现查找子串。
表示删除vector的元素,分为两个接口函数,第一个接口函数是删除迭代器位置的一个元素,第二个接口函数是删除两个迭代器之间的元素。
具体的使用案例我们在网站就能查到,这里不再赘述。
我们知道,算法库里面也有一个swap函数:
这两个函数哪一个函数更建议使用?
答:我们更建议使用vector的成员函数swap,如图所示:
标准库里面的swap函数是通过类模板实现的,并且中间使用了三次拷贝构造函数,因为vector中的数据类型也可能是自定义类型,对于自定义类型的拷贝构造效率太低,所以我们尽量使用vector自己提供的swap函数。
既然vector已经提供了swap函数,但是为什么标准库std又特殊的为vector提供了swap函数,如图所示:
我们写一串代码进行分析:
void test_vector3()
{
vector v,v1;
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
v1.swap(v);
swap(v1, v);
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
这里出现了两个swap函数,第一个swap函数是vector的成员函数,第二个swap函数是标准库std里面的swap函数。
我们知道,算法库里面的swap函数走的是三次深拷贝,但是我们这里的第二个函数调用的就是算法库里面的swap,是否真的会调用三次深拷贝呢?
答:并不会,如图所示:
我们知道,编译器在匹配摸板的时候,一定找最合适的进行匹配,因为我们已经写好了我们是vector的摸板,所以调用算法库里面的swap函数直接匹配这里的摸板,有了摸板就不需要多次深拷贝了。
简单的说,如图所示:
我们这里调用的swap(v1,v)会进行检查,编译器发现swap的两个参数都是vector,所以我们直接调用转换为调用vector的成员函数swap,也就是这样:v1.swap(v)
所以我们调用的这两个函数,最终的结果是一样的,第一个函数直接调用vector的成员函数swap,第二个调用标准库的swap函数,swap函数再进行识别,我们转而调用vector的成员函数swap。
表示把vector的容量调整到和vector的size相同。
提出一个问题:缩容适合使用吗?
不适合,原因如下:首先,缩容是异地缩容,如图所示:
我们的size是8,我们的capacity是16,假如我们要进行缩容,我们首先开辟一个空间大小为8的一份空间:
然后我们把v1的有效元素放到tmp上,然后让v1指向tmp,然后释放掉v1即可。
这就是异地缩容的原理,所以异地缩容比较繁琐,效率低下,有没有原地缩容?
答:并没有原地缩容,例如:
这是我们向内存申请的空间,假如我们想要释放空间的话,我们是不允许从middle位置释放空间的,因为这一部分空间是连贯的,我们只能从begin位置释放整个空间,所以也就不支持本地缩容。
vector函数支持缩容吗?
答:不支持,我们进行实验:
void test_vector3()
{
vector v,v1;
v1.reserve(10);
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
cout << v1.capacity() << endl;
v1.reserve(3);
cout << v1.capacity() << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
假如vector支持缩容时,我们打印的结果应该为3.
所以vector并不支持缩容。
resize可以缩容吗?
答:不可以,我们进行实验:
void test_vector3()
{
vector v,v1;
v1.reserve(10);
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
cout << v1.capacity() << endl;
v1.resize(3);
cout << v1.capacity() << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
我们进行运行:
resize也不能缩容。
总结:vector一般不支持缩容。
在如今的计算机领域,硬件存储空间越来越大的情况下,时间就要比空间重要的多,所以我们一般不频繁使用缩容函数,因为缩容一定是异地缩容,异地缩容的本质是以时间换空间。
我们对shrink_to_fit进行实验:
void test_vector3()
{
vector v,v1;
v1.reserve(10);
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
cout << v1.capacity() << endl;
v1.shrink_to_fit();
cout << v1.capacity() << endl;
}
int main()
{
/*vector_test1();*/
test_vector3();
return 0;
}
我们进行运行:
成功缩容。
我们首先看一下vector的源代码,我们通过调试就可以看pj版本的源代码:
vector在pj版本下的源代码大概是3000行,这里的内容是开源声明。
sgi的版本更适合我们初学者学习,我们查看sgi版本。
我们把我们在源码中找最重要的代码,进行精简:
我们可以发现,vector的成员变量的雷西那个都是迭代器。
vector的结构图如图所示:
我们按照这个框架模拟实现vector
#pragma once
namespace bit
{
template
clasee vector
{
public:
typedef T* iterator;
private:
iterator _start;
iterator _finish;
iterator _endofstorge;
};
}
T是我们定义的摸板,我们把T*类型重命名为iterator,我们有三个成员变量。
这是源代码中vector的构造函数,其中使用了初始化列表,把三个迭代器都置为0,我们按照他的写法实现构造函数。
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorge(nullptr)
{}
我们的三个成员变量都是迭代器,我们可以先把迭代器理解为指针,对于指针的初始化,我们可以赋值nullptr,初始化列表是在调用函数内容之前就已经实现的。
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endofstorage - _start;
}
我们把迭代器先理解为指针,指针与指针相减的结果为数字,size是有效元素的个数,capacity是容量,我们根据图像就能够写出对应的函数。
void reserve(size_t n)
{
if (n > capacity())
{
T*tmp = new T*[n];
memcpy(tmp, _start, sizeof(T)*size());
delete[] _start;
_start = tmp;
_finish = _start + size();
_endofstorge = _start + n;
}
}
我们进行画图解释:
只有当n大于我们的capacity时,我们才进行扩容,我们的reserve不缩容。
我们的思路是异地扩容,我们新申请一个大小为n的空间tmp,然后把vector上的数据拷贝到tmp上,然后释放掉原来vector的数据,让新的空间指向tmp,再对_finish和endofstorge进行修改。
这里可以用malloc初始化吗?
答:不能,因为malloc只会对内置类型初始化,对于自定义类型不初始化。
new相较于malloc的优点:
malloc需要判断是否申请空间失败,而new不需要,new失败的话会抛异常。
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
T&operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
我们的[]函数内部必须要断言:要求pos一定要小于size()
void push_back(const T&x)
{
if (_finish == _endofstorge)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
++_finish;
}
我们进行尾插的数据类型 T既有可能是内置类型,也有可能是自定义类型,对于内置类型,我们可以不传引用,但是对于自定义类型必须传引用。
我们传了引用,在函数中可能会导致x的值发生变化,我们加上const表示x是只读的。
假如我们的元素数和容量相等,表示我们需要扩容了,又因为capacity的初始化值可能为0,所以我们使用三目操作符,假如capacity为0时,我们把4赋给newCapacity,然后调用reserve函数进行扩容。
判断并执行扩容之后,接下来,进行尾插元素,我们的迭代器可以理解为指针 ,我们在_finish插入元素x,再++_finish即可。
特殊情况:
当我们的vector没有一个元素,我们进行尾插时,我们的newcapacity就被设置为4,然后我们调用reserve函数进行扩容:
这时候,因为我们vector中的元素实际为空,所以我们并不需要进行内存拷贝,所以我们可以优化一下reserve函数
void reserve(size_t n)
{
if (n > capacity())
{
T*tmp = new T*[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T)*size());
delete[] _start;
}
_start = tmp;
_finish = _start + size();
_endofstorge = _start + n;
}
}
接下来,我们测试之前写的代码是否有误。
测试代码
using namespace std;
#include
#include"vector.h"
void test_vector1()
{
bit::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
cout << v.capacity() << endl;
cout << v.size() << endl;
}
int main()
{
test_vector1();
return 0;
}
报错的原因在我们的reserve函数上:
我们画图进行解释:
我们让_start指向tmp对应的空间:
这时候,我们的_start和_finish不在同一块空间内,我们调用size函数:
两块不同的空间进行相加减的值是未知的,我们接下来会对_finsh进行解引用,就会报错。
我们可以这样处理:
我们可以先处理finish,画图进行解释:
我们先处理finish
size函数表示_finish和_start相减,两个指针相减的结果是两个指针区域指向的相同数据的数目,是整型,我们让tmp+_finish.
这时候,我们再把tmp赋值给_start。
这时候就没有什么问题了,我们继续测试:
我们发现,begin和end都有两个接口函数,分别是const和非const。
这两个const有什么区别吗?
答:第二个const:假如调用begin()函数的对象用const修饰了,我们就调用这个begin()函数。
第一个const:我们用const对象调用begin函数,返回的迭代器也用const修饰。
const iterator begin() const
{
return _start;
}
const iterator end() const
{
return _finish;
}
const T&operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
bool empty() const
{
return _start == _finish;
}
当_start和_finsh相等时,也就是当_start=_finsh=nullptr时,vector对象为空。
void resize(size_t n, T val = T())
{
if (n>capacity())
{
reserve(n);
}
if (n > size())
{
while (_finish < _start + n)
{
*_finish = val;
++_finish;
}
}
else
{
_finish = _start + n;
}
}
这里的缺省值可以写0吗?
答:不行,对于内置类型,我们可以用0初始化,但是vector存储的数据既有可能是内置类型,也有可能是自定义类型,而对于自定义类型,我们不能用0去初始化。
我们首先判断n是否大于capacity,如果大于我们需要进行扩容。
判断n是否大于size,大于时需要填元素,我们调用循环把val填入到vector中。
这里表示n小于size,所以我们需要元素,我们可以直接修改_finish的值,就能达到删除数据的目的。
void pop_back()
{
_finish--;
}
我们直接让_finish--来删除数据。
不断的尾删会导致什么样的结果:
using namespace std;
#include
#include
#include"vector.h"
void test_vector1()
{
bit::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
while (1)
{
v.pop_back();
cout << v.size() << endl;
Sleep(100);
}
}
int main()
{
test_vector1();
return 0;
}
我们进行运行:
因为我们的size是无符号类型,所以不断的进行尾删之后,size反而变成一个非常大的数据了。
我们调用库里面的尾删函数进行尝试:
using namespace std;
#include
#include
#include
//#include"vector.h"
void test_vector1()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
while (1)
{
v.pop_back();
cout << v.size() << endl;
Sleep(100);
}
}
int main()
{
test_vector1();
return 0;
}
我们发现,库里面报的错误是断言错误,所以我们可以仿照库的写法对尾删函数进行完善:
void pop_back()
{
assert(_finish >= _start);
_finish--;
}
void insert(iterator pos, const T& val)
{
if (_finish == _endofstorge)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
}
insert表示我们从pos位置处插入元素val。
为什么这里要加上const和&?
答:首先,必须加上引用,因为我们insert插入的极有可能是内置类型,也有可能是自定义类型,传参的过程本质是一个拷贝,对于自定义类型的深拷贝效率太低,消耗很大,所以我们通过传引用来传参,传引用的话我们就要限制函数内部,禁止对val进行修改,所以要加上const。
首先,我们要判断是否需要扩容。
我们的思路是这样的,先把pos位置后的数据全部往后面挪动一个位置,然后才填数据。
我们对insert函数进行实验:
using namespace std;
//#include
#include
#include
#include
#include"vector.h"
void test_vector1()
{
bit::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
bit::vector::iterator it = v.end();
v.insert(it, 15);
for (auto s : v)
{
cout << s << " ";
}
cout << endl;
}
int main()
{
test_vector1();
return 0;
}
我们在调试时发现两处异常:
我们发现,编译器会一直执行while循环,不会终止。
异常2:
我们进行的是尾插,pos与_finish的值却不相等。
这些问题是扩容的问题:
扩容导致了迭代器失效:
因为我们只有我们的capacity和size都为4,我们要插入元素就需要扩容,我们的pos的值和_finish是相等的。
我们的原空间被销毁,所以pos迭代器就失效了,pos和_finish无法比较大小,所以while死循环,pos位置已经被释放了所以无法被继续访问。
我们要做的就是更新pos:
void insert(iterator pos, const T& val)
{
assert(pos <= _finish);
assert(pos >= _start);
if (_finish == _endofstorge)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
}
我们思考一个问题:insert之后的迭代器还能继续使用吗?
如代码所示:
答:最好不要,首先,只要我们因为调用insert而导致扩容,就会导致迭代器失效,it位置已经被释放。
许多人会有疑问:我们不是已经解决了迭代器失效了吗?
我们只是保证函数能正常使用,但是并没有保证迭代器没有失效:如图:
我们调用insert传参传递的it是传值传参,pos只是it的拷贝,pos值的修改并不影响it的变化,所以迭代器it还是失效了 。
那为什么insert函数不传引用传递第一个参数呢?
如图:
为什么不这样写呢?
答:因为限制条件太多 ,例如:
假如我们这样调用函数时,因为我们是传引用,我们的v.begin()是调用begin函数的返回值,是一个临时变量,临时变量具有常数属性,我们无法把临时变量当作参数调用insert函数。
所以,调用insert(pos,val)之后,pos对应的迭代器最好不要再使用。
void test_vector1()
{
bit::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
bit::vector::iterator it = find(v.begin(), v.end(), 3);
if (it != v.end())
{
v.insert(it, 30);
}
for (auto s : v)
{
cout << s << " ";
}
cout << endl;
}
我们进行运行: