天天看点

《C++编程剖析:问题、方案和设计准则》——第一章泛型编程与C++标准库1.1:vector的使用

本节书摘来自异步社区出版社《c++编程剖析:问题、方案和设计准则》一书中的第1章,第1.1节,作者:【美】herb sutter(赫布 萨特),更多章节内容可以访问云栖社区“异步社区”公众号查看。

c++编程剖析:问题、方案和设计准则

c++最强大的特性之一就是对泛型编程的支持。c++标准库的高度灵活性就是明证,尤其是标准库中的容器、迭代器以及算法部分(最初也称为stl)。

与我的另一本书more exceptional c++ [sutter02]一样,本书的开头几条也是介绍stl中一些我们平常熟悉的部件,如vector和string,另外也介绍了一些不那么常见的设施。例如,在使用最基本的容器vector时如何避免常见的陷阱?如何在c++中进行常见的c风格字符串操纵?我们能够从stl中学到哪些库设计经验(不管是好的、坏的,还是极其糟糕的)?

在考察了stl中的模板设施之后,接着讨论关于c++中的模板和泛型编程的一些更一般性的问题。例如,如何让我们的模板代码避免不必要地(且相当不经意地)损失泛型性。为什么说特化函数模板实际上是个糟糕的主意,而我们又应当怎么替换它?在模板的世界中,我们如何才能正确且可移植地完成像授予友元关系这样看似简单的操作?此外还有围绕着export这个有趣的关键字发生的种种故事。

随着我们逐步深入与c++标准库及泛型编程相关的主题,就会看到关于上述(以及其他)问题的讨论。

难度系数:4

几乎每个人都会使用std::vector,这是个好现象。不过遗憾的是,许多人都误解了它的语义,结果无意间以奇怪和危险的方式使用它。本条款中阐述的哪些问题会出现在你目前的程序中呢?

初级问题

下面的代码中,注释a跟注释b所示的两行代码有何区别?

vector v;

v.reserve(2);

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

v[0] = 1;

v[1] = 2;

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

cout << *i << endl;

}

cout << v[0];

v.reserve(100);

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

v[2] = 3;

v[3] = 4;

// ……

v[99] = 100;

// 示例1-1: [] vs. at

//

void f(vector< int >& v) {

v[0];    // a

v.at(0);   // b

}<code>`</code>

在示例1-1中,如果v非空,a行跟b行就没有任何区别;如果v为空,b行一定会抛出一个std::out_of_range异常,至于a行的行为,标准未加任何说明。

有两种途径可以访问vector内的元素。其一,使用vector::at。该成员函数会进行下标越界检查,确保当前vector中的确包含了需要的元素。试图在一个目前只包含10个元素的vector中访问第100个元素是毫无意义的,这样做会导致抛出一个std::out_of_range异常。

其二,我们也可以使用vector::operator[],c++98标准说vector::operator可以、但不一定要进行下标越界检查。实际上,标准对operator[]是否需要进行下标越界检查只字未提,不过标准同样也没有说它是否应该带有异常规格声明。因此,标准库实现方可以自由选择是否为operator[]加上下标越界检查功能。如果使用operator[]访问一个不在vector中的元素,你可就得自己承担后果了,标准对这种情况下会发生什么事情没有做任何担保(尽管你使用的标准库实现的文档可能做了某些保证)——你的程序可能会立即崩溃,对operator[]的调用也许会引发一个异常,甚至也可能看似无恙,不过会偶尔或神秘地出问题。

既然下标越界检查帮助我们避免了许多常见问题,那为什么标准不要求operator[]实施下标越界检查呢?简短的答案是效率。总是强制下标越界检查会增加所有程序的性能开销(虽然不大),即使有些程序根本不会越界访问。有一句名言反映了c++的这一精神:一般说来,不应该为不使用的东西付出代价(或开销)。所以,标准并不强制operator[]进行越界检查。况且我们还有另一个理由要求operator[]具有高效性:设计vector是用来替代内置数组的,因此其效率应该与内置数组一样,内置数组在下标索引时是不进行越界检查的。如果你需要下标越界检查,可以使用at。

调整vector的大小

现在看示例1-2,该示例对vector进行了简单操作。

考虑如下的代码:

v[1] = 2;<code>`</code>

上面这些代码中的问题都是比较明显的,但可能是比较难于发现的明显错误,因为它们很可能会在你所使用的标准库实现上“勉强”能够“正常运行”。

大小(size,跟resize相对应)跟容量(capacity,与reserve相对应)之间有着很大的区别。

size告诉你容器中目前实际有多少个元素,而对应地,resize则会在容器的尾部添加或删除一些元素,来调整容器当中实际的内容,使容器达到指定大小。这两个函数对list、vector和deque都适用,但对其他容器并不适用。

capacity则告诉你最少添加多少个元素才会导致容器重分配内存,而reserve在必要的时候总是会使容器的内部缓冲区扩充至一个更大的容量,以确保至少能满足你所指出的空间大小。这两个函数仅对vector适用。

本例中我们使用的是v.reserve(2),因此我们知道v.capacity()&gt;=2,这没有问题,但值得注意的是,我们实际上并没有向v当中添加任何元素,因而v仍然是空的!v.reserve(2)只是确保v当中有空间能够放得下两个或更多的元素而已。

准则

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

我们只可以使用operator(或at())去改动那些确实存在于容器中的元素,这就意味着它们是跟容器的大小息息相关的。首先你可能想知道为什么operator[]不能更智能一点,比如当指定地点的元素不存在的时候“聪明地”往那里塞一个元素,但问题是假设我们允许operator以这种方式工作,就可以创建一个有“漏洞”的vector了!例如,考虑如下的代码:

//……这里v[0]至v[98]的值是什么呢

正是因为标准并不强制要求operator进行区间检查,所以在大多数实现上,v[0]都会简单地返回内部缓冲区中用于存放但尚未存放第一个元素的那块空间的引用。因此v[0]=1;这行语句很可能被认为是正确的,因为如果接下来输出v0的话,或许会发现结果确实是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),它们的作用是向容器的尾部追加元素,而使用它们总是安全的。

template

outputiterator copy(const container&amp; c, outputiterator result) {

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

}<code>`</code>`

这里,我们只需简单地包装std::copy(),让它对整个容器进行操作,此外由于我们是以const&amp;来接受容器参数的,因而迭代器自然就是const_iterator了。

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

尽量使用!=而不是&lt;来比较两个迭代器。

养成默认情况下使用前缀形式的--和++的习惯,除非你的确需要用到原来的值。

实施复用:尽量复用已有的算法,特别是标准库算法(例如for_each()),而不是手写循环。

接下来我们遇到下面这行代码:

当程序执行这一行的时候,可能会打印出1。这是因为前面的程序以错误的方式改写了v[0]所引用的那块内存,只不过,这行代码也许并不会导致程序立即崩溃,真遗憾!

`

assert(v.capacity() == 100);`

同样,这里的断言表达式当中应该使用&gt;=,而且和前面一样,这也是多余的。

<code>cout &lt;&lt; v[0];</code>

很奇怪!这次的输出结果可能为0,我们刚刚赋值的1神秘失踪了!

为什么?我们假设reserve(100)确实引发了一次内部缓冲区的重分配(即如果第一次reserve(2)并没有使内部缓冲区扩大到100或更多的话),这时v就只会将它确实拥有的那些元素复制到“新家”当中,而问题是实际上v认为它内部空空如也(因此不复制任何元素)!另一方面,新分配的内部缓冲区最初值可能为0(严格讲不确定),因此就出现了上述情况。

v.at(2) = 3;

v.at(3) = 4;

v.at(99) = 100;<code>`</code>

那么问题就会变得明朗了,因为第一个调用语句就会抛出一个out_of_range异常。

继续阅读