C++ primer第九章—顺序容器

9.1 顺序容器概述

  1. 一个容器就是一些特定类型对象的集合。顺序容器(sequential container) 为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
  2. 顺序容器类型:
    在这里插入图片描述
  3. 除了固定大小的 array 外,其他容器都提供高效、灵活的内存管理。例如:
  • string 和 vector 将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入/删除位置之后的所有元素,来保持连续存储。而且添加一个元素有时可能还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。
  • list 和 forward_list 两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且与 vector 、deque 和array 相比, 这两个容器的额外内存开销也很大。
  • deque 是一个更为复杂的数据结构。与 string 和 vector 类似,deque 支持快速的随机访问。与 strìng 和 vector 一样,在 deque 的中间位置添加或删除元素的代价(可能)很高。但是,在 deque 的两端添加或删除元素都是很快的,与 list 或forward_list 添加删除元素的速度相当。
  1. C++程序应该使用标准容器库,而不是更原始的数据结构,例如内置数组
  2. array 对象的大小是固定的,forward_list 没有 size 操作。
  3. 容器选择原则:
  • 除非有合适的理由选择其他容器,否则应该使用 vector。
  • 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list。
  • 如果程序要求随机访问容器元素,则应该使用 vector 或 deque。
  • 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用 deque。
  • 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
    (1)先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向 vector 追加数据,再调用标准库的 sort 函数重排元素,从而避免在中间位置添加元素。
    (2)如果必须在中间位置插入元素,可以在输入阶段使用 list。输入完成后将 list 中的内容拷贝到 vector 中。
  • 不确定应该使用哪种容器时,可以先只使用 vector 和 list 的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择 vector 或 list 都很方便。

9.2 容器库概览

  1. 每个容器都定义在一个头文件中,文件名与类型名相同。容器均为模板类型。
  2. 顺序容器构造函数的一个版本接受容器大小参数,使用了元素类型的默认构造函数,如果有的类,没有默认构造函数,不能只传递给他一个元素数目参数
// 假定noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10, init);  // 正确:提供了元素初始化器
vector<noDefault> v1(10);     // 错误:必须提供一个元素初始化器
  1. 容器操作
    **在这里插入图片描述**
    在这里插入图片描述
    在这里插入图片描述

9.2.1 迭代器

  1. forward_list 类型不支持递减运算符 --。
while (begin != end)
{
    *begin = val;   // 正确:范围非空,因此begin指向一个元素
    ++begin;    	// 移动迭代器,获取下一个元素
}

9.2.2 容器类型成员

  1. 通过类型别名,可以在不了解容器元素类型的情况下使用元素。如果需要元素类型,可以使用容器的 value_type。如果需要元素类型的引用,可以使用 reference 或 const_reference。
  2. 为了使用这些类型,我们必须显式使用其类名
// iter是通过list<string>定义的一个迭代器类型
list<string>::iterator iter;
// count是通过vector<int>定义的一个difference_type类型
vector<int>::difference_type count;

9.2.3 begin和end成员

  1. begin 和 end 操作有多个版本:带 r 的版本返回反向迭代器。以 c 开头的版本(C++11新增)返回 const 迭代器。不以 c 开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对 const 对象调用时,返回 const 迭代器。
list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin();    // list<string>::iterator
auto it2 = a.rbegin();   // list<string>::reverse_iterator
auto it3 = a.cbegin();   // list<string>::const_iterator
auto it4 = a.crbegin();  // list<string>::const_reverse_iterator
  1. 以 c 开头的版本是C++新标准引入的,用以支持 auto 与 begin 和 end 函数结合使用。
  2. 当 auto 与 begin 或 end 结合使用时,返回的迭代器类型依赖于容器类型。但调用以 c 开头的版本仍然可以获得 const 迭代器,与容器是否是常量无关。
  3. 当程序不需要访问时,应该使用 cbegin 和 cend。

9.2.4 容器定义和初始化

在这里插入图片描述

  1. 将一个新容器创建为另一个容器的拷贝的方法有两种: 可以直接拷贝整个容器,或者(array 除外)拷贝由一个迭代器对指定的元素范围。
  2. 将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。
// 每个容器有三个元素,用给定的初始化器进行初始化
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());
  1. C++11允许对容器进行列表初始化。
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
  1. 只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
  2. 定义和使用 array 类型时,需要同时指定元素类型和容器大小。
array<int, 42>      // 类型为:保存42个int的数组
array<string, 10>   // 类型为:保存10个string的数组
array<int, 10>::size_type i;   // 数组类型包括元素类型和大小
array<int>::size_type j;       // 错误:array<int>不是一个类型
  1. 对 array 进行列表初始化时,初始值的数量不能大于 array 的大小。如果初始值的数量小于 array 的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。
  2. 可以对 array 进行拷贝或赋值操作,但要求二者的元素类型和大小都相同(不能对内置数组进行拷贝和赋值操作)。
int digs[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int coy[10] = digs;     // 错误:内置数组不支持拷贝和赋值
array<int, 10> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> copy = digits;      // 正确:只要数组类型匹配即合法

9.2.5 赋值和swap

在这里插入图片描述

  1. 赋值运算符两侧的运算对象必须类型相同。assign 允许用不同但相容的类型赋值,或者用容器的子序列赋值。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle;   // 错误: 容器类型不匹配
// 正确:可以将const char*转换为string
names.assign(oldstyle.cbegin(), oldstyle.cend());


// assign的第二个版本
// 等价于slist1.clear();
// 后跟slist1.insert(slist1.begin(), 10, "Hiya!");
list<string> slist(1);
slist1.assign(10, "Hiya!");
  1. 由于其旧元素被替换,因此传递给 assign 的迭代器不能指向调用 assign 的容器本身。
  2. swap 交换两个相同类型容器的内容:除 array 外,swap 不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成(常数时间)。
vector<string> svec1(10);   // 10个元素的vector
vector<string> svec2(24);   // 24个元素的vector
swap(svec1, svec2);
  1. 对于 array,swap 会真正交换它们的元素(时间与array中元素的数目成正比)。因此在 swap 操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。对于其他容器类型(除 string),指针、引用和迭代器在 swap 操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。新标准库同时提供了成员和非成员函数版本的 swap。非成员版本的 swap 在泛型编程中非常重要,建议统一使用非成员版本的 swap。
  2. 对一个string调用swap会导致迭代器、引用和指针失效
  3. 统一使用非成员版本的swap是一个好习惯

9.2.6 容器大小操作

  1. size 成员返回容器中元素的数量;
  2. empty 当 size 为0时返回 true,否则返回 false;
  3. max_size 返回一个大于或等于该类型容器所能容纳的最大元素数量的值。
  4. forward_list 支持 max_size 和 empty,但不支持 size。

9.2.7 关系运算符

  1. 每个容器类型都支持相等运算符(==、!=)。除无序关联容器外,其他容器都支持关系运算符(>、>=、<、<=)。关系运算符两侧的容器类型和保存元素类型都必须相同。
  2. 两个容器的比较实际上是元素的逐对比较,其工作方式与string的关系运算符类似:
  • 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。
  • 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
  • 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。
  1. 容器的相等运算符实际上是使用元素的 == 运算符实现的,而其他关系运算符则是使用元素的 < 运算符。如果元素类型不支持所需运算符,则保存该元素的容器就不能使用相应的关系运算。

9.3 顺序容器操作

  1. 除 array 外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。

9.3.1 向顺序容器添加元素

  1. 注意向vector、string或deque插入元素会使所有指向容器的迭代器,引用和指针失效
    在这里插入图片描述
  2. push_back 将一个元素追加到容器尾部,push_front 将元素插入容器头部。除 array 和 forward_list 之外, 每个顺序容器(包括 string 类型〉都支持 push_back。
    在这里插入图片描述
  3. insert 将元素插入到迭代器指定的位置之前。一些不支持 push_front 的容器可以使用 insert 将元素插入开始位置。
vector<string> svec;
list<string> slist;
// 等价于调用slist.push_front("Hello!");
slist.insert(slist.begin(), "Hello!");
// vector不支持push_front,但我们可以插入到begin()之前
// 警告:插入到vector末尾之外的任何位置都可能很慢
svec.insert(svec.begin(), "Hello!");
  1. 将元素插入到 vector、deque 或 string 的任何位置都是合法的,但可能会很耗时。
  2. 插入范围内元素:注意迭代器表示要拷贝的范围,不能指向与目标位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end());  // 错误
  1. 在新标准库中,接受元素个数或范围的 insert 版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回 void。如果范围为空,不插入任何元素,insert 会返回第一个参数。
list<string> 1st;
auto iter = 1st.begin();
while (cin >> word)
    iter = 1st.insert(iter, word);  // 等价于调用push_front
  1. 新标准库增加了三个直接构造而不是拷贝元素的操作:emplace_front、emplace_back 和 emplace,其分别对应 push_front、push_back 和 insert。当调用 push或insert 时,元素对象被拷贝到容器中。而调用 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));
  1. emplace 函数在容器中直接构造元素。传递给 emplace 函数的参数必须与元素类型的构造函数相匹配。

9.3.2 访问元素

  1. 每个顺序容器都有一个 front 成员函数,而除了 forward_list 之外的顺序容器还有一个 back 成员函数。这两个操作分别返回首元素和尾元素的引用。
  • 迭代器 end 指向的是容器尾元素之后的(不存在的)元素。为了获取尾元素,必须首先递减此迭代器;
  • 在调用front和back之前(或解引用 begin 和 end 返回的迭代器)之前,要确保容器非空。
    在这里插入图片描述
  1. 在容器中访问元素的成员函数都返回引用类型。如果容器是 const 对象,则返回 const 引用,否则返回普通引用。
if (!c.empty()){
	c.front() = 42;   // 将42赋予c中的第一个元素
	auto &p = c.back();    // 获得指向最后一个元素的引用
	p = 1024;              // 改变c中的元素
	auto p2 = c.back();     // p2不是一个引用,它是一个c.back()的拷贝
	p2 = 0;           // 未改变c中的元素
}
  1. 可以快速随机访问的容器(string、vector、deque 和 array)都提供下标运算符。保证下标有效是程序员的责任。
  2. 如果希望确保下标合法,可以使用 at 成员函数。at 类似下标运算,但如果下标越界,at 会抛出 out_of_range 异常。
vector<string> svec;  // 空vector
cout << svec[0];      // 运行时错误:svec中没有元素!
cout << svec.at(0);   // 抛出一个out_of_range异常

9.3.3 删除元素

在这里插入图片描述

  1. erase 函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的 erase 都返回指向删除元素(最后一个)之后位置的迭代器。
// 删除两个迭代器表示的范围内的元素
// 返回指向最后一个被删元素之后位置的迭代器
elem1 = slist.erase(elem1, elem2);  // 调用后,elem1 == e1em2
  1. clear 函数删除容器内的所有元素。
slist.clear(); 								// 删除容器中所有元素
slist.erase(slist.begin(), slist.end()); 	// 等价调用

9.3.4 特殊的forward_list操作

  1. 在 forward_list 中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。
    在这里插入图片描述

  2. 由于这些操作与其他容器上的操作的实现方式不同,forward_list 并未定义 insert、emplace 和 erase,而是定义了名为 insert_after、emplace_after和 erase after 的操作。
    在这里插入图片描述

forward_list<int> flst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto prev = flst.before_begin();
auto curr = flst.begin();
while(curr!=flst.end()){
	if(*curr % 2){
		curr = flst.erase_after(prev);
	}else{
		prev = curr;
		++curr;
	}
}

9.3.5 改变容器大小

  1. 顺序容器的大小操作:
    在这里插入图片描述
list<int> ilist(10, 42);      // 10个int;每个的值都是42
ilist.resize(15);             // 将5个值为0的元素添加到ilist的末尾
ilist.resize(25,-1);          // 将10个值为-1的元素添加到ilist的末尾
ilist.resize(5);              // 从ilist末尾删除20个元素

9.3.6 容器操作可能是迭代器失效

  1. 向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。
  • 向容器中添加元素后:
    (1)如果容器是 vector 或 string 类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。
    (2)如果容器是 deque 类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。
    (3)如果容器是 list 或 forward_list 类型,指向容器的迭代器、指针和引用仍然有效。
  • 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效:
    (1)如果容器是 list 或 forward_list 类型,指向容器其他位置的迭代器、指针和引用仍然有效。
    (2)如果容器是 deque 类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。
    (3)如果容器是 vector 或 string 类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。
// 傻瓜循环,删除偶数元素,复制每个奇数元素
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);          // 删除当前元素
	}
}
  1. 如果在一个循环中插入/删除 deque、string 或 vector 中的元素,不要缓存 end 返回的迭代器。
// 更安全的方法:在每个循环步添加/删除元素后都重新计算end
while (begin != v.end())
{
    // 做一些处理
    ++begin;    // 向前移动begin,因为我们想在此元素之后插入元素
    begin = v.insert(begin, 42);    // 插入新位
    ++begin;    // 向前移动begin,跳过我们刚刚加入的元素
}

9.4 vector对象是如何增长

  1. vector 和 string 的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。
  2. 容器大小管理操作:
    在这里插入图片描述
  3. reserve 并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间。
  4. capacity 函数返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。reserve 函数告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小。
  5. 只有当需要的内存空间超过当前容量时,reserve 才会真正改变容器容量,分配不小于需求大小的内存空间。当需求大小小于当前容量时,reserve 并不会退回内存空间。因此在调用 reserve 之后,capacity 会大于或等于传递给 reserve 的参数。
  6. 每个 vector 实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。

9.5 额外的string操作

  1. string类型的额外操作大部分要么是为了提供string类和C风格字符数组之间的相互转换,要么是为了允许我们用下标代替迭代器的版本

9.5.1 构造string的其他方法

在这里插入图片描述
在这里插入图片描述

9.5.2 改变string的其他方法

在这里插入图片描述
在这里插入图片描述

  1. append 函数是在 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
  1. replace 函数是调用 erase 和 insert 函数的简写形式。
// 将"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

9.5.3 string搜索操作

  1. string 的每个搜索操作都返回一个 string::size_type 值,表示匹配位置的下标。如果搜索失败,则返回一个名为 string::npos 的 static 成员。标准库将 npos 定义为 const string::size_type 类型,并初始化为-1。
  2. 不建议用 int 或其他带符号类型来保存 string 搜索函数的返回值。
    在这里插入图片描述
    在这里插入图片描述

9.5.4 compare函数

  1. string 类型提供了一组 compare 函数进行字符串比较操作,类似C标准库的 strcmp 函数。
    在这里插入图片描述

9.5.5 数值转换

  1. C++11增加了 string 和数值之间的转换函数:
    在这里插入图片描述
  2. 进行数值转换时,string 参数的第一个非空白字符必须是符号(+ 或 -)或数字。它可以以 0x 或 0X 开头来表示十六进制数。对于转换目标是浮点值的函数,string 参数也可以以小数点开头,并可以包含 e 或 E 来表示指数部分。
  3. 如果给定的 string 不能转换为一个数值,则转换函数会抛出 invalid_argument 异常。如果转换得到的数值无法用任何类型表示,则抛出 out_of_range 异常。

9.6 容器适配器

  1. 标准库定义了 stack、queue 和 priority_queue 三种容器适配器。容器适配器可以改变已有容器的工作机制。
  2. 所有容器适配器都支持的操作和类型:
    在这里插入图片描述
  3. 默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是基于 vector 实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
// 在vector上实现的空栈
stack<string, vector<string>> str_stk;
// strstk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);
  1. 所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在 array 上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用 forward_list 构造适配器。
  2. 栈适配器stack定义在头文件stack中,其支持的操作如下:
    在这里插入图片描述
  3. 队列适配器queue和priority_queue定义在头文件queue中,其支持的操作如下:
    在这里插入图片描述
    在这里插入图片描述
  4. 优先队列允许我们为队列中的元素建立优先级,进加入的元素会排在所有优先级比它低的已有元素之前,标准库在元素类型上使用<运算符来确定相对优先级。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值