《C++ Primer》学习笔记(九):顺序容器
顺序容器为程序员提供了控制元素存储和访问顺序的能力,这种顺序与元素加入容器时的位置相对应。而与之相对的,关联容器则是根据关键字的值来存储元素。
容器库概述
容器选择基本原则:
- 除非有合适的理由选择其他容器,否则应该使用
vector
。 - 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用
list
或forward_list
。 - 如果程序要求随机访问容器元素,则应该使用
vector
或deque
。 - 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用
deque
。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向 vector 追加数据,再调用标准库的 sort 函数重排元素,从而避免在中间位置添加元素。
- 如果必须在中间位置插入元素,可以在输入阶段使用
list
。输入完成后将 list 中的内容拷贝到vector
中。
- 不确定应该使用哪种容器时,可以先只使用
vector
和list
的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vector
或list
都很方便。
容器定义和初始化
为了创建一个容器为另一个容器的拷贝,两个容器的容器类型和元素类型都必须相同。传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
list<string> list2(authors); // 正确:类型匹配
deque<string> authList(authors); // 错误:容器类型不匹配
vector<string> words(articles); // 错误:容器类型必须匹配
// 正确:可以将const char*元素转换为string
forward_list<string> words(articles.begin(), articles.end());
定义和使用 array
类型时,需要同时指定元素类型和容器大小。
array<int, 42> // 类型为:保存42个int的数组
array<string, 10> // 类型为:保存10个string的数组
array<int, 10>::size_type i; // 数组类型包括元素类型和大小
array<int>::size_type j; // 错误:array<int>不是一个类型
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但是我们可以对 array
进行拷贝或赋值操作,前提是二者的元素类型和大小都相同。
int digs[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int cpy[10] = digs; //错误:内置数组类型不支持拷贝或赋值
array<int, 10> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> copy = digits;//正确:只要数组类型匹配即合法
赋值和swap
赋值运算符要求两侧的运算对象有相同的类型。而assign
允许我们从一个不同但相容的类型赋值,或者从一个容器的子序列赋值。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误: 容器类型不匹配
// 正确:可以将const char*转换为string
names.assign(oldstyle.cbegin(), oldstyle.cend());
注意:由于其旧元素被替换,因此传递给assign
的迭代器不能指向调用assign
的容器。
除 array
外,swap
不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。
vector<string> svec1(10); // 10个元素的vector
vector<string> svec2(24); // 24个元素的vector
swap(svec1, svec2);
与其他容器不同,对一个string
调用swap
会导致迭代器、引用和指针失效。
与其他容器不同,swap
两个array
会真正交换它们的元素。
新标准库同时提供了成员和非成员函数版本的 swap
。非成员版本的 swap
在泛型编程中非常重要,建议统一使用非成员版本的 swap
。
顺序容器操作
向顺序容器中添加元素
- 除了
array
和forward_list
之外,每个顺序容器(包括string
类型)都支持push_back
。 list
、forward_list
和deque
支持push_front
操作。vector
、deque
、list
和string
都支持insert
成员。forward_list
提供了特殊版本的insert
成员。
通过使用insert
的返回值,可以在容器中的一个特定位置反复插入元素:
list<string> lst;
auto iter = lst.begin();
string word;
while(cin >> word)
iter = lst.insert(iter, word); //等价于调用push_front
emplace
函数在容器中直接构造元素。传递给emplace
函数的参数必须与元素类型的构造函数相匹配。
// 在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));
访问元素
表中所述的访问元素操作返回的都是引用。如果容器是一个const
对象,则返回值是const
的引用。如果容器不是const
的,则返回的是普通引用。
删除元素
删除元素的成员函数并不检查其参数。在删除元素之前,程序用必须确保它(们)是存在的。
特殊的forward_list操作
在一个forward_list
中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。由于这些操作与其他容器上的操作的实现方式不同,forward_list
并未定义insert
、emplace
和erase
,而是定义了insert_after
、emplace_after
和erase_after
操作。例如图9.1所示,为了删除elem3
,应该用指向elem2
的迭代器调用erase_after
。为了支持这些操作,forward_list
也定义了before_begin
。它返回一个首前迭代器。
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;
}
}
改变容器大小
resize
函数接受一个可选的元素值参数,用来初始化添加到容器中的元素,否则新元素进行值初始化。如果容器保存的是类类型元素,且 resize
向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。
容器操作可能使迭代器失效
向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。
- 向容器中添加元素后:
- 如果容器是
vector
或string
类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。 - 如果容器是
deque
类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。 - 如果容器是
list
或forward_list
类型,指向容器的迭代器、指针和引用仍然有效。
- 如果容器是
- 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效, 对于其他元素:
- 如果容器是
list
或forward_list
类型,指向容器其他位置的迭代器、指针和引用仍然有效。 - 如果容器是
deque
类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。 - 如果容器是
vector
或string
类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。
- 如果容器是
建议:管理迭代器
当你使用迭代器(或指向容器元素的引用或指针)时,最小化要求迭代器必须保持有效的程序片段是一个好的方法。
由于向迭代器添加元素和从迭代器删除元素的代码可能会使选代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对 vector
、string
和 deque
尤为重要。
//傻瓜循环、删除偶数元素,复制每个奇数元素
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指向我们删除的元素之后的元素
}
}
注意:添加或删除元素的循环程序必须反复调用end
,而不能在循环之前保存end
返回的迭代器。
// 更安全的方法:在每个循环步添加/删除元素后都重新计算end
while (begin != v.end())
{
// 做一些处理
++begin; // 向前移动begin,因为我们想在此元素之后插入元素
begin = v.insert(begin, 42); // 插入新位
++begin; // 向前移动begin,跳过我们刚刚加入的元素
}
vector对象是如何增长的
为了减少容器空间重新分配次数的策略,当不得不获取新的内存空间时,vector
和string
的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间备用。
注意:reserve()
并不改变容器中元素的数量,它仅影响vector
预先分配多大的内存空间。只有当需要的内存空间超过当前容量时,reserve
调用才会改变vector
的容量。如果需求大小小于或等于当前容量,则reserve
什么也不做;特别是当需求大小小于当前容量时,容器不会退回内存空间(即调用reserve
永远不会减少容器占用的内存空间)。
- 容器的
size
是容器当前已经保存的元素数目。 - 容器的
capacity
是容器在不重新分配新的内存空间的前提下最多可以保存多少元素。
额外的string操作
构造string的其他方法
const char *cp = "Hello World!!!"; //以空字符结束的数组
char noNull[] = {'H', 'i'}; //不是以空字符结束
string s1(cp);//拷贝cp中的字符直到遇到空字符,s1="Hello World!!!"
string s2(noNull,2); //从noNull拷贝2个字符,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]开始拷贝,直至s1末尾;s6="World!!!"
string s7(s1, 6, 20);//正确,只拷贝到s1末尾,s7="World!!!"
string s8(s1, 16);//错误:抛出一个out_of_range异常
改变string的其他方法
string s("C++ Primer"), s2 = s; // 将s和s2初始化为"C++ Primer"
s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed."
s2.append(" 4th Ed."); // 等价方法:将" 4th Ed."追加到s2; s == s2
// 将"4th"替换为"5th"的等价方法
s.erase(11, 3); // s == "C++ Primer Ed."
s.insert(11, "5th"); // s == "C++ Primer 5th Ed."
// 从位置11开始,删除3个字符并插入"5th"
s2.replace(11, 3, "5th"); // 等价方法: s == s2
string搜索操作
string
的每个搜索操作都返回一个 string::size_type
值,表示匹配位置的下标。如果搜索失败,则返回一个名为 string::npos
的 static
成员。标准库将 npos
定义为 const string::size_type
类型,并初始化为-1
。
注意:string
搜索函数返回string::size_type
值,该值是一个unsigned
类型。因此用一个int
或其他带符号类型来保存这些函数的返回值不是一个好主意。
find
操作是从左向右搜索,而rfind
是从右向左搜索。
compare函数
string
类型提供了一组类似C标准库的 strcmp
函数的 compare
函数进行字符串比较操作。
数值转换
C++11
提供了实现数值数据与标准库string
类型之间的转换。
要转换为数值的string
中第一个非空白符必须是数值中可能出现的字符。进行数值转换时,string
参数的第一个非空白字符必须是符号(+
或-
)或数字。它可以以 0x
或 0X
开头来表示十六进制数。对于转换目标是浮点值的函数,string
参数也可以以小数点开头,并可以包含 e
或 E
来表示指数部分。
string s2 = "pi = 3.14";
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
如果给定的 string
不能转换为一个数值,则转换函数会抛出 invalid_argument
异常。如果转换得到的数值无法用任何类型表示,则抛出 out_of_range
异常。
容器适配器
标准库定义了 stack
、queue
和 priority_queue
三种顺序容器适配器。容器适配器可以改变已有容器的工作机制,使其行为看起来像一种不同的类型。
适配器是标准库中的一个通用概念,容器、迭代器和函数都有适配器。本质上,适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。
默认情况下,stack
和 queue
是基于 deque
实现的,priority_queue
是基于 vector 实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
// 在vector上实现的空栈
stack<string, vector<string>> str_stk;
// strstk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);
所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在 array
上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用 forward_list
构造适配器。
stack
可以使用除array
和forward_list
之外的任何容器类型来构造。queue
由于要求push_front
操作,不能使用vector
构造,可以构造于list
或deque
之上。priority_queue
由于需要支持随机访问迭代器,以始终在内部保持堆结构,故不能基于list
构造,可以构造与vector
或deque
上。
queue
和priority_queue
都定义在头文件queue中,queue
使用先进先出(first-in,first-out,FIFO)的存储和访问策略。priority_queue
允许我们为队列中的元素建立优先级,新加入的元素会排在所有优先级比它低的已有元素前。其支持的操作如下:
练习
- 下面的程序有何错误?你应该如何修改它?
list<int> lst1;
list<int>::iterator iter1 = lst1.begin(),
iter2 = lst1.end();
while (iter1 < iter2) /* ... */
list
是将元素以链表方式存储,两个指针的大小关系与它们指向的元素的前后关系并不一定是吻合的,实现 <
运算将会非常困难和低效。因此list
的迭代器不支持 <
运算,只支持递增、递减、=
以及 !=
运算。
- 编写程序,将一个
list
中的char
* 指针元素赋值给一个vector
中的string
。
#include<iostream>
#include<string>
#include<vector>
#include<list>
using namespace std;
int main()
{
list<char*> clist = { "hello", "world", "!" };
vector<string> svec;
// svec = clist;//错误:容器类型不同,不能直接赋值
// 元素类型相容,可采用范围初始化
svec.assign(clist.begin(), clist.end());
cout << svec.capacity() << " " << svec.size() << " " <<
svec[0] << " " << svec[svec.size() - 1] << endl;
system("pause");
return 0;
}
- 假定 c1 和 c2 是两个容器,下面的比较操作有何限制?
if (c1 < c2)
-
容器类型和元素类型必须相同。
-
元素类型必须支持 < 运算符。
- 假定
iv
是一个int
的vector
,下面的程序存在什么错误?你将如何修改?
vector<int>::iterator iter = iv.begin(),
mid = iv.begin() + iv.size() / 2;
while (iter != mid)
if (*iter == some_val)
iv.insert(iter, 2 * some_val);
循环中未对 iter
进行递增操作,iter
无法向中点推进。并且insert()
会使iter 和 mid
失效。
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main()
{
vector<int> iv = { 1, 1, 2, 1 };
int some_val = 1;
vector<int>::iterator iter = iv.begin();
int org_size = iv.size(), new_ele = 0;
//任何时候,iv.begin()+org_size/2+newele 都能正确指向 iv 原来的中央元素。
while (iter != (iv.begin() + org_size / 2 + new_ele))
{
if (*iter == some_val){
iter = iv.insert(iter, 2 * some_val);
new_ele++;
iter++; iter++;
}
else{
iter++;
}
}
for (iter = iv.begin(); iter != iv.end(); iter++){
cout << *iter << endl;
}
system("pause");
return 0;
}
- 编写程序,查找并删除
forward_list<int>
中的奇数元素。
在 forward_list
中插入、删除元素既需要该元素的迭代器,也需要前驱迭代器。为此,forward_list
提供了 before_begin
来获取首元素之前位置的迭代器,且插入、删除都是 after
形式,即删除(插入)给定迭代器的后继。
#include <iostream>
#include <forward_list>
using namespace std;
int main()
{
forward_list<int> iflst = { 1, 2, 3, 4, 5, 6, 7, 8 };
auto prev = iflst.before_begin();
auto curr = iflst.begin();
while (curr != iflst.end())
{
if (*curr & 1){
curr = iflst.erase_after(prev);
}
else{
prev = curr;
++curr;
}
}
for (curr = iflst.begin(); curr != iflst.end(); curr++){
cout << *curr << " ";
}
cout << endl;
system("pause");
return 0;
}
5. 编写程序,从一个 vector<char>
初始化一个 string
。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main()
{
vector<char> vc = { 'a', 'b', 'c', 'd', 'e' };
string s(vc.data(), vc.size());
cout << s << endl;
system("pause");
return 0;
}
- 编写一个函数,接受三个
string
参数是s
、oldVal
和newVal
。使用迭代器及insert
和erase
函数将s
中所有oldVal
替换为newVal
。测试你的程序,用它替换通用的简写形式,如,将"tho"替换为"though",将"thru"替换为"through"。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
void replace_string(string &s, const string &oldVal, const string &newVal)
{
auto old_size = oldVal.size();
if(oldVal.empty())
return;
auto iter1 = s.begin();
while(iter1 < s.end()){
auto iter2 = iter1;
auto iter3 = oldVal.begin();
while(iter3 != oldVal.end() && iter2 != s.end() && *iter2 == *iter3){
++iter2;
++iter3;
}
if(iter3 == oldVal.end()){
iter1 = s.erase(iter1, iter2);
if(!newVal.empty()){
auto iter4 = newVal.end();
do{
--iter4;
iter1 = s.insert(iter1, *iter4);
}while(iter4 > newVal.begin());
}
iter1 += newVal.size();
}else{
++iter1;
}
}
}
int main()
{
string s = "tho thru tho!";
replace_string(s, "thru", "through");
cout << s << endl;
replace_string(s, "tho", "though");
cout << s << endl;
replace_string(s, "through", "");
cout << s << endl;
system("pause");
return 0;
}
- 重写上一题的函数,这次使用一个下标和
replace
。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
void replace_string(string &s, const string &oldVal, const string &newVal)
{
auto old_size = oldVal.size();
if(oldVal.empty())
return;
auto match_pos = s.find(oldVal);
while(match_pos != string::npos){
s.replace(match_pos, oldVal.size(), newVal);
match_pos += newVal.size();
match_pos = s.find(oldVal, match_pos);
}
}
int main()
{
string s = "tho thru tho!";
replace_string(s, "thru", "through");
cout << s << endl;
replace_string(s, "tho", "though");
cout << s << endl;
replace_string(s, "through", "");
cout << s << endl;
system("pause");
return 0;
}
- 设计一个类,它有三个
unsigned
成员,分别表示年、月和日。为其编写构造函数,接受一个表示日期的string
参数。你的构造函数应该能处理不同的数据格式,如January 1,1900
、1/1/1990
、Jan 1 1900
等。
Date.h
:
#ifndef DATE_H_INCLUDED
#define DATE_H_INCLUDED
#include <iostream>
#include <string>
#include<stdexcept> //异常处理机制
using namespace std;
class Date{
public:
friend ostream& operator<<(ostream&, const Date&);
Date() = default;
Date(const string &ds);
unsigned get_year() const {return year;}
unsigned get_month() const {return month;}
unsigned get_day() const {return day;}
private:
unsigned year;
unsigned month;
unsigned day;
};
// 月份全称
const string month_name[] = { "January", "February", "March",
"April", "May", "June", "July", "August", "September",
"October", "November", "December" };
// 月份简写
const string month_abbr[] = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sept", "oct", "Nov", "Dec" };
// 每月天数
const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
inline unsigned month_parsing(const string& ds, size_t &end_pos){
if(ds.empty() || end_pos >= ds.size()){
throw invalid_argument("illegal date1");
}
int i = 0;
size_t j=0;
//首先检测是不是月份简写
for(i=0; i<12; ++i){
for(j=0; j<month_abbr[i].size(); ++j){
if(ds[j] != month_abbr[i][j]){
break;
}
}
if(j == month_abbr[i].size()){
break; //匹配简写成功
}
}
if(i==12){//匹配简写失败
throw invalid_argument("illegal date2");
}
if(ds[j] == ' '){ //空白符,则是简写
end_pos = j + 1;
return i+1;//返回对应月份
}else{//如果不是简写则继续匹配完整月份
for(; j<month_name[i].size(); ++j){
if(ds[j] != month_name[i][j]){
break;
}
}
if(j == month_name[i].size() && ds[j] == ' '){//匹配月份全称成功
end_pos = j + 1;
return i+1;
}
}
throw invalid_argument("illegal date3");;//匹配简写和全称均失败
}
inline unsigned day_parsing(string&ds, unsigned month, size_t &p){
size_t q;
int day = stoi(ds.substr(p), &q); // 从p开始的部分转换为日期值,
if (day<1 || day>days[month])
throw invalid_argument("illegal date4");
p += q;//移动到日期值之后
return day;
}
inline unsigned year_parsing(string &ds, size_t &p){
size_t q;
int year = stoi(ds.substr(p), &q); // 从p开始的部分转换为年
if (p + q < ds.size())
throw invalid_argument("illegal ending5");
return year;
}
Date::Date(const string&ds){
string s = ds;
auto pos1 = s.find_first_of("0123456789");//返回第一个数字的位置
if(pos1 == string::npos){
throw invalid_argument("illegal date6");
}
if(pos1 > 0){ //如果string的第一个不是数字,则证明是月份的英文全称或缩写
month = month_parsing(s, pos1); //返回月份值,并在函数内修改pos1为day的下一位的索引
day = day_parsing(s, month, pos1);
if (s[pos1] != ' ' && s[pos1] != ',')
throw invalid_argument("illegal spacer7");
++pos1;
year = year_parsing(s, pos1);
}else{ //string中的月份也是数字格式
size_t pos2 = 0;
month = stoi(s, &pos2); //获取月份,并将月份的下一位的索引保存在pos2中
pos1 = pos2;
if (month<1 || month >12)
throw invalid_argument("not a legal month value8");
if (s[pos1++] != '/')
throw invalid_argument("illegal spacer9");
day = day_parsing(s, month, pos1);
if (s[pos1++] != '/')
throw invalid_argument("illegal spacer10");
year = year_parsing(s, pos1);
}
}
ostream & operator<<(ostream& out, const Date& d){
out << "year:" << d.get_year() << " month:" << d.get_month() << " day:" << d.get_day() << endl;
return out;
}
#endif // DATE_H_INCLUDED
main.cpp
:
#include <iostream>
#include <string>
#include "Date.h"
using namespace std;
int main()
{
string dates[] = { "Jan 1, 2014", "February 1 2014", "3/1/2014", "3 1 2014"
//"Jcn 1,2014",
//"Janvary 1,2014",
//"Jan 32,2014",
//"Jan 1/2014",
};
try{
for (auto ds : dates){
date dl(ds);
cout << dl;
}
}
catch (invalid_argument e){
cout << e.what() << endl;
}
system("pause");
return 0;
}