C++教程:std::vector入门指南(一)

2,105 阅读15分钟

C++教程:std::vector入门指南(一)

本篇是译文,原文地址

文中一些术语:

  • ctor constructor的缩写,指类的构造器

本篇教程是为了帮助初、中级C++程序员更好地理解C++语言中的标准模板类。

使用Vectors

C++标准的最后一次投票是在1997年11月14日,已经过去很久了。但是,标准的一些重要部分,尤其是标准库( Standard Library)仍然没有在C++的使用者之间流行。经常阅读CodeGuru的C++论坛的人可能会注意到,许多问题和答案仍然在使用手工编码的方式解决,然而这些问题使用标准库可以很优雅地解决。

一个经常出现的问题是使用C风格的数组,它们充斥着问题和缺点。人们似乎很害怕标准vector和它的兄弟deque。其中一个原因可能是,标准库的文档大多比较省略且难懂。

在这篇文章中,我将讨论C++中的vector,并试图以一种更容易理解和掌握的方式来解释它们。这篇文章并不是完美的,它的目的是让你在使用vector时有一个开始,并帮助你避免最常见的陷阱。我们将从小处着手,不会试图非常学术地处理这个话题。

vector简介

Vector是一个模板类,它完美地替代了老式的C语言数组。它允许使用与普通数组相同的原生语法,此外还提供了一系列服务,将C++程序员从处理分配的内存的工作中解放出来,并帮助对包含的元素进行一致的操作。

使用vector的第一步是引入对应的头文件

#include <vector>

注意头文件的名称没有任何扩展名,所有标准库的头文件都是如此。此外,所有的标准库都包含在std的命名空间中。你必须在名字前加上*std::*来使用它们。

std::vector<int> v;    // 声明一个integer类型的vector

对于小型项目,你可以通过在.cpp文件顶部插入一个using指令,将整个命名空间std纳入范围。

#include <vector>
using namespace std;
//...
vector<int> v;         // 不再需要std::前缀

对小项目来说这样是可以的,只要你把using指令写在你的.cpp文件中。但是千万不要把using指令写在头文件中!这样写每个包含该头文件的cpp文件都会应用这条指令。对于较大的项目来说,最好对每一个名字进行相应的明确限定(即添加前缀)。我并不喜欢这种省略的写法(使用using)。在这篇文章中,我将对每个名字进行相应的限定。在适当的地方,我将在例子中引入一些typedef,以提高可读性。

什么是std::vector

什么是std::vector v;?它是一个模板类,将包裹一个Ts的数组。在这个广泛使用的符号中,'T'代表任何数据类型,以及内置的或用户定义的类。这个vector将把Ts存储在一个连续的内存区域,并让你通过v[0]、*v[1]*等方式来访问各个Ts,就像C风格的数组一样。

请注意,对于较大的项目来说,重复写vector的显式类型可能会很乏味。如果你愿意,你可以使用typedef

typedef std::vector<int> int_vec_t;    // 类型名自定义
//...
int_vec_t v;

注意:不要使用宏!

#define int_vec_t std::vector<int> ;    // ❌

首先,让我们看看vector能为我们做什么。让我们从小处着手,以一个整数数组为例。如果你使用普通数组,你可以准备一个静态数组或动态数组。

size_t size = 10;
int sarray[10];
int *darray = new int[size];
// do something with them:
for(int i=0; i<10; ++i){
    sarray[i] = i;
    darray[i] = i;
}
//当你完成相应的工作时记得把darray删除掉
delete [] darray;

使用 vector完成同样的工作

#include <vector>
//...
size_t size = 10;
std::vector<int> array(size);    //申请一个包含10个interger类型的内存区域并初始化为0 
                                
// do something with them:
for(int i=0; i<size; ++i){
    array[i] = i;
}
// 用完不再需要删除array

正如你看到的,vector 结合了静态数组和动态数据的优点,像动态数组一样它可以提供一个动态的size参数,像静态数组一样可以自动删除已使用的内存。

标准的vector定义了 操作符*[],以保持和原生数组一直的语法。出于性能的考虑,[]*并不检查索引是否有效。与C风格的数组类似,使用一个无效的索引大多数情况下会造成访问错误。

除了*[]之外,vector还定义了成员函数at()。这个函数与[]做同样的事情,但是会进行检查索引。如果索引是无效的,它将抛出一个std::out_of_range*错误类的实例。

std::vector<int&gt array;
try{
    array.at(1000) = 0;
}
catch(std::out_of_range o){
    std::cout<<o.what()<<std::endl;
}

根据你使用的C++标准库的实现,上述片段将打印出或多或少的明确错误信息。STLPort打印的是 "cector "一词,Visual C++附带的Dinkumware实现打印的是 "无效的vector下标"。其他实现可能会打印其他东西。

请注意,vector是一个标准的容器。列表数据也可以使用迭代器来访问。在本文后面会有更多关于迭代器的内容。现在,keep it simple!

如果你不知道你将有多少个元素该怎么办(下面代码所示)?如果使用C风格的数组来存储元素,你要么需要实现一个逻辑,允许开发人员不时地增长你的数组;要么你将分配一个 "足够大 "的数组。后者是一种比较戳的方法,而前者会让你头疼。而vector则不然。

#include <vector>
#include <iostream>
//...
std::vector<char> array;
char c = 0;
while(c != 'x'){
  std::cin>>c;
  array.push_back(c);
}

在前面的例子中,push_back()一次向数组中添加一个元素。这就是我们想要的,但是它有一个小陷阱。为了理解这一点,你必须知道,一个vector有一个所谓的 "controlled sequence(受控序列)",以及为该序列分配的一块存储空间。受控序列只是vector中的数组的另一个名称。为了容纳这个数组,vector会分配一些内存,一般会比需要的多一些。你可以push_back()元素,直到分配的内存用完。然后,vector会触发一次重新分配,并且会增加分配的内存块。这可能意味着它将不得不把受控的序列移动(即:复制)到一个更大的块中。而复制大量的元素会大大降低你的应用程序的速度。请注意,重新分配对你来说是绝对透明的(除非是移动失败--内存越界)。一般情况下,你什么都不需要做,vector会做所有的事情。当然,你可以做一些事情来避免vector频繁地重新分配存储。请继续阅读。

在前面的例子中,我们使用其默认的构造函数声明了vector。这就创建了一个空的vector。根据所使用的标准库的实现,空的vector可能会也可能不会分配一些内存 "以备不时之需"。如果我们想避免过于频繁地重新分配vector的存储空间,我们可以使用其*reserve()*成员函数。

#include <vector>
#include <iostream>
//...
std::vector<char> array;
array.reserve(10);    // 保留10个元素所需要的内存空间
char c = 0;
while(c != 'x'){
  std::cin>>c;
  array.push_back(c);
}

当然,我们传递给reserve()的参数取决于上下文。在这种情况下,函数reserve()将确保我们至少有10个元素的空间。如果vector已经有了容纳所需数量元素的空间,*reserve()*就不会做任何事情。换句话说,如果有必要的话,reserve()会增加分配给vector的存储空间,但绝不会缩小它。

顺便提一下,下面两个代码片段不是一回事。

// snip 1:
std::vector<int> v(10);
// snip 2:
std::vector<int> v;
v.reserve(10);

第一个片段定义了一个包含10个整数的vector,并以其默认值(0)初始化它们。如果我们没有整数,而是一些用户定义的类,vector将调用默认的ctor(构造器) 10次,并包含10个容易构建的对象。第二个片段定义了一个空的vector,然后告诉它要为10个整数腾出空间。这个vector将分配足够的内存来容纳至少10个整数,但不会初始化这块内存。如果我们没有整数,但有一些用户自定义的类,第二个片段就不会构造该类的任何实例。

要想知道一个vector的当前分配的存储空间可以容纳多少个元素,请使用capacity()成员函数。要想知道vector目前包含多少个元素,请使用*size()*成员函数。

#include <vector>
#include <iostream>
//...
std::vector<int> array;
int i = 999;          // some integer value
array.reserve(10);    // make room for 10 elements
array.push_back(i);
std::cout<<array.capacity()<<std::endl;
std::cout<<array.size()<<std::endl;

打印如下:

10
1

这意味着,在不触发重新分配的情况下,可以添加到vector的元素数量始终是capacity()-size()

注意,对于前面的例子,只有0是array的有效索引。这是因为,虽然我们已经用reserve()为至少10个元素留出了空间,但是内存并没有被初始化。因为int是一个内置类型,用*[]写入所有10个元素实际上是可行的,但是我们会有一个处于不一致状态的vector*,因为size()还是会返回1。此外,如果我们试图用array.at()访问除第一个元素以外的其他元素,就会抛出错误**std::out_of_range**。乍一看,这似乎很不方便,但仔细想想就会发现为什么会这样。如果vector包含的是用户自定义类的对象,*reserve()*就不会调用任何ctor。访问一个尚未构造的对象会产生未定义的结果,这在任何情况下都是不得行的。最后,需要记住的是,*reserve()的作用是尽量减少潜在的重新分配内存的数量,它不会影响受控序列中的元素数量。调用参数小于当前capacity()reserve()*是有益的,它只是什么都不做。

扩大所含元素数量的正确方法是调用vector的成员函数resize()。成员函数*resize()*有以下属性。

  • 如果新的大小大于vector的原有大小,它将保留已有元素;其余元素根据第二个参数进行初始化。如果新的大小小于原有大小,会根据新的大小保留元素,其余的被丢弃。
  • 如果新的大小大于capacity(),它将重新分配存储空间以便容纳所有的元素。注意:resize()不会缩小capacity()

例子:

std::vector<int> array;   // 创建一个空的vector
array.reserve(3);         // 腾出3个元素的空间
                          // 在这里, capacity() 是 3
                          // size() 是0
array.push_back(999);     // 插入元素
array.resize(5);          // 调整元素大小
                          // 这时候, vector包含的元素是:999, 0, 0, 0, 0
                          
array.push_back(333);     // 在vector中插入元素333
                          // vector中的元素包括:999, 0, 0, 0, 0, 333
                          
array.reserve(1);         //什么都不会做,因为 capacity() > 1
array.resize(3);          //这时候,vector中的元素时: 999, 0, 0
                         
                          // capacity()仍然是 6
                          // size()是 3
array.resize(6, 1);       // 再次调整大小,用1填满剩余空间
                          // 这时候,cector中的元素为: 999, 0, 0, 1, 1, 1
                        

另一种扩大控制元素数量的方法是使用push_back()。在某些情况下,这可能比调用resize()然后再写元素更有效。让我们仔细看看vector的内部结构,看看下面的例子。

class X
{
public:
    X():val_(0){}
    X(int val):val_(val){}
    int get(){return val_;}
    void set(int val){val_=val;}
private:
    int val_;
};
//....
std::vector<X> ax;         // 创建一个空的vector,用来容纳X类型的对象 

// version 1:
ax.resize(10);             // resize
for(int i=0; i<10; ++i){
    ax[i].set(i);          // 赋值
}
//...
// version 2:
ax.reserve(10);            // 保留10个元素的空间
for(int i=0; i<10; ++i){
    ax.push_back(X(i));    //使用X的构造函数插入元素
}

这两个版本是等价的,它们会产生相同的结果。在这两种情况下,我们都从一个空的vector开始。在第一个版本中,我们使用resize()将受控序列的大小增加到10个元素。当resize()完成后,我们的vector中会有10个有效的X类型的对象,所有这些对象的val_ == 0,因为这就是X的默认ctor的作用。然后,我们选择序列中的每一个X,并使用X::set()来改变其val_

在第二个版本中,我们调用*reserve()来为10个元素留出空间。vector将重新分配其存储空间,除此之外不做任何事情,也没有构建任何元素。在第二步中,我们使用第X的ctor创建10个X类型的对象,从而直接赋值,并将它们push_back()*到vector中。

哪种方法更高效?这可能也取决于标准库的实现,但第二个版本可能性能更好,因为它没有为每个元素调用X::set()

到目前为止我们已经看到了如何声明一个vector,以及如何填充它,让我们看看如何对它进行操作。我们将从与C风格数组的类比开始,寻求最佳实践。

有两种访问C风格数组的方法:要么使用下标操作符,要么使用指针。此外,将一个C风格的数组传递给一个函数意味着传递一个指向第一个元素的指针。我们可以用vector做同样的事情吗?答案是肯定的。举个例子。

#include <iostream>

double mean(double *array, size_t n)
{
    double m=0;
    for(size_t i=0; i<n; ++i){
        m += array[i];
    }
    return m/n;
}

int main()
{
    double a[] = {1, 2, 3, 4, 5};
    std::cout<<mean(a, 5)<<std::endl;    // 输出 3
    return 0;
}

当调用mean(a, 5)时,第一个参数实际上是数组中第一个元素的地址*&a[0]。我们知道,一个vector需要将其元素按顺序放在一个连续的内存块中。这意味着我们只需将vector的第一个元素的地址传递给函数mean()*,它就会工作。

int main()
{
    std::vector<double> a;
    a.push_back(1);
    a.push_back(2);
    a.push_back(3);
    a.push_back(4);
    a.push_back(5);
    std::cout<<mean(&a[0], 5)<<std::endl;    // 输出 3
    return 0;
}

这很nice,但还是有些不同。虽然我们能够直接初始化C风格的数组,但是我们必须push_back()元素到vector中。我们可以做得更好吗?嗯,可以。我们不直接为vector使用初始化列表,而是使用一个中间数组。

double p[] = {1, 2, 3, 4, 5};
std::vector<double> a(p, p+5);

这里我们使用vector提供的另一个构造函数。它需要两个参数:一个指向C型数组第一个元素的指针和一个指向该数组最后一个元素之后的指针。它将用数组中每个元素的副本来初始化vector。有两件事需要注意:第一,数组被复制了,原数组与我们创建的vector毫无瓜葛,第二,我们提供的范围是从数组中的第一个元素到最后一个元素。

在使用vector或任何其他标准容器时,理解第二点是至关重要的。受控序列总是用**[first,one-past-last]**来表示--不仅仅ctors是这样,其他操作元素范围的函数也是如此。

在获取vector中包含的元素的地址时,必须注意一些问题:vector的内部重新分配将使你持有的指向其元素的指针失效。

std::vector<int> v(5);
int *pi = &v[3];
v.push_back(999); // <-- 可能会触发重新分配内存
*pi = 333;        // <-- 可能造成错误,指针pi不再有效 

在前面的例子中,我们取了vector的第四个元素的地址,并将其存储在pi中。然后我们push_back()另一个元素到vector中。然后我们尝试使用pi。 如果这个存储不足,无法容纳额外的元素,push_back()可能会触发内存的重新分配,。然后pi将指向一个刚刚被删除的内存地址,使用它的结果是无法定义的。坏消息是,vector可能会也可能不会重新分配内存--一般情况下无法判断。解决的办法是不使用可能已经失效的指针,或者确保vector不会重新分配。后者意味着明智地使用reserve(),以便让vector在规定的时间处理内存(重新)分配。

到目前为止,我们见到的成员函数中,只有push_back()resize()可以使指向vector的指针失效。此外,还有其他可以使指针失效的成员函数,我们将在本教程的后面讨论它们。

请注意,下标操作符和成员函数at()都不会使指向vector的指针失效。

说到vector的指针,我们可以在这时引入一个标准的概念:迭代器。迭代器是标准库为所有容器--vector、list、set、deque等--建立通用接口的方式。迭代器出现的原因是对一个容器来说 "原生 "的操作(比如vector的下标操作)对其他容器来说没有意义。标准库需要一种通用的方法来将迭代、查找、排序等算法应用于所有容器。这种通用的方式就是迭代器。

迭代器是对所含元素的一个句柄。如果你想的话,你可以在你最喜欢的教科书中找到一个准确的定义。迭代器的内部表示是不重要的,重要的是,如果你有一个迭代器,你可以解除引用以获得它 "指向 "的元素(对于vector,迭代器最自然的实现确实是一个普通的指针,但不要指望这个)。让我们通过一个小例子来了解一下迭代器。

#include <vector>
#include <iostream>

int main()
{
    std::vector<double> a;
    std::vector<double>::const_iterator i;
    a.push_back(1);
    a.push_back(2);
    a.push_back(3);
    a.push_back(4);
    a.push_back(5);
    for(i=a.begin(); i!=a.end(); ++i){
        std::cout<<(*i)<<std::endl;
    }
    return 0;
}

让我们一步一步地看这个程序:

std::vector<double>::const_iterator i;

This declares a const iterator i for a vector. We are using a const iterator because we do not intend to modify the contents of the vector.

这里声明了一个double类型的const_iterator ,命名为i。使用常量是因为不会对它进行更改。

...i=a.begin();...

成员函数begin()返回一个迭代器,指向序列中的第一个元素。

...i!=a.end();...

成员函数end()返回一个迭代器,指向序列中的最后一个元素。注意,取消对*end()*返回的迭代器的引用是非法的,会产生未定义的结果。

...++i

你可以通过递增迭代器挨个访问元素。

请注意,同样的程序,使用指针而不是迭代器,代码结构和上面类似。

#include <vector>
#include <iostream>

int main()
{
    std::vector<double> a;
    const double *p;
    a.push_back(1);
    a.push_back(2);
    a.push_back(3);
    a.push_back(4);
    a.push_back(5);
    for(p=&a[0]; p!=&a[0]+5; ++p){
        std::cout<<(*p)<<std::endl;
    }
    return 0;
}

那么,如果我们可以用指针以同样的方式来实现同样的事情,为什么还要用迭代器呢?答案是,如果我们想在vector上应用一些标准的算法,比如排序,我们就必须使用迭代器。标准库并没有把算法作为各种容器的成员函数来实现,而是作为可以在许多容器上操作的自由模板函数。

一般来说,标准容器(特别是vector)和标准算法的结合是一个非常强大的工具;不幸的是,它经常被程序员所忽视。通过使用它,你可以避免大量手工制作的、容易出错的代码,它使你能够写出紧凑、可移植和可维护的程序。

让我们来看看vector提供的成员函数。

构造函数

C++标准中提供了一套完整的**C++构造器C++析构器**和拷贝操作符。让我们以一个标准字符串的vector为例来看看它们。

typedef std::vector<std::string> str_vec_t;
str_vec_t v1;                       // 创建一个空的 vector
str_vec_t v2(10);                   // 10个空字符串的副本
str_vec_t v3(10, "hello");          // 10个字符串"hello"的副本
                                   
str_vec_t v4(v3);                   // 拷贝v3

    std::list<std::string> sl;      // 创建一个字符串的列表并填充 
                                  
    sl.push_back("cat");
    sl.push_back("dog");
    sl.push_back("mouse");

str_vec_t v5(sl.begin(), sl.end()); // 从列表 s1中复制元素到v5中 

v1 = v5;                            //把所有的元素从v5复制到v1中
                                  

assign()

*assign()*函数将重新初始化该vector。我们可以使用[first, last]迭代器传递一个有效的元素范围,或者我们可以指定要创建的元素数量和元素值。

v1.assign(sl.begin(), sl.end());    //从list中复制值到vector中  

v1.assign(3, "hello");              // 使用3"hello"元素初始化

assign()完全改变了vector的元素。旧的元素(如果有的话)被丢弃,vector的大小被设置为分配的元素数。当然,*assign()*可能会触发内存的重新分配。

栈操作

我们已经看到了*push_back()这个函数。它将一个元素添加到受控序列的末端。有一个对应的函数,pop_back(),可以删除受控序列中的最后一个元素。被删除的元素变得无效,并且size()*被递减。请注意,*pop_back()*并不返回被移除元素的值。你必须在弹出它之前检查它。这样做的原因是为了保证安全。在一个空的vector上弹出元素会产生未定义的结果。

std::vector<int> v;
v.push_back(999);
v.pop_back();

注意,*pop_back()不会影响capacity()*的输出。

预定义的迭代器

我们已经看到了迭代器begin()end()。它们分别指向受控序列中的第一个和最后一个元素。此外还有rbegin()rend(),它们分别指向反向序列的第一个和最后一个元素。请注意,rbegin()rend()都返回类型reverse_iterator(或const_reverse_iterator的const版本)--这与iterator(分别是const_iterator)不同。要从一个反向迭代器中获得一个 "正常 "的迭代器,需要使用reverse_iterator的*base()*成员函数。

std::vector<int> v;
v.push_back(999);
std::vector<int>::reverse_iterator r = v.rbegin();
std::vector<int>::iterator i = r.base(); //指向列表中的最后一个元素

访问元素

我们已经看到了下标运算符[],它提供了未经检查的访问,以及成员函数at(),如果传递的索引无效,它将抛出一个std::out_of_range类型的对象。此外还有两个成员函数,front()back(),它们分别返回受控序列中第一个和最后一个元素的引用。需要注意的是它们不返回迭代器。

std::vector<int> v;
v.push_back(999);
// fill up the vector
//...
// 下面表达式等价:
int i = v.front();
int i = v[0];
int i = v.at(0);
int i = *(v.begin());
// 下面表达式等价:
int j = v.back();
int j = v[v.size()-1];
int j = v.at(v.size()-1);
int j = *(v.end()-1);

注意,我们不能写入**(-v.end()),因为v.end()*不是一个左值。

列表操作

vector提供了一些列表原生操作,这些操作大多数容器基本上都会实现,它们作用是在受控序列中插入或者删除元素。让我们通过一些例子看一下。

#include <vector>
#include <iostream>
int main()
{
    std::vector<int> q;
    q.push_back(10); q.push_back(11); q.push_back(12);

    std::vector<int> v;
    for(int i=0; i<5; ++i){
        v.push_back(i);
    }
    // v 包含元素: 0 1 2 3 4

    std::vector<int>::iterator it = v.begin() + 1;
    // 在第二个元素之前插入33 
    it = v.insert(it, 33);
    // v 包含元素: 0 33 1 2 3 4
    // it指向被插入的元素

    //在第二个元素之前插入q中所有元素 
    v.insert(it, q.begin(), q.end());
    // v 包含元素: 0 10 11 12 33 1 2 3 4
    // iterator 'it' 失效

    it = v.begin() + 3;
    // it 指向v中第四个元素
    // 在第四个元素之前插入三个-1
    v.insert(it, 3, -1);
    // v 中元素: 0 10 11 -1 -1 -1 12 33 1 2 3 4
    // iterator 'it' 失效

    // 删除v中第5个元素
    it = v.begin() + 4;
    v.erase(it);
    // v contains 0 10 11 -1 -1 12 33 1 2 3 4
    // iterator 'it' 失效

    // 删除v中第2个到第5个元素元素
    it = v.begin() + 1;
    v.erase(it, it + 4);
    // v contains 0 12 33 1 2 3 4
    // iterator 'it' is invalid

    //清空v
    v.clear();

    return 0;
}

注意,insert()erase()都可能使你持有的迭代器失效。第一个版本的insert()返回一个指向被插入元素的迭代器。其他两个版本则返回void。插入元素可能会触发一个重新分配。在这种情况下,容器中的所有迭代器都会失效。如果没有发生重新分配(例如,在插入之前调用reserve()),只有在插入点和序列末端之间的迭代器会失效。

删除元素不会触发重新分配,也不会影响capacity()。但是,所有指向被删除的第一个元素和序列结束之间的迭代器都会失效。

调用*clear()*将所有元素从受控序列中移除。但是,分配的内存并没有被释放。当然,所有的迭代器都会失效。

注意,insert()erase()对于vector来说都不是高效的。它们的时间复杂度是O(n)+。如果你的应用程序经常使用插入和删除,vector可能不是你的最佳容器选择。

比较操作

你可以使用运算符*==, !=<比较两个vector的内容(会在内部进行逐个元素比较)。如果两个vector具有相同的size(),并且对应位置的元素相等,那么它们相等。注意,两个相等的vector的capacity()不需要相同。此外,使用运算符<*比较 vector时,排序方式按字母顺序进行的。

std::vector<int> v1, v2;
//...
if(v1 == v2) ...

交换Vectors中的内容

有时候,能够交换两个vector的内容是很实用的。一个常见的应用是释放vector所持有的内存。我们知道,删除元素或清除vector并不影响其分配的内存。为了释放占用的内存,我们需要一个小技巧。

std::vector<int> v;
//...
v.clear();
v.swap(std::vector<int>(v));

通常情况下,vector只是简单地交换其内存。在前面的例子中,我们通过使用copy ctor来创建一个临时的vector,并将其内容与v进行swap()。临时对象将接收v所持有的全部内存,而v将接收临时对象所持有的内存--可能没有。最终的结果是:临时创建的vector在上述语句的最后被销毁,v持有的内存被释放。

vector类模板有第二个默认的模板参数。

template<class T, class A = allocator<T> >
        class vector ...

allocator是一个类,它提供了容器用来为其元素分配和删除内存的函数。在本教程中,我们做了一个贯彻始终的假设:即假设我们有一个默认的allocator。最后补充一个需要注意的点,如果两个allocator是相同的,swap()将在恒定时间内执行(即简单地交换两个vector的内部结构)。大多数情况下是这样的。

在本篇教程中,我们接触了标准库的表层,认识了std::vector。下篇教程我们将讨论与vector有关的更多高级话题,分别对其应用标准算法,并讨论一些设计决策,例如 何时存储对象与何时在vector中存储对象指针的问题。此外,我们还将介绍vector的一个兄弟,即std::deque