第1条 vector的使用(2)
2. 考虑如下的代码:
- // 示例1-2: vector的一些函数
- //
- vector<int> v;
- v.reserve(2);
- assert(v.capacity() == 2);
这里的断言存在两个问题,一个是实质性的,另一个则是风格上的。
首先,实质性问题是,这里的断言可能会失败。为什么?因为上一行代码中对reserve的调用将保证vector的容量至少为2,然而它也可能大于2。事实上这种可能性是很大的,因为vector的大小必须呈指数速度上升,因而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<
再一次提醒,标准并无任何保证说在你使用的标准库实现上一定会出现上述情形,本例只是展示了一种典型的可能情况。标准并没有要求特定的实现在这类情况下(诸如对一个空的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现在根本就是空的!这可能会让代码的作者感到意外,因为他们还没意识到其实前面的代码根本就没有往vector中添加任何东西。实际上,跟vector中的那些已经预留但尚未正式使用的空间"玩游戏"是很危险的。
话虽如此,这个循环本身并没有任何明显的问题,只不过如果在代码审查阶段看到这段代码的话,我会指出其中存在的一些风格上的问题。大多数意见都是基础性的,如下所示。
(1) 尽量做到const正确性。以上的循环当中,迭代器并没有用来修改vector中的元素,因此应当改用const_iterator。
(2) 尽量使用!=而不是<来比较两个迭代器。确实,由于vector::iterator恰巧是一个随机访问迭代器(当然,并不一定是int*),因此在这种特定情况下将它跟v.end()比较是没有任何问题的。但问题是<只对随机访问迭代器有效,而!=对于任何迭代器都是有效的,因此我们应该将使用!=比较迭代器作为日常惯例,除非某些情况下确实需要<(注意,使用!=还有一个好处,就是便于将来需要时更改容器类型)。例如,std::list的迭代器并不支持<,因为它们只不过是双向迭代器。
(3) 尽量使用前缀形式的--和++。让自己习惯于写++i而不是i++,除非真的需要用到i原来的值。例如,如果既要访问i所指的元素,又要将i向后递增一位,后缀形式v[i++]就比较适用了。