C++学习笔记八-顺序容器(一)
一、顺序容器:将单一类型元素聚集起来成为容器,然后根据位置来存储和访问这些元素,这就是顺序容器。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。
1.标准库定义了三种顺序容器类型:vector、list 和 deque(是双端队列“double-ended queue”的简写,发音为“deck”)。它们的差别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。
vector 支持快速随机访问
list 支持快速插入/删除
deque 双端队列
2.标准库还提供了三种容器适配器(adaptors)。实际上,适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型。顺序容器适配器包括 stack、queue 和 priority_queue 类型.
stack 后进先出(LIFO)堆栈
queue 先进先出(FIFO)队列
priority_queue 有优先级管理的队列
3.容器只定义了少量操作。大多数额外操作则由算法库提供。标准库为由容器类型定义的操作强加了公共的接口。这些容器类型的差别在于它们提供哪些操作,但是如果两个容器提供了相同的操作,则它们的接口(函数名字和参数个数)应该相同。容器类型的操作集合形成了以下层次结构:
A.一些操作适用于所有容器类型。
B.另外一些操作则只适用于顺序或关联容器类型。
C.还有一些操作只适用于顺序或关联容器类型的一个子集
二、顺序容器的定义:
所有的容器都是类模板。要定义某种特殊的容器,必须在容器名后加一对尖括号,尖括号里面提供容器中存放的元素的类型:
vectorsvec; // empty vector that can hold strings list ilist; // empty list that can hold ints deque items; // empty deque that holds Sales_items 所有容器类型都定义了默认构造函数,用于创建指定类型的空容器对象。默认构造函数不带参数。为了使程序更清晰、简短,容器类型最常用的构造函数是默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行时性能,并且使容器更容易使用。
三、容器元素的初始化:
1.将一个容器初始化为另一个容器的副本:当不使用默认构造函数,而是用其他构造函数初始化顺序容器时,必须指出该容器有多少个元素,并提供这些元素的初值。同时指定元素个数和初值的一个方法是将新创建的容器初始化为一个同类型的已存在容器的副本:
vectorivec; vector ivec2(ivec); // ok: ivec is vector list ilist(ivec); // error: ivec is not list vector dvec(ivec); // error: ivec holds int not double 将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须相同。
2.初始化为一段元素的副本:
A.迭代器标记了要复制的元素范围,这些元素用于初始化新容器的元素。迭代器标记出要复制的第一个元素和最后一个元素。采用这种初始化形式可复制不能直接复制的容器。更重要的是,可以实现复制其他容器的一个子序列:
// initialize slist with copy of each element of svec list slist(svec.begin(), svec.end()); // find midpoint in the vector vector::iterator mid = svec.begin() + svec.size()/2; // initialize front with first half of svec: The elements up to but not including *mid deque front(svec.begin(), mid);B.回顾一下指针,我们知道指针就是迭代器,因此允许通过使用内置数组中的一对指针初始化容器也就不奇怪了:
char *words[] = {"stately", "plump", "buck", "mulligan"}; // calculate how many elements in words size_t words_size = sizeof(words)/sizeof(char *); // use entire array to initialize words2 list words2(words, words + words_size);其中第二个指针提供停止复制的条件,其所指向的位置上存放的元素并没有复制。
3.分配和初始化指定数目的元素:
A.创建顺序容器时,可显式指定容器大小和一个(可选的)元素初始化式。容器大小可以是常量或非常量表达式,元素初始化则必须是可用于初始化其元素类型的对象的值:
const list::size_type list_size = 64; list slist(list_size, "eh?"); // 64 strings, each is eh? B.不提供元素初始化式时,标准库将为该容器实现值初始化。采用这种类型的初始化,元素类型必须是内置或复合类型,或者是提供了默认构造函数的类类型。如果元素类型没有默认构造函数,则必须显式指定其元素初始化式。
接受容器大小做形参的构造函数只适用于顺序容器,而关联容器不支持这种初始化。
四、容器内元素的类型约束:
1.C++ 语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足以下两个约束:
A.元素类型必须支持赋值运算。
B.元素类型的对象必须可以复制。
C.除了引用类型外,所有内置或复合类型都可用做元素类型。引用不支持一般意义的赋值运算,因此没有元素是引用类型的容器。
D.除输入输出(IO)标准库类型,auto_ptr 类型之外,所有其他标准库类型都是有效的容器元素类型。特别地,容器本身也满足上述要求,因此,可以定义元素本身就是容器类型的容器。Sales_item 类型也满足上述要求。
2.容器操作的特殊要求:支持复制和赋值功能是容器元素类型的最低要求。此外,一些容器操作对元素类型还有特殊要求。如果元素类型不支持这些特殊要求,则相关的容器操作就不能执行:我们可以定义该类型的容器,但不能使用某些特定的操作。
其中一种需外加类型要求的容器操作是指定容器大小并提供单个初始化式的构造函数。如果容器存储类类型的对象,那么只有当其元素类型提供默认构造函数时,容器才能使用这种构造函数。尽管有一些类没有提供默认构造函数,但大多数类类型都会有。例如,假设类 Foo 没有默认构造函数,但提供了需要一个 int 型形参的构造函数。现在,考虑下面的声明:
vector<Foo> empty; // ok: no need for element default constructor vector<Foo> bad(10); // error: no default constructor for Foo vector<Foo> ok(10, 1); // ok: each element initialized to 13.容器的容器:A.因为容器受容器元素类型的约束,所以可定义元素是容器类型的容器。例如,可以定义 vector 类型的容器 lines,其元素为 string 类型的 vector 对象:
// note spacing: use ">>" not ">>" when specifying a container element type vector< vector> lines; // vector of vectors B.注意,在指定容器元素为容器类型时,必须如下使用空格:
vector< vector> lines; // ok: space required between close > vector< vector > lines; // error: >> treated as shift operator 必须用空格隔开两个相邻的 > 符号,以示这是两个分开的符号,否则,系统会认为 >> 是单个符号,为右移操作符,并导致编译时错误。
五、迭代器和迭代器范围
1.vector 和 deque 容器的迭代器提供额外的运算:
A.C++ 定义的容器类型中,只有 vector 和 deque 容器提供下面两种重要的运算集合:迭代器算术运算,以及使用除了 == 和 != 之外的关系操作符来比较两个迭代器(== 和 != 这两种关系运算适用于所有容器)。
B.关系操作符只适用于 vector 和 deque 容器,这是因为只有这种两种容器为其元素提供快速、随机的访问。它们确保可根据元素位置直接有效地访问指定的容器元素。这两种容器都支持通过元素位置实现的随机访问,因此它们的迭代器可以有效地实现算术和关系运算。
C.list 容器的迭代器既不支持算术运算(加法或减法),也不支持关系运算(<=, <, >=, >),它只提供前置和后置的自增、自减运算以及相等(不等)运算。
2.迭代器范围:C++ 语言使用一对迭代器标记迭代器范围(iterator range),这两个迭代器分别指向同一个容器中的两个元素或超出末端的下一位置,通常将它们命名为 first 和 last,或 beg 和 end,用于标记容器中的一段元素范围.
A.此类元素范围称为左闭合区间(left-inclusive interval),其标准表示方式为:
// to be read as: includes first and each element up to but not including last [ first, last )表示范围从 first 开始,到 last 结束,但不包括 last。迭代器 last 可以等于 first,或者指向 first 标记的元素后面的某个元素,但绝对不能指向 first 标记的元素前面的元素。
3.对形成迭代器范围的迭代器的要求:
A.它们指向同一个容器中的元素或超出末端的下一位置。
B.如果这两个迭代器不相等,则对 first 反复做自增运算必须能够到达 last。换句话说,在容器中,last 绝对不能位于 first 之前。
C.编译器自己不能保证上述要求。编译器无法知道迭代器所关联的是哪个容器,也不知道容器内有多少个元素。若不能满足上述要求,将导致运行时未定义的行为。
4.使迭代器失效的容器操作:一些容器操作会修改容器的内在状态或移动容器内的元素。这样的操作使所有指向被移动的元素的迭代器失效,也可能同时使其他迭代器失效。使用无效迭代器是没有定义的,可能会导致与悬垂指针相同的问题。
六、顺序容器的类型及操作
1.每种顺序容器都提供了一组有用的类型定义以及以下操作:a.在容器中添加元素。 b.在容器中删除元素。 c.设置容器大小。 d.(如果有的话)获取容器内的第一个和最后一个元素。
2.容器定义的类型别名:
size_type 无符号整型,足以存储此容器类型的最大可能容器长度
iterator 此容器类型的迭代器类型
const_iterator 元素的只读迭代器类型
reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 元素的只读(不能写)逆序迭代器
difference_type 足够存储两个迭代器差值的有符号整型,可为负数
value_type 元素类型
reference 元素的左值类型,是 value_type& 的同义词
const_reference 元素的常量左值类型,等效于 const value_type&
A.逆序迭代器从后向前遍历容器,并反转了某些相关的迭代器操作:例如,在逆序迭代器上做 ++ 运算将指向容器中的前一个元素。
B.需要使用元素类型时,只要用 value_type 即可。程序员无须直接知道容器元素的真正类型,就能使用它。如果要引用该类型,则通过 reference 和 const_reference 类型实现。
3.容器的 begin 和 end 操作:
c.begin() 返回一个迭代器,它指向容器 c 的第一个元素
c.end() 返回一个迭代器,它指向容器 c 的最后一个元素的下一位置
c.rbegin() 返回一个逆序迭代器,它指向容器 c 的最后一个元素
c.rend() 返回一个逆序迭代器,它指向容器 c 的第一个元素前面的位置
4.在顺序容器中添加元素:在容器中添加元素时,系统是将元素值复制到容器里。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,容器内元素值发生变化时,被复制的原值不会受到影响,反之亦然。
c.push_back(t) 在容器 c 的尾部添加值为 t 的元素。返回 void 类型
c.push_front(t) 在容器 c 的前端添加值为 t 的元素。返回 void 类型 只适用于 list 和 deque 容器类型.
c.insert(p,t) 在迭代器 p 所指向的元素前面插入值为 t 的新元素。返回指向新添加元素的迭代器
c.insert(p,n,t). 在迭代器 p 所指向的元素前面插入 n 个值为 t 的新元素。返回 void 类型
c.insert(p,b,e) 在迭代器 p 所指向的元素前面插入由迭代器 b 和 e 标记的范围内的元素,但不包括e本身,e可以是指向最后一个元素的下一个元素的迭代器。返回 void 类型
5.任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。避免存储 end 操作返回的迭代器
6.关系操作符:
A.所有的容器类型都支持用关系操作符来实现两个容器的比较。显比较的容器必须具有相同的容器类型,而且其元素类型也必须相同。
B.容器的比较是基于容器内元素的比较。容器的比较使用了元素类型定义的同一个关系操作符:两个容器做 != 比较使用了其元素类型定义的 != 操作符。如果容器的元素类型不支持某种操作符,则该容器就不能做这种比较运算。
C.如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相等;否则,它们就不相等。
如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
如果两个容器都不是对文的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。
D.C++ 语言只允许两个容器做其元素类型定义的关系运算。
7.容器大小的操作:
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
注意:resize 操作可能会使迭代器失效。在 vector 或 deque 容器上做 resize 操作有可能会使其所有的迭代器都失效。对于所有的容器类型,如果 resize 操作压缩了容器,则指向已删除的元素迭代器失效。
8.访问元素:
c.back() 返回容器 c 的最后一个元素的引用。如果 c 为空,则该操作未定义
c.front() 返回容器 c 的第一个元素的引用。如果 c 为空,则该操作未定义
c[n] 返回下标为 n 的元素的引用。如果 n <0 或 n >= c.size(),则该操作未定义。 只适用于 vector 和 deque 容器
c.at(n) 返回下标为 n 的元素的引用。如果下标越界,则该操作未定义。只适用于 vector 和 deque 容器
注意:a.使用越界的下标,或调用空容器的 front 或 back 函数,都会导致程序出现严重的错误。
b.我们注意到程序员必须保证在指定下标位置上的元素确实存在。下标操作符本身不会做相关的检查。
c.如果给出的下标无效,at 函数将会抛出 out_of_range 异常.
9.删除元素:
c.erase(p) 删除迭代器 p 所指向的元素.返回一个迭代器,它指向被删除元素后面的元素。如果 p 指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置。如果 p 本身就是指向超出末端的下一位置的迭代器,则该函数未定义
c.erase(b,e) 删除迭代器 b 和 e 所标记的范围内所有的元素,但不会删除e.返回一个迭代器,它指向被删除元素段后面的元素。实际上返回的就是e.如果 e 本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器的超出末端的下一位置
c.clear() 删除容器 c 内的所有元素。返回 void
c.pop_back() 删除容器 c 的最后一个元素。返回 void。如果 c 为空容器,则该函数未定义
c.pop_front() 删除容器 c 的第一个元素。返回 void。如果 c 为空容器,则该函数未定义.只适用于 list 或 deque 容器
注意:erase、pop_front 和 pop_back 函数使指向被删除元素的所有迭代器失效。对于 vector 容器,指向删除点后面的元素的迭代器通常也会失效。而对于 deque 容器,如果删除时不包含第一个元素或最后一个元素,那么该 deque 容器相关的所有迭代器都会失效。
10.赋值与 swap:
c1 = c2 删除容器 c1 的所有元素,然后将 c2 的元素复制给 c1。c1 和 c2 的类型(包括容器类型和元素类型)必须相同
c1.swap(c2) 交换内容:调用完该函数后,c1 中存放的是 c2 原来的元素,c2 中存放的则是 c1 原来的元素。c1 和 c2 的类型必须相同。该函数的执行速度通常要比将 c2 复制到 c1 的操作快
c.assign(b,e) 重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。b 和 e 必须不是指向 c 中元素的迭代器
c.assign(n,t) 将容器 c 重新设置为存储 n 个值为 t 的元素
注意:A.赋值和 assign 操作使左操作数容器的所有迭代器失效。swap 操作则不会使迭代器失效。完成 swap 操作后,尽管被交换的元素已经存放在另一容器中,但迭代器仍然指向相同的元素。
B.assign 操作首先删除容器中所有的元素,然后将其参数所指定的新元素插入到该容器中。与复制容器元素的构造函数一样,如果两个容器类型相同,其元素类型也相同,就可以使用赋值操作符(=)将一个容器赋值给另一个容器。如果在不同(或相同)类型的容器内,元素类型不相同但是相互兼容,则其赋值运算必须使用 assign 函数。例如,可通过 assign 操作实现将 vector 容器中一段 char* 类型的元素赋给 string 类型 list 容器。
C.由于 assign 操作首先删除容器中原来存储的所有元素,因此,传递给 assign 函数的迭代器不能指向调用该函数的容器内的元素。带有一对迭代器参数的 assign 操作允许我们将一个容器的元素赋给另一个不同类型的容器。
D.swap 操作实现交换两个容器内所有元素的功能。要交换的容器的类型必须匹配:操作数必须是相同类型的容器,而且所存储的元素类型也必须相同。调用了 swap 函数后,右操作数原来存储的元素被存放在左操作数中,反之亦然。
vector<string> svec1(10); // vector with 10 elements vector<string> svec2(24); // vector with 24 elementsvector<string>::iterator itor=svec1.begin();
vector<string>::iterator itor2=svec1.end();svec1.swap(svec2); //执行 swap 后,容器 svec1 中存储 24 个 string 类型的元素,而 svec2 则存储 10 个元素。//执行swap后,itor,itor2不失效,而是分别指向svec2中对应的元素
七.vector 容器的自增长:
1.为了使 vector 容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些。vector 容器预留了这些额外的存储区,用于存放新添加的元素。于是,不必为每个新元素重新分配容器。所分配的额外内存容量的确切数目因库的实现不同而不同。比起每添加一个新元素就必须重新分配一次容器,这个分配策略带来显著的效率。事实上,其性能非常好,因此在实际应用中,比起 list 和 deque 容器,vector 的增长效率通常会更高。
2.弄清楚容器的 capacity(容量)与 size(长度)的区别非常重要。size 指容器当前拥有的元素个数;而 capacity 则指容器在必须分配新存储空间之前可以存储的元素总数。
3.vector 容器处理内存分配的细节是其实现的一部分。然而,该实现部分是由 vector 的接口支持的。vector 类提供了两个成员函数:capacity 和 reserve 使程序员可与 vector 容器内存分配的实现部分交互工作。capacity 操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而 reserve 操作则告诉 vector 容器应该预留多少个元素的存储空间。
4.vector 的每种实现都可自由地选择自己的内存分配策略。然而,它们都必须提供 vector 和 capacity 函数,而且必须是到必要时才分配新的内存空间。分配多少内存取决于其实现方式。不同的库采用不同的策略实现。