94.顺序容器操作

  顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。本部分将介绍顺序容器所特有的操作。

1.向顺序容器添加元素

  除array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。表9.5列出了向顺序容器(非array)添加元素的操作。

表9.5:向顺序容器添加元素的操作
这些操作会改变容器的大小;array不支持这些操作。
forward_list有自己专有版本的insert和emplace;参见9.3.4节(第312页)。
forward_list不支持push_back和emplace_back。
vector和string不支持push_front和emplace_front。
c.push_back(t) 在c的尾部创建一个值为t或由args创建的元素。返回void
c.emplace_back(args)
c.push_front(t) 在c的头部创建一个值为t或由args创建的元素。返回void
c.emplace_front(args)
c.insert(p, t) 在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加的元素的迭代器
c.emplace(p, args)
c.insert{p, n, t} 在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素的迭代器;若n为0,则返回p
c.insert(p, b, e) 将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素。返回指向新添加的第一个元素的迭代器:若范围为空,则返回p
c.insert(p, il) il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器:若列表为空,则返回p
警告:向一个vector、String或deque插入元素会使所有指向容器的迭代器、引用和指针失效。

  当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加 元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。

1.1使用 push_back

  除array和forward_list之外,每个顺序容器(包括string类型)都支持 push_back。

例如,下面的循环每次读取一个string到 word中, 然后追加到容器尾部:

//从标准输入读取数据,将每个单词放到容器末尾
string word; 
while(cin >> word) 
    container.push_back(word); 

  对push_back的调用在container尾部创建了一个新的元素,将container的size增大了1。该元素的值为 word的一个拷贝。container的类型可以是list、vector或deque。

  由于string是一个字符容器,我们也可以用push_back在string末尾添加字符:

void pluralize(size_t cnt, string &word) 
{
    if(cnt > 1) 
        word.push_back('s');//等价于word += 's'
}
关键概念:容器元素是拷贝
--------------------------------------------------------------------------------------------------------------------------------
    当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数(参见3.2.2节,第79页)一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。

1.2使用 push_front

  除了push_back、lis、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部:

list<int> ilist; 
//将元素添加到ilist开头
for(size_t ix = 0; ix != 4; ++ix) 
    ilist.push_front(ix); 

  此循环将元素0、1、2、3添加到llist头部。每个元素都插入到list的新的开始位置(new beginning)。即,当我们插入1时,它会被放置在0之前,2被放置在1之前,依此类推。因此,在循环中以这种方式将元素添加到容器中,最终会形成逆序。在循环执行完毕后,ilist保存序列3、2、1、0。

  注意,deque像vector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_front。deque保证在容器首尾进行插入和删除元素的操作都只花费常数时间。与vector一样,在deque首尾之外的位置插入元素会很耗时。

1.3在容器中的特定位置添加元素

  push_back和push_front操作提供了一种方便地在顺序容器尾部或头部插入单个元素的方法。insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector、deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员,我们将在9.3.4节(第312页)中介绍。
  每个insert函数都接受一个迭代器作为其第一个参数。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以insert函数将元素插入到迭代器所指定的位置之前。例如,下面的语句

slist.insert(iter, "Hello'");//将”Hello!"添加到iter之前的位置

将一个值为'Hello"的string插入到iter指向的元素之前的位置。

  虽然某些容器不支持push_front操作,但它们对于insert操作并无类似的限制(插入开始位置)。因此我们可以将元素插入到容器的开始位置,而不必担心容器是否支持 push_front:

vector<string> svec; 
list<string> slist; 
//等价于调用slist.push_front("Hello!"); 
slist.insert(slist.begin(), "Hello!"); 
//vector不支持push_front,但我们可以插入到begin()之前
//警告:插入到vector末尾之外的任何位置都可能很慢
svec.insert(svec.begin(), "Hello!"); 
警告:将元素插入到vector、deque和string中的任何位置都是合法的。然而,这样做可能很耗时。

1.4插入范围内元素

  除了第一个迭代器参数之外,insert函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素都按给定值初始化:

  这行代码将10个元素插入到svec的末尾,并将所有元素都初始化为string"Anna”。
接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:

svec.insert(svec.end(), 10, "Anna"); 

这行代码将10个元素插入到svec的末尾,并将所有元素都初始化为string"Anna”。
  接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:

vector<string> v = {"quas正", "simba", "frollo", "scar"};//将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
slist.insert(slist.end(),{"these", "words", "will", "go", "at", "the", "end"}); 
//运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end()); 

如果我们传递给 insert一对迭代器,它们不能指向添加元素的目标容器。

  在新标准下,接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器(在旧版本的标准库中,这些操作返回 void)如果范围为空,不插入任何元素,insert操作会将第一个参数返回。

1.5使用 insert 的返回值

通过使用 insert的返回值,可以在容器中一个特定位置反复插入元素:

list<string> lst; 
auto iter = lst.begin(); 
while (cin >> word) 
    iter = lst.insert(iter, word);//等价于调用push_front
注意:理解这个循环是如何工作的非常重要,特别是理解这个循环为什么等价于调用push_front尤为重要。

在循环之前,我们将iter初始化为lst.begin()。第一次调用insert会将我们刚刚读入的 string 插入到 iter 所指向的元素之前的位置。insert 返回的迭代器恰好指向这一个新元素。我们将此迭代器赋予 iter并重复循环,读取下一个单词。只要继续有单词读入,每步 while 循环就会将一个新元素插入到 iter 之前,并将 iter 改变为新加入元素的位置。此元素为(新的 )首元素。因此,每步循环将一个新元素插入到list 首元素之前的位置。

1.6使用emplace操作

  新标准引入了三个新成员 emplace_front、emplace 和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应 push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

  当调用 push 或 insert 成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个 emplace 成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定 c 保存 Sales_data(参见 7.1.4 节,第 237 页)元素:

//在c的末尾构造一个Sales_data对象
//使用三个参数的Sales—data构造函数
c.emplace_back("978-0590353403", 25, 15.99);
//错误:没有接受三个参数的push_back版本
c.push_back("978-0590353403", 25, 15.99);
//正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data("978-0590353403", 25, 15.99)); 

其中对 emplace_back的调用和第二个 push_back 调用都会创建新的 Sales_data 对象。在 调用 emplace_back 时,会在容器管理的内存空间中直接创建对象。而调用push_back则会创建一个局部临时对象,并将其压入容器中。

  emplace 函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配:

//iter指向c中一个元素,其中保存了Sales_data元素
c.emplace_back();//使用Sales_data的默认构造函数
c.emplace(iter, "999-999999999");//使用Sales_ data(string)
//使用Sales_data的接受一个ISBN、一个count和一个price的构造函数
c.emplace_front("978-0590353403", 25, 15.99);
注意:emp1ace素类型的构造函数相匹配。函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。

2.访问元素

  表9.6列出了我们可以用来在顺序容器中访问元素的操作。如果容器中没有元素,访问操作的结果是未定义的。

包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有个back成员函数。这两个操作分别返回首元素和尾元素的引
用:

//在解引用一个迭代器或调用front或back之前检查是否有元素
if(!c.empty()) 
{
    //val和val2是c中第一个元素值的拷贝
    auto val = *c. begin(), val2 = c. front(); 
    //val3和val4是c中最后一个元素值的拷贝
    auto last = c.end(); 
    auto val3 = *(--last);//不能递减forward list 迭代器   
    auto val4 = c.back(); //forward—list不一支持
}

此程序用两种不同方式来获取c中的首元素和尾元素的引用。直接的方法是调用front和back。而间接的方法是通过解引用begin返回的迭代器来获得首元素的引用,以及通过递减然后解引用end返回的迭代器来获得尾元素的引用。

  这个程序有两点值得注意:迭代器end指向的是容器尾元素之后的(不存在的)元素。为了获取尾元素,必须首先递减此迭代器。另一个重要之处是,在调用 front和back(或解引用begin和end返回的迭代器)之前,要确保 c非空。如果容器为空,if中操作的行为将是未定义的。

表9.6: 在顺序容器中访问元素的操作
at和下标操作只适用于string、vector、deque和array。
back不适用于forward_list。
c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
c.front() 返回c中首元素的引用。若c为空,函数行为未定义
c[n] 返回c中下标为n的元素的引用,n是一个无符号整数。若n>=c.size(), 则函数行为未定义
c.at(n) 返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常
对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误。

2.1访问成员函数返回的是引用

  在容器中访问元素的成员函数(即, front、back、 下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值:

if (!c.empty())
{
    c. front() = 42;//将42赋子c中的第一个元素           
    auto &v = c.back();//获得指向最后一个元素的引用
    V = 1024;//改变c中的元素 
    auto v2 = c.back();//v2不是一个引用,它是c.back()的一个拷贝
    v2 = 0;//未改变c中的元素
}

与往常一 样,如果我们使用auto变量来保存这些函数的返回值,并且希望使用此变量来 改变元素的值,必须记得将变量定义为引用类型。

2.2下标操作和安全的随机访问

  提供快速随机访问的容器(string、vector、deque和array)也都提供下标运算符(参见3.3.3节, 第91页)。就像我们已经看到的那样,下标运算符接受一个下标参数,返回容器中该位置的元素的引用。给定下标必须 “在范围内 “ (即, 大于等于0,且小于容器的大小)。 保证下标有效是程序员的责任,下标运算符并不检查下标是否在合法范围内。使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误。
  如果我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at会抛出一个out_of_range异常(参见5.6节, 第173页):标参

vector<string> svec;//空vector
cout << svec[0];//运行时错误:svec中没有元素! 
cout << svec.at(0);//抛出 一个out_of_range异常 

3.删除元素

表9.7: 顺序容器的删除操作
这些操作会改变容器的大小,所以不适用于array。
forward_list有特殊版本的erase,参见9.3.4节(第312页)。
forward_list不支持pop_back;vector和string不支持pop_front。
c.pop_back() 删除c中尾元素。若c为空,则函数行为未定义。函数返回void
c.pop_front() 删除c中首元素。若c为空,则函数行为未定义。函数返回void
c.erase(p) 删除迭代器p所指定的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后(off-the-end)迭代器。若p是尾后迭代器,则函数行为未定义
c.erase(b, e) 删除迭代器b和e所指范围内的元素。返回一个指向最后一个被删除元素之后的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear() 删除c中的所有元素。返回void
警告:删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
警告:删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它们是存在的。

3.1pop_front 和 pop_back 成员函数

  pop_front和pop—back成员函数分别删除首元素和尾元素。与vector和string不支持push_front一样,这些类型也不支待pop_front。类似的,forward_list不支持pop_back。与元素访问成员函数类似,不能对一个空容器执行弹出操作。

  这些操作返回void。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:

while (!ilist.empty()) 
{
    process (ilist.front());//对1巨st的首元素进行一些处理 
    ilist.pop—front();//完成处理后删除首元素
}

3.2从容器内部删除一个元素

  成员函数erase从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,那么erase(i) 将返回指向j的迭代器。

例如,下面的循环删除一个list中的所有奇数元素:

list<int> 1st = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
auto it = 1st.begin(); 
while(it != 1st.end()) 
{
    if (*it%2)//若元素为奇数
        it = lst.erase(it);//删除此元素
    else 
        ++it;
}

每个循环步中,首先检查当前元素是否是奇数。如果是,就删除该元素,并将it设置为我们所删除的元素之后的元素。如果*it为偶数,我们将it递增,从而在下一步循环检查下一个元素。

3.3删除多个元素

  接受一对迭代器的erase版本允许我们删除一个范围内的元素:

//删除两个迭代器表示的范围内的元素
//返回指向最后一个被删元素之后位置的迭代器
elem1 = slist.erase(elem1, elem2);//调用后,eleml == elem2 

迭代器elem1指向我们要删除的第一个元素,elem2指向我们要删除的最后一个元素之后的位置。
  为了删除一个容器中的所有元素,我们既可以调用clear,也可以用begin和end获得的迭代器作为参数调用erase:

slist.clear();//删除容器中所有元素
slist.erase(slist.begin (), slist.end());//等价调用

4.特殊的forward_list操作

  为了理解 forward_list 为什么有特殊版本的添加和删除操作,考虑当我们从一个单向链表中删除一个元素时会发生什么。如图9.1所示,删除一个元素会改变序列中的链接。在此情况下,删除elem3会改变elem2,elem2原来指向elem3,但删除elem3后,elem2 指向了elem4 0

  当添加或删除一个元素时,删除或添加 的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,我们需要访问其前驱,以便改变前驱的链接。但是,forward_list是单向链表 。在 一个单向链表中,没有简单的方法来获取一个元素的前驱。出于这个原因,在 一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,我们总是可以访问到被添加或删除操作所影响的元素。

  由于这些操作与其他容器上的操作的实现方式不同,forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after和erase_after的操作(参见表9.8)。例如,在我们的例子中,为了删除elem3,应该用指向elem2 的迭代器调用erase_after 。为了支持这些操作,forward_list也定义了before_begin,它返回一个首前(off-the-beginning)迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素” 之后 “添加或删除元素(亦即在链表首元素之前添加删除元素)。

表9.5:向顺序容器添加元素的操作
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。cbefore_begin()返回一个const_iterator
lst.cbefore_begin()
lst.before_begin() 在迭代器p之后的位置插入元素。t是一个对象,n是数量,b和e是表示范围的一对迭代器(b和e不能指向lst内),il是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义
lst.insert_after(p, n, t)
lst.insert_after(p, b, e)
lst.insert_after(p,il)
emplace_after(p, args) 使用args在p指定的位宜之后创建一个元素。返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义
lst.erase_after(p) 删除p指向的位置之后的元素,或删除从b之后直到(但不包含)e之间的元素。返回一个指向被删元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p指向1st的尾元素或者是一个尾后迭代器,则函数行为未定义
lst.erase_after(b,e)

  当在 forward_list中添加或删除元素时,我们必须关注两个迭代器---一个指向我们要处理的元素,另一个指向其前驱。例如,可以改写第312页中从list中删除奇数元素的循环程序,将其改为从forward_list中删除元素:

forward_list<int> flst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto prev = flst.before_begin();//表示flst的“首前元素”
auto curr = flst.begin();//表示flst中的第一个元素 
while (curr != flst.end())//仍有元素要处理 
{
    if(*curr % 2)//若元素为奇数
        curr = flst.erase_after(prev);//删除它并移动curr
    else
    {       
        prev = curr;//移动迭代器curr,指向下一个元素,prev指向
        ++curr;//curr之前的元素
    }
}

此例中,curr表示我们要处理的元素,prev表示curr的前驱。调用begin来初始化curr,这样第一步循环就会检查第一个元素是否是奇数。我们用before_begin来初始化prev,它返回指向curr之前不存在的元素的迭代器。

  当找到奇数元素后,我们将prev传递给erase_after。 此调用将prev之后的元素删除,即,删除 curr指向的元素。然后我们将curr重置为erase_after的返回值,使得curr指向序列中下一个元素,prev保持不变,仍指向(新) curr之前的元素。如果curr指向的元素不是奇数,在else中我们将两个迭代器都向前移动。

5.改变容器大小

  如表9.9所描述,我们可以用resize来增大或缩小容器,与往常一样,array不支 持resize。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:

list<int> ilist(10, 42);//10个int:每个的值都是42 
ilist.resize(15);//将5个值为0的元素添加到过ilist的末尾
ilist.resize(25, -1);//将10个值为-1的元素添加到ilist的末尾
ilist.resize(5);//从ilist末尾删除20个元素 

  resize操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化(参见.3.3.1节, 第88页)。如果容器保存的是类类型元素,且resize向容器添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数 。

表9.9:顺序容器大小操作
resize不适用于array
c.resize(n) 调整c的大小为n个元素。 若n < c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化
c.resize(n,t) 调整c的大小为n个元素。任何新添加的元素都初始化为值t
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效。

6.容器操作可能使迭代器失效

  向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针样的问题(参见2.3.2节,第49页)

在向容器添加元素后:

●如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代 器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
●对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
●对于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。

当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了。当我们删除一个元素后:

●对于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和 首前迭代器)、引用和指针仍有效。
●对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代 器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
●对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效。注意:当我们删除元素时,尾后迭代器总是会失效。

注意:使用失效的迭代器、指针或引用是严重的运行时错误。
建议:管理迭代器
当你使用迭代器(或指向容器元素的引用或指针)时,最小化要求迭代器必须保持有效的程序片段是一个好的方法。
   由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector、string和deque尤为重要。

6.1编写改变容器的循环程序

  添加/删除vector、string或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert 或 erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可 以用来更新:

//傻瓜循环,删除偶数元素,复制每个奇数元素
vector<int> vi= {0,1,2,3,4,5,6,7,8,9}; 
auto iter = vi.begin();//调用begin而不是cbegin,因为我们要改变vi
while (iter != vi.end()) 
{
    if (*iter % 2) 
    {
        iter = vi.insert(iter, *iter);//复制当前元素
        iter += 2;//向前移动迭代器,跳过当前元素以及插入到它之前的元素
    }
    else
        iter = vi.erase(iter);//删除偶数元素
    //不应向前移动迭代器,iter指向我们删除的元素之后的元素
}

此程序删除vector中的偶数值元素,并复制每个奇数值元素。我们在调用insert和 erase后都更新迭代器,因为两者都会使迭代器失效。
  在调用erase后,不必递增迭代器,因为erase返回的迭代器已经指向序列中下一个元素。调用insert后,衙要递增迭代器两次。记住,insert在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用insert后,iter指向新插入元素,位于我们正在处理的元素之前。我们将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。

6.2不要保存 end 返回的迭代器

  当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作容器末尾使 用。通常C++标准库的实现中end()操作都很快,部分就是因为这个原因。

  例如,考虑这样一个循环,它处理容器中的每个元素,在其后添加一个新元素。我们希望循环能跳过新添加的元素,只处理原有元素。在每步循环之后,我们将定位迭代器,使其指向下“优化”这个循环,在循环之前保存end()返回的迭代器,一直用作容器末尾,就会导致一场灾难:

//灾难:此循环的行为是未定义的
auto begin = v. begin(); 
end = v.end();//保存尾迭代器的值是一个坏主意
while (begin != end)
{
    //做一些处理
    //插入新值,对begin重新赋值,否则的话它就会失效
    ++begin;//向前移动begin,因力我们想在此元素之后插入元素
    begin = v.insert(begin, 42);//插入新值
    ++begin;//向前移动begin跳过我们刚刚加入的元素
}

此代码的行为是未定义的。在很多标准库实现上,此代码会导致无限循环。问题在于我们一将end操作返回的迭代器保存在一个名为end的局部变量中。在循环体中,我们向容器中添加了一个元素,这个操作使保存在end中的迭代器失效了。这个迭代器不再指向v中任何元素,或是v中尾元素之后的位置。

建议:如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。

必须在每次插入操作后重新调用end(),而不能在循环开始前保存它返回的迭代器:

//更安全的方法:在每个循环步添加/删除元素后都重新计算end 
while (begin != v.end()) 
{
    //做一些处理
    ++begin;//向前移动begin,因为我们想在此元素之后插入元素 
    begin= v.insert(begin, 42);//插入新值
    ++begin;//向前移动begin,跳过我们刚刚加入的元素
}

参考资料:

C++ Primer

posted @ 2023-05-14 22:05  CodeMagicianT  阅读(25)  评论(0编辑  收藏  举报