第1条 vector的使用(3)
(4) 避免无谓的重复求值。本例中v.end()所返回的值在整个循环的过程中是不会改变的,因此应当避免在每次判断循环条件时都调用一次v.end(),或许我们应当在循环之前预先将v.end()求出来。
注意,如果你的标准库实现中的vector::iterator就是int*,而且能够将end()进行内联及合理优化的话,原先的代码也许并无任何额外开销,因为编译器或许能够看出end()返回的值一直是不变的,从而安全地将求值提到循环外部。这是一种相当常见的情况。然而,如果你的标准库实现的vector::iterator并非int*(例如,在大多数调试版实现当中,其类型都是类类型的),或者end()之类的函数并没有内联,或者编译器并不能进行相应的优化,那么只有手动将这部分代码提出才能获得一定程度的性能提升。
(5) 尽量使用\n而不是endl。使用endl会迫使输出流刷新其内部缓冲区。如果该流的确有内部缓冲区,而且又确实不需要每次都刷新它的话,可以在整个循环结束之后写一行刷新语句,这样程序会执行得快很多。
最后一个意见稍微高级一些。
(6) 尽量使用标准库中的copy()和for_each(),而不是自己手写循环,因为利用标准库的设施,你的代码可以变得更为干净简洁。这里,风格跟美学判断起作用了。在简单的情况下,copy()和for_each()可以而且确实比手写循环的可读性要强。不过,也只有像本例这样的简单情形才会如此,如果情况稍微复杂一些的话,除非你有一个很好的表达式模板库,否则使用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];
很奇怪!这次的输出结果可能为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;
那么问题就会变得明朗了,因为第一个调用语句就会抛出一个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问题,因此程序永远不会在这些方面犯错误。良好的复用通常也会让代码自动变得更快和更安全。
小结
了解size()和capacity()之间的区别,了解operator[]()跟at()之间的区别。如果需要越界检查,请使用at()而不是operator[]()。这么做可以帮助我们节省大量的调试时间。