之前看了一遍容器和迭代器,但听了学长说的,感觉我的理解有点偏差。打算来学习学习容器的具体用法,敲一敲。
先来学习顺序容器vector的用法。
类似于C风格的数组;元素内存空间连续,每个元素有自己的槽。在vector中可建立索引。可以在任何位置添加或删除新元素,需要线性时间。在尾部执行操作时,实际运行时间为摊还常量。(那么什么是摊还常量呢?)。随机访问单个元素的复杂度为常量时间。
概述
在vector头文件中被定义为一个带有2个类型参数的类模板:一个参数为要保存的元素类型,另一个参数为分配器(allocator)类型。
template <class T, class Allocator = allocator<T>> class vector;
Allocator参数指定内存分配器对象的类型,客户可设置内存分配器。(这章介绍的都是默认模板参数的内存分配器)
固定长度的vector
vector最简单的用法莫过于把它当做C风格数组的替代品(长度固定)。vector提供了一个可以指定元素数量的构造函数,还提供了重载的operator[]来访问和修改元素。
同时C++标准指出,通过operator[]来访问vector边界之外的元素,是UB。
除了使用operator[]运算符之外,还能用at(),front(),back()访问元素。
at() = operator[],不过at()会检查边界,并在越界时抛出out_of_range。
front()和back()分别返回第一个元素和最后一个元素的引用。
下面敲一个来“标准化”考试分数的程序,经过标准化后,最高分100,其他所有分数据此调整。它创建了一个带有10个double值的vector,然后读入10个值,将每个值除以最高分,再乘以100,最后打印出新值。
vector<double> doubleVector(10);
double max = -numeric_limits<double>::infinity();
for(size_t i =0; i < doubleVector.size();i++){
cout << "Enter score " << i+1 << ": ";
cin >> doubleVector[i];
if(doubleVector[i]>max){
max = doubleVector[i];
}
}
max /= 100.0;
for(auto& element: doubleVector){
element /= max;
cout << element << " ";
}
这里要注意的几点是:(1).第一个for循环使用size()方法来确定容器元素的个数。
(2).基于区间的for循环中用auto&而不是auto,因为这里要用引用,才能在每次迭代中修改元素。
动态长度的vector
vector真正好用之处在于它的动态增长。如前面的处理分数程序,如果要再加上一项任意数量的要求,那么则有:
vector<double> doubleVector;
double max = -numeric_limits<double>::infinity();
for(size_t i = 1; true; i++){
double temp;
cout << "Enter score " << i << "(-1 to stop)";
cin >> temp;
if(temp == -1) break;
doubleVector.push_back(temp);
if(temp > max) max = temp;
}
max /= 100.0;
for(auto& element: doubleVector){
element/= max;
cout << element << " ";
}
这创建了一个不包含元素的空vector,然后每读取一个值,通过push_back()方法添加到vector,push_back()能为新元素分配空间。基于区间的for循环不需要做修改。
详解
构造函数和析函数
默认的构造函数的创建一个不包含元素的vector
vector<int> intVector;
但也可以指定元素个数,并指定元素的值,如:vector<int> intVector(10,100)
这个便指定了10个元素,初始化值为100
如果没有提供默认值,则对新对象进行0初始化。0初始化通过默认构造函数创建对象,将基本的整数类型初始化为0,浮点数0.0,指针类型nullptr。
还可以创建内建类的vector,如下所示:vector<string> stringVector(10,"Hello");
用户自定义的类也可:
class Element{
public:
Element(){}
virtual ~Element() = default;
};
...
vector<Element> elementVector;
还可以用初始化列表:vector<int> intVector({1,2,3,4,5,6});
。
initializer_list还可以用于统一初始化:
vector<int> intVector1 = {1,2,3,4,5,6};
vector<int> intVector2{1,2,3,4,5,6};
还可以在堆上分配:auto elementVector = make_unique<vector<Element>>(10);
复制和赋值
存储对象,其析构函数调用每个对象的析构函数。vector类的复制构造函数和赋值运算符对vector中的元素执行深赋值。所以,我们应该通过引用或者const引用向函数和方法传递vector来提高效率。
除了普通的复制和赋值运算符,还有assign()方法,删除所有元素,并添加任意树目的新元素。此方法适用于vector的重用。下面举个例子:intVector包含10个默认值为0的元素,然后通过assign()删除所有的10个元素,并以5个值为100的元素替代:
vector<int> intVector(10);
//other code
intVector.assign(5, 100);
而且assign还能接收initializer_list。现在intVector有4个具有给定值的元素:intVector.assign({1,2,3,4});
vector还提供了swap()方法,可以交换两个vector的内容,并且具有常量时间复杂度。举例:
vector<int> vectorOne(10);
vector<int> vectorTwo(5,100);
vectorOne.swap(vectorTwo);
比较
vector提供了:==,!=,<,>,<=,>=这6个重载的比较运算符。如果两个vector的元素数量相等,且对应元素也相等,那么2个vector相等。其比较用字典顺序(和字符串比较差不多)
下面是一个比较元素类型为int的两个vector的程序:
vector<int> vectorOne(10);
vector<int> vectorTwo(10);
if(vectorOnve == vectorTwo){
cout << "equal!" << endl;
}else{
cout << "not equal!"<< endl;
}
vectorOne[3] = 50;
if(vectorOne < vectorTwo){
cout << "vectorOne is less than vectorTwo" << endl;
}else{
cout <<"vectorTwo is not less than vectorTwo" << endl;
}
vector迭代器
先用迭代器将前面那个区间循环替代掉:
for(vector<double>::iterator iter = begin(doubleVector);
iter != end(doubleVector); ++iter){
*iter /= max;
cout << *iter << " ";
}
这里: vector<double>::iterator iter = begin(doubleVector);
。每个容器都定义了一种名为iterator的类型,来表示该容器类型的迭代器。begin()返回引用第一个元素的迭代器。
然后判断iter是否遇到了元素序列的尾部。++iter则是递增迭代器,以引用vector下一个元素。
循环体里面包含的两句。第一行通过*解除引用iter,从而获得iter引用的元素,然后给这个元素赋值。第二行再次解除引用,将元素流式输出到cout
可以通过auto来简化迭代器初始化:
for(auto iter = begin(doubleVector);
iter != end(doubleVector); ++iter){
...
}
访问对象元素中的字段
如果迭代器是对象,那么可以通过->来调用对象的方法或者访问对象成员。如下列程序,建立了包含10个字符串的vector,然后遍历所有字符串,给每个字符串追加一个新的字符串:
vector<string> stringVector(10,"hello");
for(auto it = begin(stringVector);it != end(stringVector);++it){
it->append(" there");
}
或者用基于区间的循环:
vector<string> stringVector(10,"hello");
for(auto& str : stringVector){
str.append(" there");
}
const_iterator
正如const一样,对const对象调用begin和end,或者cbegin和cend,将会得到const_iterator。这是只读的,不能通过const_iterator修改元素。iterator始终可以转换为const_iterator。因此底下的行为是安全的:vector<type>::const_iterator it = begin(myVector);
然而,const_iterator不能转换为iterator.
在使用auto推断时,应该用cbegin和cend来返回const迭代器。基于区间也可以在区间元素前加const来强制使用const_iterator
迭代器的安全性
通常情况下,迭代器的安全性和指针接近:不安全。比如:
vector<int> intVector;
auto iter = end(intVector);
*iter = 10;
end()对空容器使用,返回的迭代器越过了尾部,试图解除引用会产生UB。(不会有检查的行为。)
如果使用了不匹配的迭代器,则可能出现其他UB行为:
vector<int> vectorOne(10);
vector<int> vectorTwo(10);
for(auto iter = begin(vectorTwo); iter != end(vectorOne); ++iter){
//Loop body
}
其他迭代器操作
vector迭代器是随机访问的,因此可以自由前移、后移、跳跃。下列代码将第5个元素的值改为4:
vector<int> intVector(10);
auto it = begin(intVector);
it += 5;
--it;
*it = 4;
迭代器?索引?
前面写的功能更加用普通的索引也可以完成。那么为什么要用迭代器呢?
- 使用迭代器可以在任意位置 插入、删除元素或者元素序列。
- 迭代器可使用标准库算法。
- 通过迭代器顺序访问元素,通常比索引效率高(vector没有,但是list,map,set中效率提高)。
在vector中存储引用
需包含fuctional头文件,在容器中存储std::reference_wrapper。std::ref()和cref()用于创建非const和const reference_wrapper实例。
string str1 = "Hello";
string str2 = "World";
vector<reference_wrapper<string>> vec{ ref(str1) };
vec.push_back(ref(str2));
vec[1].get() += "!";
cout << str1 <<" "<< str2 << endl;
添加和删除元素
前面push_back可以向vector追加元素。还可以用pop_back()来删除元素。(它不会返回删除的元素,要返回得先调用back()来获得元素。)
通过insert()可以在任意位置插入元素,这个方法在迭代器指定位置添加一个或多个元素。它有5种重载形式:
- 插入单个元素
- 插入单个元素的n份副本
- 从某个迭代器范围插入元素。迭代器范围是半开区间,所以只包含初始迭代器,不包含尾部迭代器。
- 使用移动语义,将给定的元素转移到vector中,插入一个元素。
- 向vector中插入一列元素,这列元素是通过initializer_list指定。
通过erase()在vector中删除元素,clear()删除所有元素。erase有2个形式:1.单个迭代器,删除单个元素。2.2个迭代器,删除范围元素。
如果要删除满足条件的多个元素,以之前的想法是写个循环遍历所有元素。但这个方法有平方复杂度。可以使用删除-擦除惯用法(remove-erase-idiom),线性复杂度。
- insert(const_iterator pos, const T& x):将x插入pos位置
- insert(const_iterator pos, size_type n, const T& x):将x 值在位置pos插入n次。
- insert(const_iterator pos, InputIterator first,InputIterator last):将[first,last)范围内的元素插入pos
template<typename T>
void printVector( const vector<T>& v){
for(auto& element : v) {
cout << element <<" ";
cout << endl;
}
}
vector<int> vectorOne = { 1, 2, 3, 4, 5};
vector<int> vectorTwo;
vectorOne.insert(cbegin(vectorOne)+3, 4);
for(int i = 6; i<=10; i++){
vectorTwo.push_back(i);
}
printVector(vectorOne);
printVector(vectorTwo);
//Add all the elements from vectorTwo to the end of vectorOne
vectorOne.insert(cend(vectorOne),cbegin(vectorTwo),cend(vectorTwo));
printVector(vectorOne);
//now erase the numbers 2 through 5 in vectorOne
vectorOne.erase(cbegin(vectorOne)+1,cbegin(vectorOne)+5);
printVector(vectorOne);
//clear vectorTwo entirely
vectorTwo.clear();
//add 10 coples of the value 100
vectorTwo.insert(cbegin(vectorTwo), 10, 100);
//decide we only want 9 elements
vectorTwo.pop_back();
printVector(vectorTwo);
移动语义
所有的标准库容器都包含移动构造函数和移动赋值函数,从而实现移动语义。这样的一大好处是可以通过传值的方式从函数返回标准库容器,而不会降低性能。
vector<int> createVectorOfSize(size_t size)
{
vector<int> vec(size);
int contents = 0;
for(auto& i : vec){
i = contents++;
}
return vec;
}
...
vector<int> myVector;
myVector = createVectorOfSize(123);
如果没有移动语义,那么每次都进行防复制的话,性能会有很大影响。
push操作在某些情况下也会通过移动语义提升性能。例如,假如有一个类型为字符串的 vector,如下所示:
vector<string> vec
向这个vector添加元素,如下所示:
string myElement(5,'a') //construct the string"aaaaa"
vec.push_back(myElement);
由于myElement不是临时对象,所以pushback会生成其副本,然后存入vector。vector类还定义了push_back(const T& val)的移动版本。如果用move(),则可以避免这种复制:vec.push_back(move(myElement));
。这之后,myElement处于有效但不确定状态,不应该再使用myElement。
也可以这样:vec.push_back(string(5,'a'));
,string生成一个临时string对象,然后push_back将它直接move过去,避免了复制。
emplace操作
放置到位。
emplace_back()不会复制或移动任何数据,只是分配空间,然后就地构建对象。如:vec.emplace_back(5,'a')
emplace以可变参数模板的形式接收可变数目的参数。
从C++17开始,emplace_back()返回已插入元素的引用。在17之前,它的返回类型是void。
还有个emplace()方法,可以在指定位置就地构建对象,并返回所插入元素的迭代器。
算法复杂度和迭代器失效
在vector中插入或删除元素,会导致后面的所有元素向后移动,或向前移动。因此,它们都才用线性复杂度。此外,因为移动的原因,该点和其后的所有迭代器在操作完后都失效了。迭代器不会自己移动。
vector内部的重分配可能导致引用vector中元素的所有迭代器失效。!
内存分配方案
vector会自动分配内存来保存插如的元素。vector要求元素必须放在连续的内存,由于不可能请求在当前内存块的尾部追加内存,因此vector在申请更多内存时,要在另一个位置分配一块新的更大的内存块,然后将所有元素复制/移动到新的内存块。这很耗时,所以在执行重分配时,会分配比所需内存更多的内存,以尽量避免复制转移过程。
按道理,因为抽象原则,使用者不用考虑vector内部的内存分配方案,但是不然。
1).效率。vector分配方案能保证元素插入采用摊还常量时间复杂度:也就是说大部分操作都采用摊还常量,但是也有线性时间(需要重新分配内存),如果关注运行效率。那么可以控制vector执行内存重分配的时机。
2).迭代器失效。重分配会使引用vector内元素的所有迭代器失效。因此,vector接口允许查询或控制vector的重分配。如果不显式地控制重分配,那么应该假定每次插入都会导致重分配已经所有迭代器失效。
大小和容量
vector提供了两个可获得大小信息的方法:size()和capacity()
前者返回元素的个数,后者则返回重分配之前可以保存的元素个数。因此在重分配之前还能插入的元素个数为capacity()-size()
C++17中引入了非成员的std::size()和std::empty()全局函数。这些与用于获取迭代器的非成员函数类似(begin(),end()等)。非成员函数的size和empyt可以用于所有容器,也可以用于静态分配的C风格数组(不通过指针访问),以及initializer。
vector<int> vec{ 1,2,3 };
cout << size(vec) << endl;
cout << empty(vec) << endl;
预留容器
如果不关心效率和迭代器失效,那就不需要人为控制内存分配。但如果希望尽可能高效,或者确保迭代器不失效,就可以强制预先分配足够的空间,来保存所有元素。
另外一种方法是调用reserve(),负责分配保存指定数目元素的足够空间。
另外一种预分配空间的方法是在构造函数中,或者通过resize()或assign()方法,指定vector要保存的元素数目。会创建指定大小的vector。
直接访问数据
在内存中连续存储数据,可以用data()方法获取指向这块内存的指针。
C++17引入了非成员的data()来获取数据的指针。它可以用于array、vector容器、字符串、静态分配的C风格数组(不通过指针访问)和initializer_lists:
vector<int> vec{1,2,3};
int* data1 = vec.data();
int* data2 = data(vec);