无效的指针、摘引和迭代器

无效的指针、引用和迭代器

首先以示例代码为例:

vector<int> v;

//添加一些元素
fir(int i=0; i<10; ++i)
	v.push_back(i);

int* my_favorite_element_ptr = &v[3];
cout<<"My favorite element = "<<(*my_favorite_element_ptr)<<endl;
cout<<"Its address = "<<my_favorite_element_ptr<<endl;

cout<<"Adding more elements.."<<endl;

//添加更多元素
for(int i=0; i<100; ++i)
	v.push_back(i*10);

cout<<"My favorite element = "<<(*my_favorite_element_ptr)<<endl;
cout<<"Its address = "<<&v[3]<<endl;
对于以上的代码,会发生什么样的情况?我们创建了一个包含10个元素的vector,并出于某种原因决定保存一个指向索引位置为3的元素的指针。接着,我们向这个vector添加了另一些元素,并试图复用前面所保存的指针。这段代码会有什么错误吗?

对于以上的代码,它的输出如下:

My favorite element = 3
Its address  = 0x1001000cc

Adding more elements...

My favorite element = 3
Its address  = 0x10010028c

注意,当我们向这个vector又添加一些元素之后,元素&v[3]的地址发生了变化。问题主要在于当我们向这个vector添加一些新元素时,现有的元素可能会移动到完全不同的位置。

当我们创建一个vector时,它默认分配一定数量的元素(通常是16)。接着,当我们试图添加超出容量的元素时,这个vector就会分配一个新的、更大的数组,把原先的元素从旧位置复制到新位置,然后继续添加新元素,直到新的容量也被用完。旧的内存被销毁,可以用于其他用途。

同时,我们的指针仍然指向旧的位置,现在已经是被销毁的内存。因此,如果继续使用这个指针会发生什么情况?如果没有人复用这块内存,我们就比较“走运”,不会注意到发生了什么。但是,即使是在这种最好的情况下,如果我们写入到这个位置(赋值),它将不会修改元素v[3]的值,因为它已经位于别处。

如果我们运气不佳,这块内存已经被其他人用于其他用途,这种操作的后果可能极为不妙,有可能修改了正好位于这个位置的一个不相关的变量的值,甚至可能有导致程序崩溃的可能。

前面的示例代码中所涉及的是指针,如果涉及的是引用,也会发生同样的事情。例如:我们不是写成:

int* my_favorite_element_ptr = &v[3];

而是写成:

int& my_favorite_element_ptr = &v[3]

其结果是完全相同的。原因是引用只是“解引用后的指针”。它知道一个变量的地址,但为了访问它所指向的内存,并不需要再变量前面加上星号。因此语法虽不同,但结果却是一样的。

最后,当我们使用迭代器时,也会出现相同的结果。例如:

vector<int> v;

fir(int i=0; i<10; ++i)
	v.push_back(i);

vector<int>::const_iterator old_begin = v.begin();

cout<<"Adding more elements..."<<endl;

for(int i=0; i<100; ++i)
	v.push_back(i*10);

vector<int>::const_iterator new_begin = v.begin();

if(old_begin == new_begin)
	cout<<"Begin-s are the same."<<endl;
else
	cout<<"Begin-s are DIFFERENT."<<endl;

cout<<"My favorite element = "<<(*my_favorite_element_ptr)<<endl;
cout<<"Its address = "<<&v[3]<<endl;

它的输出结果如下:

Adding more elements...

Begin-s are DIFFERENT.

因此,如果我们保存一个指向某个元素(可以是任何元素,并不一定是begin()所指向的元素),它可能会在vector的内容被修改之后失效,因此vector的内部数组以及begin()所产生的对应迭代器可能被移动到其他位置。

因此,在修改vector之前所得到的指向其中某个元素的任何指针、引用或迭代器在vector由于增加元素而被修改之后就不应该再使用。实际上,对几乎所有STL容器以及所有可能修改容器长度的操作(例如,添加或删除元素),情况都是如此。有些容器,例如hash_set和hash_map,并不正式属于STL,但它们与STL相似,将来很可能加入到STL中。在涉及这里所讨论的问题时,它们的行为与STL容器相同:在修改容器之后,迭代器就不再有效。虽然有些STL容器在添加或删除元素之后,仍然保留原先指向它的元素的迭代器,但STL库的整体精神是可以用一种容器替换另一种容器,而原先的代码仍然没有问题。因此,在STL容器或类似STL的容器被修改之后,就不应该假设它的迭代器仍然是有效的。

注意:在前面的示例代码中,我们在访问这个指针相同的线程内修改了容器,如果一个线程中保存了一个指针、引用和迭代器,同时在另一个线程中修改容器,不仅会出现相同的问题,还会导致更复杂的情况

有趣的是,在以上的示例代码中,索引在指针失败时仍然起作用:如果通过一个基数为0的索引标记了一个元素(即第一个例子中,对int index_of_my_favorite_element = 3这样的语句),这个例子能够正确的继续执行。当然,使用索引的开销(速度更慢)要大于指针,因为访问与索引对应的元素时,vector必须执行一些运算,即每次使用[]操作符时计算变量的地址。它的优点是能够起作用,缺点是只适用于vector。对于所有其他STL容器,一旦修改了容器,必须再次找到指向所需元素的迭代器。

总结:修改了容器之后,不要再保存指向容器内元素的指针、引用或迭代器。