推荐阅读《C++ Template》这本书,关于C++ Template,这本书最好了。对于入门和深入,都是很不错的。
http://www.china-pub.com/computers/common/info.asp?id=15689
记得其中一个作者就是
《C++ Standard Library, The: A Tutorial and Reference》一书的作者,是一个德国人,C++委员会成员。另外一个作者是编写C++编译器多年的专家,更是牛人。
会打印出来。
其实《C++ Standard Library, The: A Tutorial and Reference》应该已经介绍的很详细。
在java里,难道你的代码不会抛出ourofrange的异常?
to hyifeng:你说得很对,确实会打印出来,可是我在看Vector这一章的时候,确实没有看到这样的说明哦,也许是我看得还不够仔细了。:)
至于你说的在Java里,代码会抛出异常,我不是很理解。因为在Java中不支持这样的符值方式:vector[0] = 0;
我的意思是同样逻辑的代码。
Vector v = new Vector();
v.ensureCapacity(50);
v.set(0, new Integer(0));
v.set(1, new Integer(1));
if(v.isEmpty())
{
System.out.println("The vector is empty!");
}
我给你讲两个问题
1。reserve 和 resize(包括vector(size_type n, const T& v = T(), const A& al = A()))的区别:
a. Vector.resize(5)是使得Vector里面包含5个元素,如果原来的元素数目大于5则裁减掉,如果原来的元素数目小于5则使用T()来填充。这时候 Vector.size() == 5;
b. Vector.reserve(5)的作用是使得Vector能容纳下5个元素而无需重新分配内存。而Vector.size()不变。这时候 Vector.capacity() >= 5;
2. operator[] 和 at 的区别
a. operator[]不进行范围检查,由用户自己保证范围正确,好处是速度快;
b. at 和 operator[] 功能相同,但她检查范围,越界则抛出 out_of_range 异常。
现在再来看你的代码,因为使用了reserve,所以不会产生系统级的内存访问异常,而后面使用的又是 operator[],所以不会产生C++异常,但这样做实在不好,因为你使用operator[]就必须由自己保证范围的正确。
再次感谢星哥写下那么详细的回复。对于reserve我也想说两句,如果使用reserve去获得Vector中的capacity的话,是不会改变size的值,正如星哥说的那样。同时呢,如果使用这样的方式vector<int> vector(5);则会改变size,因为这种声明方式会使用int的default值去填充Vector,直至capacity填满。也就是说这个时候的size是5。
靠,什么垃圾代码,
看到这种傻逼代码就来气
vector<int> vector;
^^^^
vector<int> vector;
---------------^^^^
变量名和类型名相同,写这代码的人习惯非常糟糕
感谢jack========兄给我的评论。 我已经将代码改过来了,这确实是一个不好的习惯。 再次感谢您的批评。
几乎每个人都会使用 std::vector ,而实际上这个容器也确实蛮好用的。不过遗憾的是,许多人都不同程度上误解了它的语义,结果无意间就走上了一条充满意外和危险的道路。本条款中展示的这些问题当中有多少是潜伏在你目前所写的程序当中的呢?
1. 下面的代码中,注释 A 所示的那行代码跟注释 B 的那行代码有何区别?
void f(vector<int>& v) {
v[0]; // A
v.at(0); // B
}
2. 考虑如下的代码:
vector<int> v;
v.reserve(2);
assert(v.capacity() == 2);
v[0] = 1;
v[1] = 2;
for(vector<int>::iterator i = v.begin(); i < v.end(); i++) {
cout << *i << endl;
}
cout << v[0];
v.reserve(100);
assert(v.capacity() == 100);
cout << v[0];
v[2] = 3;
v[3] = 4;
// …
v[99] = 100;
for(vector<int>::iterator i = v.begin(); i < v.end(); i++) {
cout << *i << endl;
}
对这段代码作出评价,从代码的风格和正确性方面着眼。
解决方案
访问 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 。
尽量使用 “ != ” 而不是 “ < ” 来比较两个迭代器。
养成缺省情况下使用前缀形式的 “ -- ” 和 “ ++ ” 的习惯,除非你的确需要用到旧值才去使用后缀形式。
实施复用:尽量复用已有的算法,特别是标准库算法(例如 for_each() ),而不是手写循环。
接下来我们遇到下面这行代码:
cout << v[0];
当程序执行这一行的时候,可能会打印出 “ 1 ” 。这是因为前面的程序以错误的方式改写了 v[0] 所引用的那块内存,只不过,这行代码也许并不会导致程序立即崩溃,真遗憾!
v.reserve(100);
assert(v.capacity() == 100);
同样,这里的断言表达式当中应该使用 “ >= ” ,而且实际上根本没有必要写这句断言。
cout << v[0];
这时候发生的情况也许会令你大吃一惊!这句 cout<<v[0]; 也许会打印出 “ 0 ” ,我们刚刚赋给它的值 “ 1 ” 好像神秘地不翼而飞了!
为什么?我们假设 reserve(100) 确实引发了一次内部缓冲区的重分配(即如果第一次 reserve(2) 并没有使内部缓冲区扩大到 100 或更多的话),这时 v 就只会将它确实拥有的那些元素拷贝到 “ 新家 ” 当中,而问题是实际上 v 认为它内部空空如也(因此不进行任何元素拷贝)!另一方面,新分配的内部缓冲区内则是未定义的值(常常是 0 ) ,因此就出现了我们所说的情况。
v[2] = 3;
v[3] = 4;
// …
v[99] = 100;
毫无疑问,现在你看到如上的代码可能心里就开始叹气了。这真是糟糕、糟糕、糟糕 ... 但由于标准并不强制 operator[]() 进行越界检查,所以在大多数实现上这种代码或许会一声不吭貌似良好地运行着,并不会立即导致异常或内存访问陷阱。
如果用户改成这样写的话:
v. at (2) = 3;
v. at (3) = 4;
// …
v. at (99) = 100;
那么问题就会变得明朗了 : “ v.at(2)=3; ” 立即会抛出一个 out_of_range 异常。
for(vector<int>::iterator i = v.begin(); i < v.end(); i++) {
cout << *i << endl;
}
再一次提醒 , 以上代码什么也不会打印出来 , 而且我们应当考虑将它改写成 :
copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n"));
再次注意,这种复用自动地解决了 “ != ” 、前缀 “ ++ ” 、 end() 以及 endl 问题,这样做的话你的程序就永远不会得到在这些方面犯错误的机会。良好的复用通常也会让代码自动变得更快和更安全。
为什么,我的向量里插入的元素只有First和Last,再加新元素会把旧元素覆盖?向量使用前要分配空间吗?向量中是否有直接在指定位置插入元素其后元素自动后移的函数?