前言
学习完string,之后学习的就是vector。vector就是之前C语言中讲到过的顺序表,用三个变量分别记录资源的位置,容器的容量和容器中元素个数。原来的写法是直接使用指针加两个int变量,而标准库中,三个都是由指针确定的,一个指向资源开头,一个指向资源容量,一个指向最后元素后一位。
vector和string是相似的,vector相当于string中的char元素变为其他元素。vector相当于是用模版,将元素的类型范围扩大了。在函数的使用方面vector也和string极为相似,所以再介绍一次就好像炒闲饭一样。为了方便更多的读者了解vector,这里对于他的函数的使用还是会继续讲解一段。
一、vector简介
vector是表示大小可以改变的数组的序列容器。
与数组一样,vector对其元素使用连续的存储位置,这意味着也可以使用指向其元素的常规指针上的偏移量来访问其元素,并且与数组中的效率一样高。但与数组不同,它们的大小可以动态变化,其存储由容器自动处理。
在内部,vector使用动态分配的数组来存储它们的元素。当插入新元素时,可能需要重新分配此数组以增加大小,这意味着分配一个新数组并将所有元素移动到其中。就处理时间而言,这是一项相对昂贵的任务,因此,向量不会在每次将元素添加到容器时重新分配。
相反,vector容器可以分配一些额外的存储空间来适应可能的增长,因此容器的实际容量可能大于容纳其元素所需的存储空间(即其大小)。库可以实现不同的增长策略来平衡内存使用和重新分配,但在任何情况下,重新分配都应该只以对数增长的间隔发生,这样在向量末尾插入单个元素就可以以摊销的恒定时间复杂度提供(见push_back)。
因此,与数组相比,vector消耗更多的内存,以换取管理存储和以高效方式动态增长的能力。
与其他动态序列容器(双端队列、列表和forward_lists)相比,vector访问其元素(就像数组一样)非常高效,并且从其末尾添加或删除元素也相对高效。对于涉及在末尾以外的位置插入或删除元素的操作,它们的性能比其他操作差,并且迭代器和引用的一致性不如列表和forward_lists。
二、vector内函数作用
介绍完vector,我很还需要知道怎么去操作它。那么vector这个类里面究竟提供了什么函数供程序员使用呢?
1、构造函数
1.1、构造函数简介
vector包括四种构造函数,他们的功能分别为:
(1) 空容器构造函数(默认构造函数)
构造一个没有元素的空容器。
(2) 填充构造函数
构造一个包含n个元素的容器。每个元素都是val的副本。
(3) 范围构造器
构造一个包含与范围[first,last)一样多的元素的容器,每个元素都按照相同的顺序从该范围内的相应元素构造而成。
(4) 复制构造函数
构造一个容器,其中包含x中每个元素的副本,顺序相同。
1.2、构造函数的使用举例
// vector的构造函数
#include <iostream>
#include <vector>void test_vector1()
{// 构造函数的使用顺序与上述相同:std::vector<int> first; // 默认构造std::vector<int> second (4,100); // 填充构造std::vector<int> third (second.begin(),second.end()); // 迭代器构造std::vector<int> fourth (third); // 拷贝构造// 迭代器构造函数也可用于从数组构造:int myints[] = {16,2,77,29};std::vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );std::cout << "The contents of fifth are:";for (std::vector<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)std::cout << ' ' << *it;std::cout << '\n';
}
如上述代码所示,四种构造的使用方法,以及迭代器遍历。值得注意的是vector并没有像string那样能够直接的输出元素,这是因为作为字符串我们当然需要的只有直接输出,而vector中的元素不一定是连续输出有意义的数据。所以vector并没有重载流插入和输出的函数。
2、析构函数
2.1、析构函数简介
作用,除了作用销毁vector中的内容,释放资源。
2.2、析构函数的使用举例
结束了生命周期后自动调用,不做举例。
3、赋值重载
3.1、赋值重载函数简介
为容器分配新内容,替换其当前内容,并相应地修改其大小。
在调用之前,容器中保存的任何元素都会被分配或销毁。
3.2、复制重载的使用举例
// vector =重载
void test_vector2()
{std::vector<int> foo (3,0);std::vector<int> bar (5,0);bar = foo;foo = std::vector<int>();std::cout << "Size of foo: " << int(foo.size()) << '\n';std::cout << "Size of bar: " << int(bar.size()) << '\n';
}
底层逻辑是创建临时变量进行深拷贝,然后与需要赋值的对象交换资源,最后临时变量析构,释放原来的资源。
4、vector的迭代器函数
4.1、迭代器简介
begin()返回vector迭代器初始位置,end()返回vector迭代器最后位置。迭代器遵循左闭右开的规则,这是C++函数共通的。
而rbegin()和rend()是反向迭代器的初始位置和结束位置。
vector的迭代器支持“++”,“--”,“+n”,“-n”,是随机迭代器。
4.2、迭代器的使用举例
这里参考构造函数的使用举例,里面有用到迭代器。迭代器在vector里实际上也是指针,不过之后做了升级,可能是由类封装的。
至于迭代器失效,计算机能够检查出来的原因是因为新封装过。迭代器为何会失效,接下来的函数中会讲到。
5、有关容积的函数
5.1、容积函数简介
容积函数包括以下7种,7中函数的功能与之前讲过的string中的同名函数效果一致,这里做一个简单介绍。
5.2、size()简介
作用:返回vector中的元素数量。
size()在之前函数中使用过,这里不再举例。resize()举例中有使用size()。
5.3、resize()简介和举例
调整容器大小,使其包含n个元素。
如果n小于当前容器大小,则内容将减少到其前n个元素,删除(并销毁)超出的元素。
如果n大于当前容器大小,则通过在末尾插入尽可能多的元素来扩展内容,以达到n的大小。
如果指定了val,则新元素将初始化为val的副本,否则将进行值初始化。
如果n也大于当前容器容量,则会自动重新分配分配的存储空间。
请注意,此函数通过插入或删除容器中的元素来更改容器的实际内容。
// resize()函数测试
void test_vector3()
{std::vector<int> myvector;// set some initial content:for (int i = 1; i < 10; i++) myvector.push_back(i);myvector.resize(5);myvector.resize(8,100);myvector.resize(12);std::cout << "myvector contains:";for (int i = 0; i < myvector.size(); i++)std::cout << ' ' << myvector[i];std::cout << '\n';
}
5.4、capacity()简介
返回当前为向量分配的存储空间大小,以元素表示。
该容量不一定等于vector大小。它可以相等或更大,额外的空间可以容纳增长,而不需要在每次插入时重新分配。
请注意,此容量并不限制向量的大小。当这个容量耗尽并且需要更多时,它会被容器自动扩展(重新分配存储空间)。向量大小的理论极限由成员max_size给出。
通过调用成员vector::reserve可以显式更改向量的容量。
具体举例在下:
5.5、reserve()简介和举例
要求vector容量至少足以包含n个元素。
如果n大于当前vector容量,则该函数会使容器重新分配其存储空间,将其容量增加到n(或更大)。
在所有其他情况下,函数调用不会导致重新分配,向量容量也不会受到影响。
此函数对向量大小没有影响,也不能更改其元素。
// reserve()函数测试
void test_vector4()
{std::vector<int>::size_type sz;std::vector<int> foo;sz = foo.capacity();std::cout << "making foo grow:\n";for (int i = 0; i < 100; ++i) {foo.push_back(i);if (sz!=foo.capacity()) {sz = foo.capacity();std::cout << "capacity changed: " << sz << '\n';}}std::vector<int> bar;sz = bar.capacity();bar.reserve(100); // this is the only difference with foo abovestd::cout << "making bar grow:\n";for (int i=0; i < 100; ++i) {bar.push_back(i);if (sz!=bar.capacity()) {sz = bar.capacity();std::cout << "capacity changed: " << sz << '\n';}}
}
结果如图所示。
5.6、empty()简介
返回向量是否为空(即其大小是否为0)。
此函数不会以任何方式修改容器。要清除向量的内容,请参见vector::clear。
此函数需要搭配其他函数使用,否则没有太大意义,其作用也如上所述,故不在举例。
5.7、其他
这里还有两个函数没有讲到,是因为使用的时候作用不大,故不细讲。
6、有关元素访问的函数
6.1、元素访问的函数概括
一共有5种访问元素的方法,这里front()函数是用来访问第一个元素的函数,back()是访问最后一个元素的函数,所以不继续做介绍,其他3个函数实用性更强,特别是[]重载。
6.2、operator[]与at()简介和举例
返回对向量容器中位置n处元素的引用。
一个类似的成员函数vector::at与此运算符函数具有相同的行为,除了vector::at被绑定检查,并通过抛出out_of_range异常来发出请求位置是否超出范围的信号。
可移植程序不应使用超出范围的参数n调用此函数,因为这会导致未定义的行为。
返回对向量中位置n处元素的引用。
// []重载测试
void test_vector5()
{std::vector<int> myvector (10); // 在容器中填充10个0std::vector<int>::size_type sz = myvector.size();// 通过[]访问对应位置元素并修改for (unsigned i = 0; i < sz; i++) myvector[i]=i;// 利用[]旋转vector中的数据for (unsigned i = 0; i < sz / 2; i++){int temp;temp = myvector[sz-1-i];myvector[sz-1-i] = myvector[i];myvector[i] = temp;}std::cout << "myvector contains:";for (unsigned i = 0; i < sz; i++)std::cout << ' ' << myvector[i];std::cout << '\n';
}
6.3、data()介绍
返回一个指向向量内部用于存储其自身元素的内存数组的直接指针。
因为向量中的元素保证以与向量表示的顺序相同的顺序存储在连续的存储位置中,所以检索到的指针可以偏移以访问数组中的任何元素。
返回指针后和数组的使用方法相同,所以不做举例,可以参抗[]重载的举例。
7、修改器类函数
7.1、修改器类函数简介
这些功能效果和string中同名函数的效果相同。其中比较简单的有push_back():尾插一个元素到vector中,pop_back():删除最后一个元素。swap():交换两vector个容器中储存的内容。clear():清除vector中所有储存的数据。
其他的函数将会在接下来的介绍中细讲。
7.2、insert()和erase()介绍和举例
这里提出来将这两个函数的原因是因为,这两个函数有迭代器失效的风险。
通过在指定位置的元素之前插入新元素来扩展向量,从而通过插入的元素数量有效地增加容器大小。
这会导致自动重新分配分配的存储空间,如果并且仅当新的向量大小超过当前的向量容量。
因为vector使用数组作为其底层存储,在向量端以外的位置插入元素会导致容器将位置之后的所有元素重新定位到新位置。与其他类型的序列容器(如list或forward_list)对同一操作执行的操作相比,这通常是一个低效的操作。
这些参数决定插入多少个元素以及将它们初始化为哪些值:
// 测试vector中的insert()
void test_vector6()
{std::vector<int> myvector (3, 100);std::vector<int>::iterator it;it = myvector.begin();it = myvector.insert (it, 200);myvector.insert (it, 2, 300);// 因为使用了insert插入元素,所以上一个迭代器失效,需要更新它it = myvector.begin();std::vector<int> anothervector (2, 400);myvector.insert (it + 2, anothervector.begin(), anothervector.end());int myarray [] = { 501,502,503 };myvector.insert (myvector.begin(), myarray, myarray + 3);std::cout << "myvector contains:";for (it = myvector.begin(); it < myvector.end(); it++)std::cout << ' ' << *it;std::cout << '\n';
}
迭代器失效的具体举例放到最后,接下来是erase()介绍和举例。
从向量中删除单个元素(位置)或一系列元素([first,last))。
这有效地减少了被移除的元素数量,从而减小了容器的大小。
因为vector使用数组作为其底层存储,所以擦除向量末端以外位置的元素会导致容器在擦除段后将所有元素重新定位到新位置。与其他类型的序列容器(如list或forward_list)对同一操作执行的操作相比,这通常是一个低效的操作。
// vector中的erase()
void test_vector7()
{std::vector<int> myvector;// 在vector中存入10个元素for (int i = 1; i <= 10; i++) myvector.push_back(i);// 删除第6个元素myvector.erase (myvector.begin() + 5);// 继续删除前三个元素myvector.erase (myvector.begin(), myvector.begin() + 3);// 打印vector中剩余数据std::cout << "myvector contains:";for (unsigned i = 0; i < myvector.size(); ++i)std::cout << ' ' << myvector[i];std::cout << '\n';
}
关于迭代器失效,是指之前使用过删除和插入之后,由于其他元素的位置已经改变,所以指针失去了原来的意义所以失效。比较危险的迭代器失效是数组在扩容的时候开辟了新空间,释放了旧空间,此时使用原来位置的迭代器就已经在访问野指针了。
// 迭代器失效
void test_vector8()
{std::vector<int> myvector(2, 10);std::vector<int>::iterator it = myvector.begin() + 1;std::cout << myvector.capacity() << '\n';for(int i = 0; i < 10; i++){myvector.insert(it, i);}std::cout << myvector.capacity() << '\n';for(auto e : myvector){std::cout << e << " ";}std::cout << '\n';
}
目标是在固定位置插入10个元素,但是执行起来会出错,这是因为扩容导致迭代器的失效,出现了野指针访问,程序崩溃。除了插入,删除也有同样的问题。
// 迭代器失效
void test_vector8()
{std::vector<int> myvector(10, 10);std::vector<int>::iterator it = myvector.begin();// 本意是删除vector中所有偶数元素while(it != myvector.end()){if(*it % 2 == 0)myvector.erase(it);++it;}// 遍历vectorfor(auto e : myvector){std::cout << e << " ";}std::cout << '\n';
}
这里的迭代器失效是因为删除了原来元素之后,it指向的位置虽然没变,但是下一个元素的位置前移了。而这里直接++it,使下一个元素没有检查到。
对于检查更加细致的编译器,当使用迭代器插入或删除之后会告诉你迭代器失效,需要重新给它一个值。所以erase()和insert()都会返回一个迭代器,目的就是给程序员提供接口。
7.3、emplace()和emplace_back()介绍
通过在位置插入新元素来扩展容器。这个新元素是使用args作为构造参数就地构造的。
这有效地将容器大小增加了一个。
当且仅当新的向量大小超过当前向量容量时,才会自动重新分配分配的存储空间。
因为vector使用数组作为其底层存储,在向量端以外的位置插入元素会导致容器将位置后的所有元素移动一个到新位置。与其他类型的序列容器(如list或forward_list)执行的操作相比,这通常是一种低效的操作。有关直接在末尾扩展容器的成员函数,请参见emplace_back。
通过调用allotor_traits::construct并转发args来就地构造元素。
存在一个类似的成员函数insert,它可以将现有对象复制或移动到容器中。
其实就相当于insert()的升级,可以在函数内部构造对象最后插入到vector中。
emplace_back()则是固定了尾插而已。
现阶段不要求掌握,但是以后会详细举例。
三、vector容器模拟
1、模拟
模拟中函数的作用如上面介绍的一样,且尽量和库函数中的实现方法保持一致。
#include <iostream>
#include <assert.h>
#include <string>using namespace std;namespace lcs
{template<class T>class vector{public:// Vector的迭代器是一个原生指针typedef T* iterator;typedef const T* const_iterator;iterator begin(){return _start;}iterator end(){return _finish;}const_iterator cbegin() const{return _start;}const_iterator cend() const{return _finish;}// construct and destroyvector(int n = 4) // 默认构造:_start(new T[n]),_finish(_start),_end_of_storage(_start + n){}vector(int n, const T& value = T()):_start(new T[n]),_finish(_start),_end_of_storage(_start + n){for(size_t i = 0; i < n; ++i) // 指定初始值构造{push_back(value);}}template<class InputIterator>vector(InputIterator first, InputIterator last){while(first != last) // 迭代器深拷贝{push_back(*first);++first;}}vector(vector<T>& v):_start(new T[4]),_finish(_start),_end_of_storage(_start + 4){reserve(v.capacity());for(auto& e : v) // 放入每一个成员,深拷贝{push_back(e);}}vector<T>& operator=(vector<T> v){swap(v); // 将拷贝构造的值与前者交换return *this;}~vector(){if(_start){delete[] _start;_start = _finish = _end_of_storage = nullptr;}}// capacitysize_t size() const {return _finish - _start; // size = 指针相减}size_t capacity() const{return _end_of_storage - _start; // capacity = 指针相减}void reserve(size_t n){if(n > capacity()) // 如果容量小于设定值,就扩容,拷贝数据,修改内部指针{size_t old_len = size();T* tmp = new T[n];memcpy(tmp, _start, sizeof(T) * old_len);delete[] _start;_end_of_storage = tmp + n;_finish = tmp + old_len;_start = tmp;}}void resize(size_t n, const T& value = T()){if(n < size()) // 需要减小容量{_finish = _start + n;}else // 需要增加元素{reserve(n);while(_finish < _start + n){*_finish = value;++_finish;}}}///access///T& operator[](size_t pos){assert(pos < size());return _start[pos];}const T& operator[](size_t pos)const{assert(pos < size());return _start[pos];}///modify/void push_back(const T& x){assert(_start);// 扩容if(_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);}// 储存数据*_finish = x;++_finish;}void pop_back(){assert(size());--_finish;}void swap(vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}iterator insert(iterator pos, const T& x){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 = x; // 添加数据++_finish; // 记录个数的增加return pos;}iterator erase(iterator pos){assert(pos < _finish);assert(size());//挪动数据iterator _pos = pos + 1;while(_pos < _finish){*(_pos - 1) = *(_pos);_pos++;}--_finish;return pos;}private:iterator _start; // 指向数据块的开始iterator _finish; // 指向有效数据的尾iterator _end_of_storage; // 指向存储容量的尾};}
模拟实现了大部分函数的功能,迭代器仍然是指针的重命名,接下来是对这些模块进行分块测试。
2、测试
#include "vector.hpp"// 测试自写vector
// 构造函数,迭代器,尾插尾删
void test_vector1()
{lcs::vector<int> v1(4, 1);lcs::vector<string> v2;v2.push_back("hello ");v2.push_back("world ");v2.push_back("nice for you");v1.pop_back();for(auto& e : v1){cout << e << " ";}cout << endl;for(auto& e : v2){cout << e << endl;;}
}// 测试插入删除,赋值
void test_vector2()
{lcs::vector<int> v1(4, 666);lcs::vector<int> v2(v1);v1.erase(v1.begin());v1.insert(v1.begin(), 81);v1.insert(v1.begin() + 2, 777);for(auto& e : v2){cout << e << " ";}cout << endl;v2 = v1;for(auto& e : v2){cout << e << " ";}cout << endl;
}// 测试[], capacity类函数
void test_vector3()
{lcs::vector<int> v1(4, 666);v1.erase(v1.begin());v1.insert(v1.begin(), 81);v1.insert(v1.begin() + 2, 777);cout << v1[2] << endl;cout << "capacity()->" << v1.capacity() << endl;v1.reserve(40);cout << "capacity()->" << v1.capacity() << endl;v1.reserve(20);cout << "capacity()->" << v1.capacity() << endl << endl;cout << "size()->" << v1.size() << endl;v1.resize(30);cout << "size()->" << v1.size() << endl;v1.resize(2);cout << "size()->" << v1.size() << endl;for(size_t i = 0; i < v1.size(); ++i){cout << v1[i] << " ";}cout << endl;
}int main()
{test_vector1();test_vector2();test_vector3();return 0;
}
运行后结果与期望相同,虽然没有库中函数那么多重载,但是能全部实现出来其他函数也都不会有太大问题。
结语
到这里博客的内容就告一段落了,这篇博客介绍函数的部分和上一篇介绍string的差不多,我也觉得有些冗余了。如果可以的话,下次将list会将相似的内容省略掉,直接进入正题将有差别的地方,然后模拟。
list的话就有更多的地方要讲了,特别是迭代器部分,和vector、string这样连续空间的容器实现是完全不同的,需要重载函数。更为复杂,特别是const迭代器的实现,不容错过。