vector学习

之前看了一遍容器和迭代器,但听了学长说的,感觉我的理解有点偏差。打算来学习学习容器的具体用法,敲一敲。

先来学习顺序容器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);
posted @ 2020-05-07 19:52  LeoRanbom  阅读(238)  评论(0编辑  收藏  举报