STL vector....详解

from 

http://hi.baidu.com/learnfordba/blog/item/93b5f12249d5104593580714.html

 

vector 抽象容器类型之一(还有list和deque等),与其他几中容器类型不同的是它高效支持随机访

问其中的元素。
使用vector,首先必须调用头文件(#include <vector>)
它的声明和初始化是这样的
vector <类型名> 变量名
vector <int> vi = ( 10 , 1 ) //初始化为10个元素的vector,每个元素都为1

使用迭代器标识:
vector <int> test;
text.begin() text.end()。例如:
for (vector< type > ::iterator it = text.begin(); it != text.end(); ++it)
cout << *it << ' ';

push_back()的用法是将元素插入vector容器的最尾部
举个例子
vector <int> vi;
int a[4] = { 0, 1, 2, 3};
for ( int i = 0; i<4; ++i )
vi.push_back(a[i]);

此时vi就是0 1 2 3
如果改成
vi.push_front( a[i] );
vi就是 3 2 1 0


访问 vector 的元素

1. 下面的代码中,注释 A 所示的那行代码跟注释 B 的那行代码有何区别?
// 示例 1-1: [] vs. at

//

void f(vector<int>& v) {

v[0]; // A

v.at(0); // B

}

在 Example 1-1 中 , 如果 v 是非空的话 , A 行跟 B 行就没有任何区别。如果 v 是空的话 ,

B 行就一定会抛出一个 std::out_of_range 一场 , 至于 A 行在这种情况下会有什么样的行为 ,

标准未加任何说明。

有两种途径可以用于访问容器内的元素。其一,使用 vector<T>::at ,该成员函数会进行下标越界

检查以确保当前 vector 中的确包含了你要的元素。试图访问一个眼下只包含 10 个元素的容器中的

第 100 个元素是毫无意义的,如果你确有此企图的话, vector<T>::at() 就会抛出一个

std::out_of_range 异常。

其二 , 我们也可以使用 vector<T>::operator[] , C++98 标准说 vector<T>::operator 可以

、但不一定要进行下标越界检查。实际上,标准对 operator[] 是否该进行下标越界检查只字未提,

不过标准同样也没有说它是否应该带有异常规格声明。因此,你的标准库实现方可以(也可以不)为

operator[] 加上下标越界检查功能。如果你使用 operator[] 来获取某个 vector 当中的一个根本

不存在的元素的话,后果你可就得自己承担了,标准对这种情况下会发生什么事情没有作出任何担保

(尽管你使用的标准库实现有可能在文档里作出了某些保证):你的程序可能会立即崩溃,对

operator[] 的调用也许会引发一个异常,甚至也可能一切看起来无恙,不过偶尔会神秘地出问题。

既然下标越界检查帮助我们避免了许多常见问题,那为什么标准不要求 operator[] 也实施下标越

界检查呢?简短的答案是:效率。尽管下标越界检查导致的性能开销可能微不足道,但终归还是存在

的,而且只要是使用了它的程序,不管实际上有没有进行越界访问,都同样得付出这笔开销。然而,

C++ 精神之一就是:不必为不使用的东西付出代价(或开销)。所以, C++ 标准并不强制

operator[] 进行越界检查。况且我们还有另一个理由要求 operator[] 具有高效性: vector 的设

计意图是替代内建数组,因此其效率也应当跟内建数组一样高才对,又因为内建数组在下标索引时是

不进行越界检查的,所以 vector 也理应如此。如果你需要下标越界检查能力的话,你可以去使用

at() 。

调整 vector 的大小

现在让我们转到 Example 1-2 , 该示例使用一些简单的操作来操纵 vector<int> 。

2. 考虑如下的代码:
// 示例 1-2: vector 的一些函数

//

vector<int> v;

v.reserve(2);

assert(v.capacity() == 2);

这里的断言 ( assert ) 存在两个问题 , 一个实质性的 , 另一个则是风格上的问题。

先说实质性的问题:这里的断言可能会失败。为什么?因为上一行代码中对 v.reserve(2) 的调用

只能够保证 v 的容量( capacity )至少为 2 ,而实际上它可能大于 2 。事实上这种可能性是很

大的,因为 vector 的大小( size )必须呈指数速度上升,因而 vector 的典型实现可能会选择即

便在通过 reserve() 来申请特定大小的时候也总是按指数边界来增大其内部缓冲区。因此,上面代

码中的断言条件表达式应该使用 >= ,而不是 == ,像这样:

assert(v.capacity() >= 2);

其次是风格上的问题 : 该断言是多余的 ( 即便是改正后的版本 ) 。为什么?因为标准已经保证

了这里你所断言的结论。所以再将它明确地写出来只会带来不必要的混乱。除非你怀疑你正在使用的

标准库的实现有问题,否则这样做毫无意义,然而话说回来,要是问题出在你所使用的标准库身上的

话,你可就遇到大麻烦了。

v[0] = 1;

v[1] = 2;

面这些代码中的问题都还算是比较 “ 单纯 ” 的,只不过它们可能难于发现,因为它们很可能会

在你所使用的标准库实现上 “ 勉强 ” 能够 “ 工作 ” 。


大小 ( size() , 跟 resize() 相对应 ) 跟容量 ( capacity() , 与 reserve() 相对应 )

之间有着很大的区别 :

size() 是告诉你容器当中目前实际有多少个元素,而对应地, resize() 则会调整容器当中实际

的内容,在容器的尾部添加或删除一些元素,以便使容器达到指定大小。这两个函数在 list 、

vector 以及 deque 身上都存在,但在其它容器上并不存在。

capacity() 则告诉你最多添加多少个元素才会导致容器重分配内存,而 reserve() 在必要的时候

总是会导致容器的内部缓冲区扩充至一个更大的容量,以便确保至少能满足你所指出的空间大小。只

有 vector 具有这两个函数。

本例中我们使用的是 v.reserve(2) ,因此我们知道 v.capacity()>=2 ,这没有问题,但值得注

意的是,我们实际上并没有向 v 当中添加任何元素,因而 v 仍然是空的! v.reserve(2) 只是确保

v 当中有空间能够放得下两个或两个以上的元素而已。

指导方针 :

记住 size()/resize() 以及 capacity()/reserve() 之间的区别。


我们只可以使用 operator[]() (或 at() )去改动那些确实存在于容器中的元素,这就意味着它们

是跟容器的大小息息相关的。首先你可能想知道为什么 operator[] 不能更智能一点,比如当指定地

点的元素不存在的时候 “ 聪明地 ” 往那里塞一个元素,但问题是假设我们允许 operator[]() 以

这种方式行事的话,你就可以创建一个有 “ 漏洞 ” 的 vector 了!例如,考虑如下的代码:

vector<int> v;

v.reserve(100);

v[99] = 42; // 错误!但出于讨论的目的,让我们假设这是允许的 ...

// ... 但这么一来, v[0] 至 v[98] 的值又是什么呢?


唉,正是因为标准并不强制要求 operator[]() 进行区间检查,所以在大多数实现上, v[0] 都会简

单地返回内部缓冲区中用于存放但尚未存放第一个元素的那块空间的引用。因此 “ v[0]=1; ” 这

行语句很可能看起来是能够工作的,因为如果你接下来输出 v[0] ( “ cout<<v[0] ” )的话你或

许会发现结果确实是 1 ,跟你(错误的)预期相符合。


再一次提醒,标准并无任何保证说在你使用的标准库实现上一定会出现上述情形,本例只是展示了一

种典型的可能情况。标准并没有要求特定的实现在这类情况下(诸如对一个空的 vector v 写 v[0]

)该采取什么样的措施,因为它假定程序员对这类情况有足够的了解和认知。而且毕竟,如果程序员

想要库来帮助进行下标越界检查的话,他们可以使用 v.at(0) ,不是吗?

当然 , 如果将 v.reserve(2) 改成 v.resize(2) 的话 , “ v[0]=1;v[1]=2; ” 这两行赋值语

句就能够顺利工作了。只不过上文中的代码并没有使用 resize() ,因此代码并不能保证正常工作。

作为一个替代方案,我们可以将这两行语句替换成 v.push_back(1) 和 v.push_back(2) ,它们的作

用是向容器的尾部追加元素,而使用它们总是安全的。


for(vector<int>::iterator i = v.begin(); i < v.end(); i++) {

cout << *i << endl;

}


首先,上面这段代码什么也不会打印出来,因为 vector 现在还根本就是空的呢!这可能会让代码的

作者感到意外,因为他们还没意识到其实前面的代码(如 “v[0]=1;” )根本就没有往 vector 中

添加任何东西。实际上,跟 vector 中的那些已经被预留( reserve )但尚未正式被使用的空间 “

玩游戏 ” 是很危险的。


话虽如此,这个循环本身并没有任何明显的问题,只不过如果我在代码审查阶段看到这段代码的话,

我会指出其中存在的一些风格上的问题。其中大多数都是非关紧要的,如下:

1. 尽量做到 const 正确性 。以上的循环当中 , 迭代器并没有被用来修改 vector 中的元素 ,

因此应当改用 const_iterator 。


2. 尽量使用 “ != ” 而不是 “ < ” 来比较两个迭代器 。确实,由于 vector<int>::iterator

恰巧是一个随机访问( random-access )迭代器(当然,并不一定是 int* ),因此在这种特定情

况下将它跟 v.end() 比较是没有任何问题的。但问题是 “ < ” 只对随机访问迭代器有效,而 “

!= ” 对于任何迭代器都是有效的,因此我们应该将使用 “ != ” 来比较迭代器作为一个日常惯例

,除非某些情况下我们确实需要 “ < ” (注意,使用 “ != ” 还有一个好处就是便于你日后(

如有需要)更改容器类型)。例如, std::list 的迭代器并不支持 “ < ” ,因为它们只不过是双

向( bidirectional )迭代器。


3. 尽量使用前缀形式的 “ -- ” 和 “ ++ ” 。让自己习惯于写 “ ++i ” 而不是 “ i++ ”

,除非你真的需要用到 i 的旧值。例如,如果你想要访问 i 所指的元素,同时又要将 i 向后递增

一个顺位的话,后缀形式就比较适用了: v[i++] 。


4. 避免无谓的重复求值 。本例中 v.end() 所返回的值在整个循环的过程中是不会改变的,因此应

当避免在每次判断循环条件时都调用一次 v.end() ,或许我们应当在循环之前预先将 v.end() 求出

来。


注意:如果你的标准库实现中的 vector<int>::iterator 就是 int* ,而且能够将 end() 进行内联

及合理优化的话,原先的代码也许并无任何额外开销,因为编译器或许能够看出 end() 返回的值一

直是不变的,从而将对它的求值安全地提取到循环外部。这是一种相当常见的情况。然而,如果你的

标准库实现的 vector<int>::iterator 并非 int* (例如,在大多数调试版实现当中,其类型都是

类类型的),或者 end() 之类的函数并没有内联,或者编译器并不能进行相应的优化的话,那就只

有靠你自己手动将这部分代码提取出来才能获得一定程度的性能提升了。


5. 尽量使用 '\n' 而不是 endl 。使用 endl 会迫使输出流刷新其内部缓冲区。如果该流的确是带

内部缓冲区,而且你又确实不需要每次都刷新它的话,你可以在整个循环结束之后写一行刷新语句,

这样你的程序就会执行得快得多。

最后一个意见稍为重要一些:

6. 尽量使用标准库中的 copy() 和 for_each() ,而不是自己手写循环,因为利用标准库的设施,

你的代码可以变得更为干净简洁 。这里,风格跟美学判断起作用了。在简单的情况下, copy() 和

for_each() 可以而且确实比手写循环的可读性要强。不过,也只有像本例这样的简单情形才会如此

,如果情况稍微复杂一些的话,除非你有一个很好的表达式模板( expression template )库,否

则使用 for_each() 来写循环反而会让代码的可读性降低,因为原先位于循环体中的代码必须被提取

到一个仿函数当中才能使用 for_each() 。有时候这种提取是件好事,但有时它只会导致混淆晦涩的

代码。

我之所以说你们的口味可能各有不同,就是因为这个原因。另外,在本例中我倾向于将原先的手写循

环替换成如下的形式:

copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n"));


此外,如果你像这样来使用 copy() 的话,首先有一个好处就是原先关于 “ != ” 、 “ ++ ” 、

end() 以及 endl 的问题就不用操心了,因为 copy() 已经帮你做了这些事情。(当然,我还是假定

你并不希望在每输出一个 int 的时候都去刷新输出流,否则你只有自己手写循环了)复用如果运用

得当的话不但能够改善代码的可读性,而且还可以帮你避免一些潜在的问题发生的机会,从而让代码

变得更牢固。


你可以更进一步,编写一个基于容器的算法来进行拷贝,也就是说,一个施加在整个容器(而不仅仅

是一个迭代器区间)之上的算法。这种做法同样也可以自动纠正 const_iterator 问题。例如:


template<class Container, class OutputIterator>

OutputIterator copy(const Container& c, OutputIterator result) {

return std::copy(c.begin(), c.end(), result);

}


这里 , 我们只需对 std::copy() 作一层简单的包装 , 让它对整个容器进行操作 , 此外由于我

们是以 const 引用来接受容器参数的 , 因而它的迭代器自然就是 const_iterator 了。


指导方针 :

做到 const 正确性。特别是当你并不对容器内的元素作任何改动的时候 , 记得使用

const_iterator 。

尽量使用 “ != ” 而不是 “ < ” 来比较两个迭代器。

养成缺省情况下使用前缀形式的 “ -- ” 和 “ ++ ” 的习惯,除非你的确需要用到旧值才去使

用后缀形式。

 

posted on 2012-08-17 14:09  Orz..  阅读(222)  评论(0)    收藏  举报

导航