顺序容器的操作(续)

[1. 关系操作符]

所有的容器类型都支持用关系操作符来实现两个容器的比较。
相互比较的容器必须具有相同的容器类型,而且其元素类型也必须相同。

容器的比较实质上是基于容器内元素的比较。
如果容器的元素类型不支持某种操作符,则该容器就不能做这种比较运算。

下面的操作类似于 string 类型的关系运算:

如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相等;否则,它们就不相等。
如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
如果两个容器都不是对文的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。

理解上述操作的最简单方法是研究例程:

/**
* ivec1: 1 3 5 7 9 12
* ivec2: 0 2 4 6 8 10 12
* ivec3: 1 3 9
* ivec4: 1 3 5 7
* ivec5: 1 3 5 7 9 12 
**/
// ivec1 and ivec2 differ at element[0]: ivec1 greater than ivec2
ivec1 < ivec2 // false
ivec2 < ivec1 // true

// ivec1 and ivec3 differ at element[2]: ivec1 less than ivec3
ivec1 < ivec3 // true

// all elements equal, but ivec4 has fewer elements, so ivec1 is greater than ivec4
ivec1 < ivec4 // false

ivec1 == ivec5 // true; each element equal and same number of elements
ivec1 == ivec4 // false; ivec4 has fewer elements than ivec1
ivec1 != ivec4 // true; ivec4 has fewer elements than ivec1

C++ 语言只允许两个容器做其元素类型定义的关系运算。

所有容器都通过比较其元素对来实现关系运算:

ivec1 < ivec2

假设 ivec1 和 ivec2 都是 vector<int> 类型的容器,
则上述比较使用了内置 int 型定义的小于操作符。
如果这两个 vector 容器存储的是 strings 对象,则使用 string 类型的小于操作符。
如果上述 vector 容器存储某一类型的对象,而此类型没有定义关系运算,则该比较运算不合法。

 

[2. 容器大小的操作]

所有容器类型都提供 4 种与容器大小相关的操作:

// 顺序容器的大小操作
c.size()      返回容器 c 中的元素个数。返回类型为 c::size_type

c.max_size()    返回容器 c 可容纳的最多元素个数。返回类型为 c::size_type

c.empty()      返回标记容器大小是否为 0 的布尔值

c.resize(n)    调整容器 c 的长度大小,使其能容纳 n 个元素,
           如果 n < c.size(),则删除多出来的元素;否则,添加采用默认值初始化的新元素

c.resize(n,t)     调整容器 c 的长度大小,使其能容纳 n 个元素。所有新添加的元素值都为 t

参见如下代码实例:

list<int> ilist(10, 42); // 10 ints: each has value 42

ilist.resize(15);    // adds 5 elements of value 0 to back of ilist
ilist.resize(25, -1); // adds 10 elements of value -1 to back of ilist
ilist.resize(5);    // erases 20 elements from the back of ilist

resize 操作可带有一个可选的元素值形参。
如果在调用该函数时提供了这个参数,则所有新添加的元素都初始化为这个值。
如果没有这个参数,则新添加的元素采用默认值初始化。

注意:resize 操作可能会使迭代器失效。
在 vector 或 deque 容器上做 resize 操作有可能会使其所有的迭代器都失效。
对于所有的容器类型,如果 resize 操作压缩了容器,则指向已删除的元素迭代器失效。

 

[3. 访问元素]

如果容器非空,那么容器类型的 front 和 back 成员将返回容器内第一个或最后一个元素的引用。
参见如下代码实例:

// check that there are elements before dereferencing an iterator
// or calling front or back
if (!ilist.empty()) {
  // val and val2 refer to the same element
  list<int>::reference val = *ilist.begin();
  list<int>::reference val2 = ilist.front();

  // last and last2 refer to the same element
  list<int>::reference last = *--ilist.end();
  list<int>::reference last2 = ilist.back();
}

这段程序使用了两种不同的方法获取时 ilist 中的第一个和最后一个元素的引用。
在这段程序中,有两个地方值得注意:
  1. end 迭代器指向容器的超出末端的下一位置,因此必须先对其减 1 才能获取最后一个元素;
  2. 在调用 front 或 back 函数之前,
    或者在对 begin 或 end 返回的迭代器进行解引用运算之前,必须保证 ilist 容器非空。
    如果该 list 容器为空,则 if 语句内所有的操作都没有定义。

// 访问顺序容器内元素的操作
c.back()    返回容器 c 的最后一个元素的引用。如果 c 为空,则该操作未定义

c.front()   返回容器 c 的第一个元素的引用。如果 c 为空,则该操作未定义

c[n]     返回下标为 n 的元素的引用
       如果 n <0 或 n >= c.size(),则该操作未定义
       只适用于 vector 和 deque 容器

c.at(n)     返回下标为 n 的元素的引用。如果下标越界,则该操作未定义
       只适用于 vector 和 deque 容器

由于下标操作符本身不会做相关的检查,所以程序员必须保证自己使用的指定下标位置上的元素确实存在。
使用 front 或 back 运算时,必须注意同样的问题。

如果容器为空,那么这些操作将产生未定义的结果。
如果容器内只有一个元素,则 front 和 back 操作都返回对该元素的引用。

使用越界的下标,或调用空容器的 front 或 back 函数,都会导致程序出现严重的错误。

使用下标运算的另一个可选方案是 at 成员函数。这个函数的行为和下标运算相似,
但是如果给出的下标无效,at 函数将会抛出 out_of_range 异常:

vector<string> svec; // empty vector
cout << svec[0];   // run-time error: There are no elements in svec!
cout << svec.at(0); // throws out_of_range exception

 

 [4. 删除元素]

容器类型提供了通用的 erase 操作和特定的 pop_front 和 pop_back 操作来删除容器内的元素。

// 删除顺序容器内元素的操作
c.erase(p)    删除迭代器 p 所指向的元素
          返回一个迭代器,它指向被删除元素后面的元素。
          如果 p 指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置。
          如果 p 本身就是指向超出末端的下一位置的迭代器,则该函数未定义

c.erase(b,e)   删除迭代器 b 和 e 所标记的范围内所有的元素
          返回一个迭代器,它指向被删除元素段后面的元素。
          如果 e 本身就是指向超出末端的下一位置的迭代器,
          则返回的迭代器也指向容器的超出末端的下一位置

c.clear()     删除容器 c 内的所有元素。返回 void

c.pop_back()     删除容器 c 的最后一个元素。返回 void。如果 c 为空容器,则该函数未定义

c.pop_front()    删除容器 c 的第一个元素。返回 void。如果 c 为空容器,则该函数未定义
          只适用于 list 或 deque 容器

1) 删除第一个或最后一个元素

pop_front 和 pop_back 函数用于删除容器内的第一个和最后一个元素。
但 vector 容器类型不支持 pop_front 操作。
这些操作删除指定的元素并返回 void。

pop_front 操作通常与 front 操作配套使用,实现以栈的方式处理容器:

while (!ilist.empty()) {
  process(ilist.front()); // do something with the current top of ilist
  ilist.pop_front(); // done; remove first element
}

要获取删除的元素值,则必须在删除元素之前调用 front 或 back 函数。

2) 删除容器内的一个或一段元素

该 erase 操作有两个版本:

  1. 删除由一个迭代器指向的单个元素;
  2. 删除由一对迭代器标记的一段元素。

erase 的这两种形式都返回一个迭代器,它指向被删除元素或元素段后面的元素。
即,如果元素 j 恰好紧跟在元素 i 后面,则将元素 i 从容器中删除后,删除操作返回指向 j 的迭代器。

如同其他操作一样,erase 操作也不会检查它的参数。
程序员必须确保用作参数的迭代器或迭代器范围是有效的。

通常,程序员必须在容器中找出要删除的元素后,才使用 erase 操作。
寻找一个指定元素的最简单方法是使用标准库的 find 算法
为了使用 find 函数或其他泛型算法,在编程时,必须将 algorithm 头文件包含进来。
find 函数需要一对标记查找范围的迭代器以及一个在该范围内查找的值作参数。
查找完成后,该函数返回一个迭代器,它指向具有指定值的第一个元素,或超出末端的下一位置。

string searchValue("Quasimodo");
list<string>::iterator iter =
find(slist.begin(), slist.end(), searchValue);

if (iter != slist.end())
slist.erase(iter);

注意,在删除元素之前,必须确保元素确实存在——
如果删除指向超出末端的下一位置的迭代器(end 迭代器),那么 erase 操作的行为未定义。

3) 删除容器内所有元素

要删除容器内所有的元素,可以调用 clear 函数,或将 begin 和 end 迭代器传递给 erase 函数。

slist.clear(); // delete all the elements within the container
slist.erase(slist.begin(), slist.end()); // equivalent

erase 函数的迭代器对版本提供了删除一部分元素的功能:

// delete range of elements between two values
list<string>::iterator elem1, elem2;

// elem1 refers to val1
elem1 = find(slist.begin(), slist.end(), val1);

// elem2 refers to the first occurrence of val2 after val1
elem2 = find(elem1, slist.end(), val2);

// erase range from val1 up to but not including val2
slist.erase(elem1, elem2);

这段代码首先调用了 find 函数两次,以获得指向特定元素的两个迭代器。
迭代器 elem1 指向第一个具有 val1 值的元素,
如果容器 list 中不存在值为 val1 的元素,则该迭代器指向超出末端的下一位置。
如果在 val1 元素后面存在值为 val2 的元素,
那么迭代器 elem2 就指向这段范围内第一个具有 val2 值的元素,
否则,elem2 就是一个超出末端的迭代器。
最后调用 erase 函数,
删除从迭代器 elem1 开始一直到 elem2 之间的所有元素(注意不包括 elem2 指向的元素)。

erase、pop_front 和 pop_back 函数使指向被删除元素的所有迭代器失效。
对于 vector 容器,指向删除点后面的元素的迭代器通常也会失效。
而对于 deque 容器,如果删除时不包含第一个元素或最后一个元素,
那么该 deque 容器相关的所有迭代器都会失效。

[5. 赋值与 swap]

与赋值相关的操作符都作用于整个容器。
除 swap 操作外,其他操作都可以用 erase 和 insert 操作实现。
赋值操作符首先 erases 其左操作数容器中的所有元素,
然后将右操作数容器的所有元素 inserts 到左边容器中:

c1 = c2; // replace contents of c1 with a copy of elements in c2
// equivalent operation using erase and insert
c1.erase(c1.begin(), c1.end()); // delete all elements in c1
c1.insert(c1.begin(), c2.begin(), c2.end()); // insert c2

赋值后,左右两边的容器相等:
尽管赋值前两个容器的长度可能不相等,但赋值后两个容器都具有右操作数的长度。

注意:赋值和 assign 操作使左操作数容器的所有迭代器失效。而swap 操作并不会使迭代器失效。
完成 swap 操作后,尽管被交换的元素已经存放在另一容器中,但迭代器仍然指向相同的元素。

// 顺序容器的赋值操作
c1 = c2      删除容器 c1 的所有元素,然后将 c2 的元素复制给 c1。
          c1 和 c2 的类型(包括容器类型和元素类型)必须相同

c1.swap(c2)   交换内容:c1 和 c2 中存放元素。c1 和 c2 的类型必须相同。
          该函数的执行速度通常要比将 c2 复制到 c1 的操作快

c.assign(b,e)  重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。
          b 和 e 必须不是指向 c 中元素的迭代器

c.assign(n,t)  将容器 c 重新设置为存储 n 个值为 t 的元素

关于 assign 操作

assign 操作首先删除容器中所有的元素,然后将其参数所指定的新元素插入到该容器中。
与复制容器元素的构造函数一样,如果两个容器类型相同,其元素类型也相同,
就可以使用赋值操作符(=)将一个容器赋值给另一个容器。
如果在不同(或相同)类型的容器内,元素类型不相同但是相互兼容,则其赋值运算必须使用 assign 函数。
例如,可通过 assign 操作实现将 vector 容器中一段 char* 类型的元素赋给 string 类型 list 容器。

由于 assign 操作首先删除容器中原来存储的所有元素,
因此,传递给 assign 函数的迭代器不能指向调用该函数的容器内的元素。

assign 函数的参数决定了要插入多少个元素以及新元素的值是什么。
下列语句使用了带一对迭代器参数的 assign 函数版本——

// equivalent to slist1 = slist2
slist1.assign(slist2.begin(), slist2.end());

在删除 slist1 的元素后,该函数将 slist2 容器内一段指定的元素复制到 slist2 中。
于是,这段代码行等效于将 slist2 赋给 slist1。

带有一对迭代器参数的 assign 操作允许我们将一个容器的元素赋给另一个不同类型的容器。

assign 运算的第二个版本需要一个整型数值和一个元素值做参数,
它将容器重置为存储指定数量的元素,并且每个元素的值都为指定值:

// equivalent to: slist1.clear();
// followed by slist1.insert(slist1.begin(), 10, "Hiya!");
slist1.assign(10, "Hiya!"); // 10 elements; each one is Hiya!


使用 swap 操作以节省删除元素的成本

swap 操作实现交换两个容器内所有元素的功能。要交换的容器的类型必须匹配:
操作数必须是相同类型的容器,而且所存储的元素类型也必须相同。
调用了 swap 函数后,右操作数原来存储的元素被存放在左操作数中,反之亦然。

vector<string> svec1(10); // vector with 10 elements
vector<string> svec2(24); // vector with 24 elements
svec1.swap(svec2);

关于 swap 的一个重要问题在于:
该操作不会删除或插入任何元素,而且保证在常量时间内实现交换。
由于容器内没有移动任何元素,因此迭代器不会失效。
它们指向同一元素,就像没作 swap 运算之前一样。
虽然,在 swap 运算后,这些元素已经被存储在不同的容器之中了。
例如,在做 swap 运算之前,有一个迭代器 iter 指向 svec1[3] 字符串;
实现 swap 运算后,该迭代器则指向 svec2[3] 字符串(这是同一个字符串,只是存储在不同的容器之中而已)。

 

posted @ 2013-05-28 17:17  HandsomeDragon  阅读(227)  评论(0编辑  收藏  举报