基础备忘:vector 用法

举报
ShaderJoy 发表于 2021/12/30 00:42:55 2021/12/30
【摘要】 注意,在使用前要包含vector对应的头文件: #include<vector> vector是同一种类型的对象的集合,每个对象都有一个对应的整数索引值。和string对象一样,标准库负责管理存储元素的相关内存。我们把vector称为容器,是因为它可以包 含其他对象。一个容器中的所有对象都必须是同一种类型的。我们...

注意,在使用前要包含vector对应的头文件:

#include<vector>
 

vector是同一种类型的对象的集合,每个对象都有一个对应的整数索引值。和string对象一样,标准库负责管理存储元素的相关内存。我们把vector称为容器,是因为它可以包 含其他对象。一个容器中的所有对象都必须是同一种类型的。我们将在第9章更详细地介绍容器。

 

使用vector之前,必须包含相应的头文件。本书给出的例子,都是假设已作了相应的using声明:


  
  1. #include <vector>
  2. using std::vector;

vector 是一个类模板(class template)。模板允许程序员编写单个类或函数定义,这个类和函数定义可用于不同的数据类型上。因此,我们可以定义保存string对象的 vector,或保存int值的vector,又或是保存自定义的类类型对象(如Sales_item对象)的vector。将在第16章介绍如何定义程 序员自己的类模板。幸运的是,使用类模板时只需要简单了解类模板是如何定义的就可以了。声明从类模板产生的某种类型的对象,需要提供附加信息,信息的种类取决于模板。以vector为例,必须说明vector保存何种对象的类型,通过将类型放在类模板名称后面的尖括号中来指定类型:


  
  1. vector<int> ivec; // ivec holds objects of type int
  2. vector<Sales_item> Sales_vec; // holds Sales_items

和其他变量定义一样,定义vector对象要指定类型和一个变量的列表。上面的第一个定义,类型是vector<int>,该类型即是含有若干 int类型对象的vector,变量名为ivec。第二个定义的变量名是Sales_vec,它所保存的元素是Sales_item类型的对象。

 

vector不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。vector类型的每一种都指定了其保存元素的类型。

因此,vector<int>和vector <string>都是数据类型。

 

 

3.3.1  vector对象的定义和初始化

vector类定义了好几种构造函数(2.3.3节),用来定义和初始化vector对象。表3-4列出了这些构造函数:

表3-4 几种初始化vector对象的方式


 

1. 创建确定个数的元素

若要创建非空的vector对象,必须给出初始化元素的值。当把一个vector对象复制到另一个vector对象时,新复制的


  
  1. vector<T> v1;
  2. //vector保存类型为T的对象。默认构造函数v1为空。
  3. vector<T> v2(v1);
  4. //v2是v1的一个副本。
  5. vector<T> v3(n, i);
  6. //v3包含n个值为i的元素。
  7. vector<T> v4(n);
  8. //v4含有值初始化的元素的n个副本。

个元素都初始化为原vector中相应元素的副本。 但这两个vector对象必须保存同一种元素类型 :


  
  1. vector<int> ivec1; // ivec1 holds objects of type int
  2. vector<int> ivec2(ivec1); // ok: copy elements of ivec1 into ivec2
  3. vector<string> svec(ivec1); // error: svec holds strings, not ints

可以用元素个数和元素值对vector对象进行初始化。构造函数用元素个数来决定vector对象保存元素的个数,元素值指定每个元素

的初始值:


  
  1. vector<int> ivec4(10, -1); // 10 elements, each initialized to -1
  2. vector<string> svec(10, "hi!"); // 10 strings, each initialized to "hi!"

关键概念:vector对象动态增长                                                      

vector对象(以及其他标准库容器对象)的重要属性就在于可以在运行时高效地添加元素。因为vector增长的效率高,在元素值已知的情况下,最好是动态地添加元素。

正如第4章将介绍的,这种增长方式不同于C语言中的内置数据类型,也不同于大多数其他编程语言的数据类型。特别地,如果读者习惯了C或Java的风格,由于 vector元素连续存储,可能希望最好是预先分配合适的空间。但事实上,为了达到连续性,C++的做法恰好相反,具体原因将在第9章探讨。虽然可以对给定元素个数的vector对象预先分配内存,但更有效的方法是先初始化一个空vector对象,然后再动态地增加元素(我们随后将学习如何进行这样的操作)。

 

2. 值初始化

如果没有给出元素的初始化式,那么标准库将提供一个值初始化的(value initialized)元素初始化式。这个由库生成的初始值用于初始化容器中的每个元素。而元素初始化式的值取决于存储在vector中元素的数据类型。


  
  1. 如果vector保存内置类型(如int类型)的元素,那么标准库将用0值创建元素初始化值:
  2. vector<string> fvec(10); // 10 elements, each initialized to 0
  3. 如果向量保存类类型(如string)的元素,标准库将用该类型的默认构造函数创建元素初始值:
  4. vector<string> svec(10); // 10 elements, each an empty string

第12章将介绍一些有自定义构造函数但没有默认构造函数的类,在初始化这种类型的Vector对象时,程序员就不能仅提供元素个数,还需要提供元素初始值。

还有第三种可能性:元素类型可能是没有定义任何构造函数的类类型。这种情况下,标准库仍产生一个带初始值的对象,这个对象的每个成员进行了值初始化。

 

 


3.3.2  vector的操作

vector标准库提供许多类似于string对象的操作,表3-5列出了几种最重要的vector操作。

表3-5  vector操作


  
  1. v.empty()
  2. 如果v为空,则返回true,否则返回false
  3. v.size()
  4. 返回v中元素的个数。
  5. v.push_back(t)
  6. 在v的末尾增加一个值为t的元素。
  7. v[n]
  8. 返回v中位置为n的元素。
  9. v1 = v2
  10. 把v1的元素替换为v2中元素的副本。
  11. v1 == v2
  12. 如果v1与v2相等,则返回true
  13. !=, <, <=, >, >=
  14. 保持这些操作符惯有的含义。

1. vector对象的size与capacity

empty和size操作类似于string类型的相关操作(3.2.3节)。成员函数size返回相应vector类定义的size_type的值。

capacity表示该vector对象的容量,即目前最大可存储的元素个数,而size表示的是目前vector存储的元素个数。

使用size_type类型时,必须指出该类型是在哪里定义的。vector类型总是包括vector的元素类型:


  
  1. vector<int>::size_type // ok
  2. vector::size_type // error

2. 向vector添加元素

push_back()操作接受一个元素值,并将它作为一个新的元素添加到vector对象的后面,也就是“插入(push)”到vector对象的“后面(back)”:


  
  1. // read words from the standard input and store them as elements in a vector
  2. string word;
  3. vector<string> text; // empty vector
  4. while (cin >> word) {
  5. text.push_back(word); // append word to text
  6. }

该循环从标准输入读取一系列string对象,逐一追加到vector对象的后面。首先定义一个空的vector对象text。每循环一次就添加一个新元素到vector对象,并将从输入读取的word值赋予该元素。当循环结束时,text就包含了所有读入的元素。

 

3. vector的下标操作

vector中的对象是没有命名的,可以按vector中对象的位置来访问它们。通常使用下标操作符来获取元素。vector的下标操作类似于string类型的下标操作(3.2.3节)。

vector的下标操作符接受一个值,并返回vector中该对应位置的元素。vector元素的位置从0开始。下例使用for循环把vector中的每个元素值都重置为0:


  
  1. // reset the elements in the vector to zero
  2. for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix)
  3. ivec[ix] = 0;

和string类型的下标操作符一样,vector下标操作的结果为左值,因此可以像循环体中所做的那样实现写入。另外,和string对象的下标操作类似,这里用size_type类型作为vector下标的类型。

在上例中,即使ivec为空,for循环也会正确执行。ivec为空则调用size返回0,并且for中的测试比较ix和0。第一次循环时,由于ix本身就是0,则条件测试失败,for循环体一次也不执行。

 

关键概念:安全的泛型编程                                                      

习惯 于C或Java编程的C++程序员可能会觉得难以理解,for循环的判断条件用!=而不是用<来测试vector下标值是否越界。C程序员难以理解 的还有,上例中没有在for循环之前就调用size成员函数并保存其返回的值,而是在for语句头中调用size成员函数。C++程序员习惯于优先选用!=而不是<来编写循环判断条件。在上例中,选用或不用某种操作符并没有特别的取舍理由。学习完本书第二部分的泛型编程后,你将会明白这个习惯的合理性。

调用 size成员函数而不保存它返回的值,在这个例子中同样不是必需的,但这反映了一个良好的编程习惯。在C++中,有些数据结构(如vector)可以动态增长。上例中循环仅需要读取元素,而不需要增加新的元素。但是,循环可以容易地增加新元素,如果确实增加了新元素的话,那么测试已保存的size值作为循 环的结束条件就会有问题,因为没有将新加入的元素计算在内。所以我们倾向于在每次循环中测试size的当前值,而不是在进入循环时,存储size值的副本。我们将在第7章学习到,C++中有些函数可以声明为内联(inline)函数。编译器遇到内联函数时就会直接扩展相应代码,而不是进行实际的函数调用。像size这样的小库函数几乎都定义为内联函数,所以每次循环过程中调用它的运行时代价是比较小的。

 

4. 下标操作不添加元素

初学C++的程序员可能会认为vector的下标操作可以添加元素,其实不然:


  
  1. vector<int> ivec; // empty vector
  2. for (vector<int>::size_type ix = 0; ix != 10; ++ix)
  3. ivec[ix] = ix; // disaster: ivec has no elements

上述程序试图在ivec中插入10个新元素,元素值依次为0到9的整数。但是,这里ivec是空的vector对象,而且下标只能用于获取已

存在的元素。

这个循环的正确写法应该是:


  
  1. for (vector<int>::size_type ix = 0; ix != 10; ++ix)
  2. ivec.push_back(ix); // ok: adds new element with value ix

必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加任何元素。

警告:仅能对确知已存在的元素进行下标操作                                          

对于下标操作符([]操作符)的使用有一点非常重要,就是仅能提取确实已存在的元素,例如:


  
  1. vector<int> ivec; // empty vector
  2. cout << ivec[0]; // Error: ivec has no elements!
  3. vector<int> ivec2(10); // vector with 10 elements
  4. cout << ivec[10]; // Error: ivec has elements 0...9

试图获取不存在的元素必然产生运行时错误。和大多数同类错误一样,不能确保执行过程可以捕捉到这类错误,运行程序的结果是不确定的。由于取不存在的元素的结果是未定义的,因而不同的实现会导致不同的结果,但程序运行时几乎肯定会以某种有趣的方式失败。

本警告适用于任何使用下标操作的时候,如string类型的下标操作,以及将要简要介绍的内置数组的下标操作。

不幸的是,试图对不存在的元素进行下标操作是程序设计过程中经常会犯的严重错误。所谓的“缓冲区溢出”错误就是对不存在的

元素进行下标操作的结果。这样的缺陷往往导致PC机和其他应用中最常见的安全问题。

补充:vector.resize 与 vector.reserve的区别

reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()/insert()函数。

resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。

再者,两个函数的形式是有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。下面是这两个函数使用例子:


  
  1. vector<int> myVec;
  2. myVec.reserve100 );// 新元素还没有构造,
  3. // 此时不能用[]访问元素
  4. for (int i = 0; i < 100; i++ )
  5. {
  6.      myVec.push_back( i ); //新元素这时才构造
  7. }
  8. myVec.resize102 );// 用元素的默认构造函数构造了两个新的元素
  9. myVec[100] = 1;//直接操作新元素
  10. myVec[101] = 2;
初次接触这两个接口也许会混淆,其实接口的命名就是对功能的绝佳描述,resize就是重新分配大小,reserve就是预留一定的空间。这两个接口即存在差别,也有共同点。下面就它们的细节进行分析。
     为实现resize的语义,resize接口做了两个保证:
             一是保证区间[0, new_size)范围内数据有效,如果下标index在此区间内,vector[indext]是合法的。
             二是保证区间[0, new_size)范围以外数据无效,如果下标index在区间外,vector[indext]是非法的。
     reserve只是保证vector的空间大小(capacity)最少达到它的参数所指定的大小n。在区间[0, n)范围内,如果下标是index,vector[index]这种访问有可能是合法的,也有可能是非法的,视具体情况而定。
     resize和reserve接口的共同点是它们都保证了vector的空间大小(capacity)最少达到它的参数所指定的大小。
因两接口的源代码相当精简,以至于可以在这里贴上它们:

  
  1. void resize(size_type new_size) { resize(new_size, T()); }
  2. void resize(size_type new_size, const T& x) {
  3. if (new_size < size())
  4. erase(begin() + new_size, end()); // erase区间范围以外的数据,确保区间以外的数据无效
  5. else
  6. insert(end(), new_size - size(), x); // 填补区间范围内空缺的数据,确保区间内的数据有效


点击打开链接

 

文章来源: panda1234lee.blog.csdn.net,作者:panda1234lee,版权归原作者所有,如需转载,请联系作者。

原文链接:panda1234lee.blog.csdn.net/article/details/8619556

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。