1.顺序容器概述
还有三种容器适配器:
std::stackstack; //栈 std::queue que; //队列 std::priority_queue pque; //优先队列
顺序存储:vector、array、string、deque 支持随机访问,插入删除慢(deque在内部插入删除慢) 链式存储:list、forward_list 不支持随机访问,插入删除快 forward_list和array是C++11新增类型,array比内置数组更安全更容易使用的大小固定的数组类型。 forward_list提供与最好的手写单向链表数据结构相当的性能,没有size()操作。 *****现代C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组*****
确定使用那种顺序容器
基本原则: 1.通常vector是最好的选择,除非你有很好的理由选择其他容器。 2.如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list。 3.如果程序要求随机访问元素,应使用vector或deque。 4.如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。 5.如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。 6.如果程序只有在读取输入时才需要在容器中间位置出入元素,随后需要随机访问元素,则: 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据, 然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中。 7.如果程序即要求随机访问元素,又需要在容器中间位置插入元素,那该怎么办? 取决于在list或forward_list中访问元素与vector或deque中插入或删除元素的相对性能。一般来说,应用中占主导 地位的操作决定了容器类型的选择。在此情况下,对两种容器分别测试应用的性能可能就是必要的了。 注:如果不确定应该使用哪种容器,可以在程序中只适用vector和list公共的操作:使用迭代器,不使用下标操作, 避免随机访问。这样在必要时旋转使用vector或list都很方便。
通用容器操作:
迭代器
如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。 *****迭代器范围***** 一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。 这两个迭代器通常被称为begin和end。 这种元素范围被称为左闭合区间:[begin,end) 表示范围自begin开始,于end之前结束。end不在begin之前 *****容器类型成员***** 反向迭代器:就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义也都发生了颠倒。
容器定义和初始化
只有顺序容器的构造函数才接受大小参数,关联容器并不支持
标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小: arrayarr; //3个默认初始化的int array arr2={0,1,2}; //列表初始化 array arr3={2}; //arr3[0]为2,其他为0 注:我们不能对内置数组进行拷贝或对象赋值操作,但array并无此限制: int arr[2] = {0,1}; int arr2[2] = arr; //错误:内置数组不支持拷贝或赋值 array arr3 = {3,4}; array arr4 = arr3; //正确:只有数组类型匹配即合法
赋值和swap
使用assign:仅顺序容器
vectorvec = { 1,2 }; vector vec2; vec2.assign(vec.begin(), vec.end()); //用迭代器范围赋值 vec2.assign(std::initializer_list {1, 3, 3}); //用初始化列表赋值 vec2.assign(3, 5); //赋值3个元素,值都是5
使用swap
vectorvec(10); vector vec2(20); swap(vec,vec2); //swap后,vec包含20个元素,vec2包含10个元素 除array外,交换两个容器内容的操作保证会很快--元素本身并未交换,swap只是交换了两个容器的内部数据结构。 元素不会被移动的事实意味着,出string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍 指向swap操作之前所执行的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。 例如:假定p在swap之前指向vec[1]的string,那么在swap之后它指向vec2[1]的元素。与其他容器不同,对一个string 调用swap会导致迭代器、引用和指针失效。 与其他容器不同,swap两个array会真正交换它们的元素。因此,对于array,在swap操作之后,指针、引用和迭代器所绑 定的元素保存不变,但元素值已经与另一个array中对应的元素的值进行了交换。 统一使用非成员版本swap是一个好习惯。非成员版本是新标准库(C++11)提供的,在泛型编程中是非常重要的。
容器大小操作:
1.成员函数size():返回容器中元素的数目 2.empty():当size为0时返回布尔值true,否则返回false 3.max_size():返回一个大于或等于该类型容器所能容纳的最大元素数的值。 forward_list不支持size
关系运算符:
每个容器都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=), 关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。 比较两个容器实际上是进行元素的逐对比较: 1.如果两个容器具有相同大小且所有元素都两两相等,则这两个容器相等;否则较小容器小于较大容器。 2.如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的元素,则较小容器小于较大容器。 3.如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。 vectorv1 = {1,3,5,7,9,12}; vector v2 = {1,3,9}; vector v3 = {1,3,5,7}; vector v4 = {1,3,5,7,9,12}; v1 顺序容器操作
*****容器元素是拷贝***** 当我们用一个 对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝, 而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。 *****使用insert的返回值***** 在新标准下,接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器,如果范围为空,不插入任何元素, insert操作会将第一个参数返回。 listlst; auto iter = lst.begin(); while(cin>>word) iter = lst.insert(iter,word); 等价于调用push_front,每次插入后迭代器都指向插入的新元素 *****使用emplace操作***** 新标准引入了三个新成员:emplace_front、emplace、emplace_back 这些操作构造而不是拷贝元素。是将参数传递给元素类型的构造函数,在容器中直接构造元素。 class A { public: A(string n,int a):name(n),age(a){} string name; int age; }; list lst; lst.emplace_back("xiao ming",18); 调用构造函数进行构造对象 lst.push_back("xiao ming",18); 错误,必须传对象 lst.push_back(A("xiao ming",18)); 正确 访问元素
获取首尾元素:要确保容器非空 1.直接调用front和back 2.间接方法:解引用begin返回的迭代器来获得首元素的引用,递减然后解引用end返回的迭代器来获得尾元素的引用。 *****访问成员函数返回的是引用***** 在容器中访问元素的成员函数(front、back、下标、at)返回的都是引用。 如果容器是一个const对象,则返回值是const的引用。 if(!c.empty()) { c.front() = 42; auto &v = c.back(); v = 1024; 引用可以改变容器中的值 auto v2 = c.back(); v2 = 0; v2是一个拷贝,为改变容器中的值 } *****下标操作和安全的随机访问***** 使用下标时,要保证下标有效,下标运算符并不检查下标是否在合法范围内。 可以使用at成员函数,如果下标越界,at会抛出一个out_of_range异常。删除元素
删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它是存在的特殊的forward_list操作
当在forward_list中添加或删除元素时,我们必须关注两个迭代器:一个指向我们要处理的元素,另一个指向其前驱。 forward_listflist={0,1,2,3,4,5}; auto prev = flist.before_begin(); 表示flist的首前元素 auto curr = flist.begin(); 表示flist的第一个元素 while(curr != flist.end()) { if(*curr % 2) curr = flist.erase_after(prev); 删除奇数并移动curr else { prev = curr; ++curr; } } 此例中,curr表示我们要处理的元素,prev表示curr的前驱。 改变容器大小
可以用resize来增大或缩小容器,array不支持resize。 如果当前大小大于所要求的大小,容器后部的元素会被删除; 如果当前大小小于新的大小,会将新元素添加到容器后部: listilist(1,2); ilist.resize(10); 将8个值为0的元素添加到容器末尾 ilist.resize(20,6); 将10个值为6的元素添加到容器末尾 ilist.resize(5); 删除容器后面15个元素 容器操作可能使迭代器失效
向容器添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。 一个失效的指针、引用或迭代器不再表示任何元素。 向容器添加元素后: 1.如果容器时vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。 如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后的元素迭代器指针引用无效。 2.对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效, 但指向存在的元素的引用和指针不会失效。 3.对于list和forward_list,指向容器的迭代器、指针和引用仍有效。 当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效。 删除一个元素后: 1.对于list和forward_list,指向容器其他位置的迭代器(包括尾后和首前迭代器)、引用和指针仍有效。 2.对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用和指针也会失效。 如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些 也不会受影响。 3.对于vector和string,指向被删除元素之前元素的迭代器、引用和指针仍有效,删除元素时,尾后迭代器总是会失效。 总结:添加删除对链式存储的数据结构影响更小,对顺序存储的数据结构影响较大。编写改变容器的循环程序
添加删除vector、string、或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步 中都更新迭代器、引用和指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,可以 用来更新: vectorv = {0,1,2,3,4,5,6,7,8,9}; auto iter = v.begin(); while(iter != v.end()) 更新end { if(*iter % 2) 奇数 { iter = v.insert(iter,*iter); 赋值当前元素,插入当前位置,返回指向新插入的元素 iter +=2; 移动迭代器,跳过当前元素以及插入到它前面的元素 } else iter = v.erase(iter); 删除偶数元素,返回值指向下一个元素 } 最后容器元素值:1133557799 不要保存end返回的迭代器
当我们添加、删除vector或string的元素后,或在deque中首元素之外任何位置添加删除元素后,原来end返回的迭代器总是失效。 因此添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器。 通常C++标准库的实现中end()操作都很快,部分就是因为这个原因。vector对象是如何增长的
看书理解下就行了,总之:容器容量不够就扩大容量,重新申请一块,把原来的元素拷贝过去。 shrink_to_fit只是一个请求,标准库并不保证退还内存。额外的string操作
const char* cp = "hello world!"; 以空字符结束的数组 char noNull[] = {'H','i'}; 不是以空字符‘ ’结束 string s1(cp); 拷贝cp中的字符直到遇到空字符,s1=="hello world!" string s2(noNull,2); 从noNull拷贝两个字符,s2=="Hi" string s3(noNull); 未定义:noNull不是以空字符结束 string s4(cp+6,5); 从cp[6]开始拷贝5个字符;s4=="world" string s5(s1,6,5); 从s1[6]开始拷贝5个字符;s5=="world" string s6(s1,6); 从s1[6]开始拷贝到末尾;s6=="world!" string s7(s1,6,20); 从s1[6]开始拷贝,只拷贝到末尾;s7=="world!" string s8(s1,16); out_of_range异常substr操作:pos超出string大小时会抛出out_of_range异常
改变string的其他方法
s.insert(s.size(),5, '!'); 在末尾插入5个感叹号 s.erase(s.size()-5, 5); 删除最后5个字符 const char* cp="Stately,plump Buck"; s.assign(cp,7); s=="Stately" s.insert(s.size(),cp+7); s=="Stately,plump Buck" string s="some string",s2="some other string"; s.insert(0,s2); 在s中位置0之前插入s2的拷贝 s.insert(0,s2,0,s2.size()); 在s[0]之前插入s2中s[0]开始的s2.size()个字符 s.append("abc"); 在末尾插入 s.replace(1,3,"ao"); 从位置1开始,删除3个字符并插入指定的字符aostring搜索操作
每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜素失败,则返回一个名为string::npos的 static成员。标准库将npos定义为一个const string::size_type类型,并初始化为值-1,。由于npos是一个unsigned类型, 此初始化意味着npos等于任何string最大的可能大小。compare函数:与C标准库strcmp函数很相似
数值转换一般情况,一个数的字符表示不同于其数值。例如: 数值15如果保存为16位的short类型,其二进制位模式为:00000000 00001111,而字符串"15"存为两个Lain-1编码的char 二进制位模式为:00110001 00110101。第一个字节表示字符‘1’,其八进制值为061,第二个字节表示‘5’,其八进制值为065 int i = 3; string s = to_string(i); double d = stod(s); 要转换为数值的string中第一个非空白字符必须是数值中可能出现的字符: string s2="pi = 3.14"; double d = stod(s2.substr(s2.find_first_of("+-.0123456789"))); 调用find_first_of获取s2中第一个可能是数值的一部分的字符的位置。 string参数中第一个非空白符必须是符号(+或-)或数字,也可以以0x或0X开头来表示的十六进制数。 对那些将字符串转换为浮点值的函数,string参数也可以以小数点开头,并可以包含e或E来表示指数部分。 对于那些将字符串转换为整型值的函数,根据基数不同,string参数可以包含字母字符,对应大于数字9的数。 如果string不能转换为一个数值,函数会抛出一个invalid_argument异常。 如果转换得到的数值无法用任何类型来表示,则抛出out_of_range异常。容器适配器
对于一个给定的适配器,可以使用哪些容器是有限制的,所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能 构造在array之上。类似的,我们也不能用forward_list来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾 元素的能力。 stack只要求push_back、pop_back、back操作,因此可以使用除array和forward_list之外的任何容器类型。 queue适配器要求back、push_back、front和push_front,因此它可以构造与list或deque之上,不能基于vector构造。 priority_queue出来front、push_back和pop_back操作之外还要求随机访问能力,因此它可以构造于vector或deque之上 但不能基于list构造。 ******************************************** 书上说的这些准则是基于标准库的实现。 stack不能基于array?queue不能基于vector构造? 自己实现当然可以,只不过效率没有用其他数据结构好。 如果我们自己实现stack、queue等这些适配器,底层数据结构可以随意选择,但要考虑效率。 ********************************************
栈适配器
这里书上又说queue也可以用vector实现,和前面矛盾,应该是前面的说法有问题priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。 默认情况下,标准库在元素类型上使用<运算符来确定相对优先级。后面学习如何重载这个默认设置。实际支持的操作有不同的地方 std::queueque; std::queue que2; queue支持的所有操作:没有top que.back(); que.emplace(); que.empty(); que.front(); que.pop(); que.push(1); que.size(); que.swap(que2); que._Get_container(); std::priority_queue pque; std::priority_queue pque2; priority_queue支持的所有操作:没有front、back pque.emplace(); pque.empty(); pque.pop(); pque.push(1); pque.size(); pque.swap(pque2); pque.top();



