《C++ Primer》读书笔记—第九章 顺序容器
声明:
- 文中内容收集整理自《C++ Primer 中文版 (第5版)》,版权归原书所有。
- 学习一门程序设计语言最好的方法就是练习编程
元素在顺序容器中的顺序与其加入容器时的位置有关。标准库还定义了几种关联容器,关联容器中元素的位置由元素相关联的关键字值决定。
所有容器类都共享公共接口,不同容器按不同方式对其扩展。
一个容器就是一些特定类型对象的集合。顺序容器(sequential container)为程序员提供控制元素存储和访问顺序的能力。这些顺序不依赖元素的值,而是与元素加入容器时的位置对应。
一、顺序容器概述
1、所有的顺序容器都提供快速访问元素的能力,但以下性能有所折中:
·向容器添加或者删除元素的代价
·非顺序访问容器中元素的代价
2、顺序容器类型:
- vector,string: 快速随机访问,尾部增删快。有时添加一个元素时可能需要分配额外的存储空间。
- deque :双端队列,快速随机访问,头尾增删快
- array :大小是固定的,快速随机访问,不能增删
- list :双向链表,只支持双向顺序访问,任何位置增删都快
- forward_list 只支持单向顺序访问,任何位置增删都快,设计目标是达到与最好的手写的单向链表数据结构相当的性能。没有size操作。
3、选择容器的基本原则:
- 除非有更好的选择,否则应该用vector
- 如果程序有很多小元素,且空间额外开销很重要,则不要用list或forward_list
- 如果要求随机访问元素,则用vector或者deque
- 如果要求在容器中间插入或删除元素,但不会在中间位置进行插入或删除操作,则用deque
- 如果程序只有在读取操作时才需要在容器中见位置插入,随后需要随机访问元素,则:
- 首先确定是否真的需要在中间位置插入,当处理输入元素时,可以很容易向vector追加数据,然后调用标准库sort函数来重排,避免在中间位置添加元素。
- 如果必须,则考虑输入阶段用list,一旦输入完成,则将list中的元素拷贝到一个vector中。
4、如果既要随机访问元素,又要在容器中间插入元素,取决于list或forward_list中访问元素与vector或deque中插入、删除元素的相对性能。
5、如果不确定使用哪种容器,则可以在容器中只是用vector和list的公共操作:迭代器。不适用下标操作,避免随机访问。
二、容器库概览
1、容器均定义为模板类。例如对于vector,必须提供额外信息来生成特定的容器类型。对大多数容器,还需要额外提供元素的类型信息。
2、顺序容器可以保存任何类型的元素,特别的,可以定义一个容器,其元素的类型是另一个容器。
vector<vector<string>> lines // lines是一个vector,其元素类型时string的vector。
3、迭代器有公共的接口,如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。
例如,标准容器类型上所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作的。
4、一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者尾元素之后的位置。第二个迭代器指向的不是范围中最后一个元素,而是指向尾元素之后的位置。
称为左闭和区间(left-inclusive interval) [begin, end) end 和begin可以表示相同的位置,表示空范围。
5、end指向尾后元素,begin可以通过递增到达end
这种范围的约定使得我们能简单安全的遍历容器
1 while( begin != end ) 2 { 3 cout << *begin << endl; 4 ++begin; //每次循环 begin增加一次 5 }
连额外的判空操作都不需要了,若begin == end,则范围为空,循环不会执行
6、begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器,这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。
7、将一个容器创建为另一个容器的拷贝有两种方法:
- 直接拷贝整个容器(要求两个容器的容器类型和元素类型都匹配)
- 拷贝由迭代器对指定范围内的元素(只要求元素可以转换就可以了)
8、对一个容器列表初始化:
vector<const char*>articles = {"a","an","the"};
这样显式地指定了容器中每个元素的值。对于除array以外的容器类型,初始化列表还隐含的指定了容器的大小:容器包含于初始值一样多的元素。
9、顺序容器特有的构造函数:接收一个容器大小和一个(可选的)元素初始值。如果不提供元素初始值,则标准库会创建一个值初始化器。
1 vector<int> ivec(10, -1);//10个int元素,每个初始化为-1 2 list<string> svec(10, "hi");//10个string,每个初始化为hi 3 forward_list<int> ivec (10);//10个元素,每个都是初始化为0 4 deque<string> svec(10);//10个元素,每个都是空string
10、与内置数组相同,array的大小也是类型的一部分。定义一个array时,除了指定元素类型,还要指定容器大小。默认构造的array是非空的,它包含了一个与其大小一样多的元素。
11、对内置数组不能执行拷贝或对象赋值。但对array可以。array也要求初始值的类型与创建的容器类型相同。此外,array还要求元素类型和大小也相同。
1 array<int ,5>digits={0,1,2,3,4} 2 array<int .5>copy = digits;//正确,只要数组类型匹配即合法
12、顺序容器还定义了名为assign的操作。用参数所指的元素(的拷贝)替换左边容器中的所有元素。
如:可以用assign实现将一个vector中的一段char*值赋予一个list的string:
1 list<string>names; 2 vector<const char*> oldstyle; 3 names = oldstyle;//错误,容器类型不匹配 4 names.assign(oldstyle.cbegin(),oldstyle.cend());//正确,可以将const char*转化为string
- seq.assign(beg, end) 将迭代器范围内的元素赋值给容器
- seq.assign(il) 将初始化列表中的元素赋值给容器
- seq.assign(n,t) 将容器中的元素替换为n个值为t的元素
array也不支持assign。
13、如果两个容器类型相同,可以用swap交换他们的内容。
而且swap比赋值和assign这种元素拷贝操作要快的多,因为它只改变容器的数据结构。
----------itr1-----------
-----------↓-------------
svec1→{A0, A1, A2,A3}
svec2→{B0, B1, B2, B3, B4}
swap后
----------itr1-----------
-----------↓-------------
svec2→{A0, A1, A2,A3}
svec1→{B0, B1, B2, B3, B4}
swap只是容器的结构发生了改变,之前的迭代器等指向的元素不发生变化。只是元素属于另外一个容器了。
string是一个例外,调用swap会导致迭代器、引用和指针失效。
array更加特殊,它的swap是真正的元素交换。
----------itr1-----------
-----------↓-------------
arr1→{A0, A1, A2,A3}
arr2→{B0, B1, B2, B3, B4}
swap后
arr1→{B0, B1, B2, B3, B4}
arr2→{A0, A1, A2,A3}
-----------↑-------------
----------itr1-----------
14、只有当元素类型定义了相应的比较运算符时,才能使用关系运算符来比较两个容器。如果元素类型不支持所需运算符,则保存这种元素的容器不能使用相应的关系运算。
三、顺序容器操作
1、顺序容器和关联容器不同之处在于两者组织元素的方式。这些不同关系到了元素如何存储、访问、添加以及删除。
2、向vector、string或deque插入元素有可能使所有指向容器的迭代器、引用和指针失效。因为添加元素时可能引起整个对象空间的重新分配(旧的空间不足以容纳插入新元素后的对象)。
重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
3、使用添加、删除等容器操作时,首先考虑容器分配元素空间的策略。
4、push_back
向容器尾部添加元素。
除array和forward_list外,每个顺序容器都支持。(不需要强记,考虑容器分配元素空间的策略)
container.push_back(word);
当我们用一个对象来初始化容器,或将一个对象插入到容器中时,实际上放入的是对象值的一个拷贝,而不是对象本身。也不是对象的引用,两者之间没有任何关联。
push_front
将元素插入到容器头部。
仅list、forward_list和deque支持(不需要强记,考虑容器分配元素空间的策略)
1 for(size_t ix = 0; ix != 4; ++ix ) 2 { 3 ilist.push_front(ix); 4 }
每个元素都插入到容器的头部,最终形成逆序。执行完循环后,生成的list序列为3,2,1,0。
insert
在任意位置插入0个或多个元素
vector、deque、list和string都支持,forward_list有特殊版本的insert。
insert有多个重载函数
- insert(itr, t) 在itr所指位置之前插入t
- insert(itr, n, t) 在itr所指位置之前插入n个t
- insert(itr, itr1, itr2) 在itr所指位置之前插入[itr1,itr2)范围内的元素
- insert(itr, il) 在itr所指位置之前插入一个{}列表
共同特点:
- 第一个参数都是迭代器,用来指定插入位置
- 都将元素插入到给定迭代器所指位置之前
- 返回一个迭代器,指向新添加的第一个元素
insert函数也具备push_back和push_front的功能
1 list.insert(list.begin(), 1);//在首元素之前插入,即是push_front 2 list.insert(list.end(), 1);//在尾后元素之前插入,即是push_back
利用insert函数的返回值
1 list<string> lst; 2 auto iter = lst.begin(); 3 while( cin >> word ) 4 { 5 iter = lst.insert( iter, word ); 6 }
等价于一直在调用push_front。每步循环都将一个新元素插入到list首元素之前的位置。
emplace 将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
push和insert插入的都是现成的对象。
emplace则是创建一个对象插入到容器中。
因此,它的参数与容器元素类型的构造函数的参数一致.
1 struct Sales_data{ 2 Sales_data(const string &s, unsigned n, double p); 3 }; 4 5 list<Sales_data> c; 6 c.emplace("934", 25, 1.2);
使用emplace只需要记住这一区别就可以了,其他可以作类比。
1 push_front----emplace_front 2 push_back----implace_back 3 insert-----implace
5、返回值是对应位置元素的引用。
想要得到容器首尾位置的元素,可以:
1 auto val1 = *c.begin(); 2 auto last = c.end(); 3 auto val2 = *(--last);
更为简单的方法是使用front和back:
1 auto val1 = c.front(); 2 auto val2 = c.back();
注意使用之前需要判断容器是否为空。
at和下标相比,如果出现下标越界的情况,at会抛出一个out_of_range异常。
6、删除list中所有奇数元素
1 list<int> lst = {0,1,2,3,4,5,6,7,8,9}; 2 auto it = lst.begin(); 3 while( it != lst.end() ) 4 { 5 if( *it % 2 ) 6 { 7 it = lst.erase(it); 8 } 9 else 10 { 11 ++it; 12 } 13 }
7、接收两个迭代器的erase:删除一个范围内的元素
elem1 = slist.erase(elem1,elem2);//调用后,elem1 == elem2
8、删除所有元素:
1 slist.clear(); 2 slist.erase(slist.begin(),slist.end());
9、向容器中添加元素或者删除元素可能使指向容器元素的指针、引用或迭代器失效。必须保证每次改变容器操作之后都正确的重新定位迭代器。
10、循环,删除偶数元素,复制每个奇数元素
1 vector<int> vi = {0,1,2,3,4,5,6,7,8,9}; 2 auto itr = vi.begin(); 3 4 while( itr != vi.end() ) 5 { 6 if( *itr % 2 ) 7 { 8 itr = vi.insert( itr, *itr );//复制当前元素 9 itr += 2;//向前移动迭代器,跳过当前元素以及插入到它之前的元素 10 } 11 else 12 { 13 itr = vi.erase( itr );//删除偶数元素 不移动迭代器,itr指向我们删除的元素之后的元素 14 } 15 }
11、不要保存end返回的迭代器。不能在循环之前保存end返回的迭代器,必须每次调用后反复重新调用end。
四、vector对象是如何增长的
1、为了支持快速随机访问,vector中的元素是连续存储的(连续的内存空间)。
2、向vector和string中添加新元素时,如果没有空间容纳新元素,容器不可能简单的将它添加到内存中其他位置,因为元素必须是连续的存储。容器必须分配新的内存空间保存已有元素和新元素。
3、标准库提供的方法是,每次vector和string都会分配比新空间需求更大的内存空间。备用,保存更多的新元素。
4、capacity告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素,reserve操作允许我们通知容器它应该准备保存多少个元素。
5、shrink_to_fit要求deque,string,vector返回不需要的内存空间,此函数指出我们不需要任何多余的内存空间,这只是个请求,不保证一定会返回。
6、capacity指的是不分配新的内存的前提下它最多可以保存多少元素,size指它已经保存的元素的数目。
7、只有执行insert操作时size和capacity相等,或者调用resize或reserve时给定的大小超过当前capacity,vector才能重新分配内存空间。
8、不同实现采用不同的分配策略,但原则都是:确保用push_back向vector添加元素的操作有高效率。从技术的角度,就是通过在一个初始为空的vector上调用n次push_back来创建一个n个元素的vector,所花费的时间不超过n的常数倍。
9、shrink_to_fit 只适用于vector、string和deque。
capacity和reserve只适用于vector和string。
五、额外的string操作
1、substr
s.substr(pos, n);
返回一个string,包含s中从pos开始的n个字符的拷贝。
n的默认值为 s.size()-pos。
pos不能越界,pos+n可以,只拷贝到string末尾。
2、改变string的其他方法
·顺序容器的assign、insert、erase、赋值操作
·接受下标的insert和erase版本
·接受c风格字符数组的insert和assign版本
·append和replace函数。assign和append无须指定要替换string的哪个部分,assign总是替换string中的所有内容,append总是将新字符追加到string末尾。
3、string搜索操作
返回一个string::size_type类型的值,表示匹配发生位置的下标。
如果搜索失败,返回一个名为string::npos的static成员。
string::size_type是一个unsigned类型,应当避免使用int或其他带符号类型来保存搜索的返回值。
1 s.find(args) 2 s.find_first_of(args) 3 s.find_first_not_of(args)
args必须是以下形式这一:
- c, pos //从位置pos开始查找字符c,pos默认为0
- s2, pos //查找字符串s2,pos默认为0
- cp, pos //查找c风格字符串,pos默认为0
- cp, pos, n //查找指针cp指向数组的前n个字符, pos和n无默认值
1 find----rfind 2 find_first_of----find_last_of 3 find_first_not_of----find_last_not_of
4、可以传递给find操作一个可选的开始位置,这个可选的参数指出从哪个位置开始进行搜索。一种常见的设计模式是用这个可选参数在字符串中循环的搜索子字符串出现的位置。
1 string::size_type pos = 0; 2 //每步循环查找name中下一个数 3 while((pos = name.find_first_of(number,pos)) != string::npos){ 4 cout<<"found number at index: "<< pos << " element is " << name[pos] << endl; 5 +=pos;//移动到下一个字符 6 }
5、compare函数
1 s.compare(s2); //s和s2 2 s.compare(pos1, n1, s2);//s从pos1开始的n1个字符与s2比较 3 s.compare(pos1, n1, s2, pos2, n2); //s中从pos1开始的n1个字符与s2中从pos2开始的n2个字符进行比较 4 s.compare(cp); //比较s与cp指向的以空字符结尾的字符数组 5 s.compare(pos1, n1, cp); //将s中从pos1开始的n1个字符与cp指向的以空字符为结尾的字符数组进行比较 6 s.compare(pos1, n1, cp, n2);//将s中从pos1开始的n1个字符与指针cp指向的地址开始的n2个字符进行比较
6、数值转换
1 to_string(val); 2 3 //转换成整数 4 //p是第一个非数值字符的下标,默认值为0 5 //b是基数,默认值为10 6 stoi(s, p, b); 7 stol(s, p, b); 8 stoul(s, p, b); 9 stoull(s, p, b); 10 11 //转换成浮点 12 stof(s,p) 13 stod(s,p) 14 stold(s,P)
六、容器适配器
1、容器、迭代器和函数都有适配器。适配器是标准库中的一个通用概念。
它是一种机制,能使某种事物的行为看起来像另外一种事物一样。
实际上还是一个类。
顺序容器有三个适配器:stack、queue和priority_queue。
2、栈适配器
1 支持的操作 2 3 push //将元素压入栈 4 emplace //创建一个元素并压力栈 5 pop //删除栈顶元素 6 top //返回栈顶元素的值
3、队列适配器
queue 和 priority_queue
1 pop 2 front 3 back //仅queue支持 4 top //仅priority_queue支持 5 push 6 emplace