C++primer小结11-12章

第十一章 关联容器

关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的。
与之相对,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。

关联容器支持高效的关键字查找和访问。两个主要的关联容器类型是mapset。map中的元素是一些关键字-值(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素只包含一个关键字;set支持高效的关键字查询操作——检查一个给定关键字是否在set中。

标准库提供8个关联容器:

map
set
multimap  //关键字可重复出现的map
multiset  //关键字可重复出现的set
unordered_map  //用哈希函数组织的map
unordered_set  //用哈希函数组织的set
unordered_multimap
unordered_multiset

11.2 关联容器概述

关联容器支持普通容器操作,不支持顺序容器的位置相关的操作。原因是关联容器中的元素是根据关键字存储的,这些操作对关联容器没有意义。而且关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
关联容器的迭代器都是双向的。

定义关联容器
对于set,元素类型就是关键字类型。
当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中:{key,value}。

关联容器对其关键字类型有一些限制。对于有序容器——map、multimap、set以及multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的 < 运算符来比较两个关键字。
传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。

可以向一个算法提供我们自己定义的比较操作,与之类似,也可以提供自己定义的操作来代替关键字上的 < 运算符。所提供的操作必须在关键字类型上定义一个严格弱序

用来组织一个容器中元素的操作也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出。
在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器时,才会以构造函数参数的形式提供真正的比较操作。

pair类型
一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样。
pair的默认构造函数对数据成员进行值初始化。
与其他标准库类型不同,pair的数据成员是public的,两个成员分别命名为first和second,用普通的成员访问函数来访问它们。map的元素是pair。标准库定义了有限的几个pair操作。
可以使用make_pair来生成pair对象,pair的两个类型来自于make_pair的参数。也可以对返回值进行列表初始化。

11.3 关联容器操作

关联容器额外的类型别名
key_type此容器类型的关键字类型
mapped_type每个关键字关联的类型;只适用于map
value_type对于set,与key_value相同;对于map,为pair<const key_type, mapped_type>

我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的。
与顺序容器一样,我们使用作用域运算符来提取一个类型的成员。

11.3.1关联容器迭代器

当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。
对map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。我们可以改变派人的值(value),但不能改变关键字成员的值(key)。
与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改。
map和set都支持begin和end操作,我们可以用这些函数获取迭代器,然后用迭代器来遍历容器。
我们通常不对关联容器使用泛型算法。

11.3.2 添加元素

关联容器的insert成员向容器中添加一个元素或一个元素范围。
对一个map进行insert操作时,必须记住元素类型是pair。通常,对于想要插入的数据,没有一个现成的pair对象。可以在insert的参数列表中创建一个pair。
insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。
++ret.first->second
向multiset或multimap添加元素。由于一个multi容器中的关键字不必唯一,在这些类型上调用insert总会插入一个元素。

11.3.3 删除元素

关联容器定义了三个版本的erase。与顺序容器一样,我们可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或者一个元素范围。
关联容器提供一个额外的erase操作,它接受一个key_type参数。此版本删除所有匹配给定关键字的元素,返回实际删除的元素的数量。
对于保存不重复关键字的容器,erase的返回值总是0或1。若返回值为0,则表明想要删除的元素不在容器中。
对于允许重复关键字的容器,删除元素的数量可能大于1.

11.3.4 map的下标操作

map和unordered_map提供了下标运算符和一个对应的at函数。
类似其他下标运算符,map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。
由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
map的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。

11.3.5 访问元素

关联容器提供多种查找一个指定元素的方法。应该使用哪个操作依赖于我们要解决什么问题。

c.find(k)  //返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
c.count(k)  //返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1.
c.lower_bound(k)  //返回一个迭代器,指向第一个关键字不小于k的元素
c.upper_bound(k)  //返回一个迭代器,指向第一个关键字大于k的元素
c.equal_range(k)  //返回一个迭代器pair,表示迭代器等于k的元素的范围。若k不存在,pair的两个成员均等于c.end()

对map使用find代替下标操作。
在multimap或multiset中查找元素。在容器中可能有很多元素具有给定的关键字。如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
当我们遍历一个multimap或multiset时,保证可以得到序列中所有具有给定关键字的元素。
用相同的关键字调用lower_bound和upper_bound会得到一个迭代器范围,表示所有具有该关键字的元素的范围。
lower_bound返回的迭代器可能指向一个具有给定关键字的元素,但也可能不指向。如果关键字不在容器中,则lower_bound会返回关键字的第一个安全插入点——不影响容器中元素顺序的插入位置。
如果lower_bound和upper_bound返回相同的迭代器,则给定关键字不在容器中。
equal_range,此函数接受一个关键字,返回一个迭代器pair。若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。

11.4 无序容器

无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型==运算符。

Tip:如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。

通常可以用一个无序容器替换对应的有序容器,反之亦然。但是由于元素未按顺序存储,一个使用无序容器的程序的输出(通常)会与使用有序容器的版本不同。

无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。
容器将具有一个特定哈希值的所有元素都保存在相同的桶中。无序容器的性能依赖于哈希函数的质量和桶的数量和大小。

无序容器提供了一组管理桶的函数。

桶接口
c.bucker_count()  //正在使用的桶的数目
c.max_bucket_count()  //容器能容纳的最多的桶的数量
c.bucket_size(n)  //第n个桶中有多少个元素
c.bucket(k)  //关键字为k的元素在哪个桶中
桶迭代
local_iterator
const_local_iterator
c.begin(n), c.end(n)
c.cbegin(n), c.cend(n)
哈希策略
c.load_factor()  //每个桶的平均元素数量,返回float值
c.max_load_factor()  //c试图维护的平均桶大小,返回float值
c.rehash(n)  //重组存储
c.reverse(n)  //重组存储

无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库提供了hash模板。因此,我们可以直接定义关键字是内置类型、string还是智能指针类型的无序容器。
但是我们不能直接定义关键字为自定义类型的无序容器。

第十二章 动态内存

静态内存用来保存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量。
栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。
对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆
程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。
动态对象的生存期由程序来控制,当动态对象不再使用时,代码必须显式地销毁它们。

12.1 动态内存与智能指针

C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
新的标准库提供了两种智能指针类型来管理动态对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

类似vector,智能指针也是模板。默认初始化的智能指针中保存着一个空指针。
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。
我们通常用auto定义一个对象来保存make_shared的结果。
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动地销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。析构函数一般用来释放对象所占用的资源。

当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。

Note:如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

程序使用动态内存出于以下三种原因之一:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

使用动态内存的一个常见原因是允许多个对象共享相同的状态。

直接管理内存
C++定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:int *pi = new int;
此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。

出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

动态分配的const对象
用new分配const对象是合法的:const int *pci = new const int(1024);
类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。

内存耗尽
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常。

释放动态内存
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象。
与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

动态对象的生存期直到被释放时为止
由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。
使用new和delete管理动态内存存在三个常见问题:

  1. 忘记delete内存,会导致人们常说的“内存泄漏”问题。
  2. 使用已经释放掉的对象。
  3. 同一块内存释放两次。

坚持只使用智能指针,就可以避免所有这些问题。

delete之后重置指针值。当我们delete一个指针后,指针值就变为无效。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针

shared_ptr和new结合使用。我们可以用new返回的指针来初始化智能指针。
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。

不要混合使用普通指针和智能指针
shared_ptr可以协调对象的析构,但这仅限于其自身(也是shared_ptr)的拷贝。这也是为什么我们推荐使用make_shared而不是new的原因。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr之上。

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。

get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或为另一个智能指针赋值。

我们可以用reset来将一个新的指针赋予一个shared_ptr。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。

智能指针和异常。
默认情况下,shared_ptr假定它们指向的是动态内存。当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。
注意:智能指针陷阱。
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范。

unique_ptr
一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。初始化unique_ptr必须采用直接初始化形式。
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。但可以通过调用release或reset将指针的所有权从一个unique_ptr转移给另一个unique。
release成员返回unique_ptr当前保存的指针并将其置为空。reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。
调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr。还可以返回一个局部对象的拷贝。

向unique_ptr传递删除器

weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。

12.2 动态数组

C++和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

new和数组
为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。

int *pia = new int[get_size()];  //pia指向第一个int

方括号中的大小必须是整型,但不必是常量。
也可以用一个表示数组类型的类型别名来分配一个数组:

typedef int arrT[42];
int *p = new arrT;

分配一个数组会得到一个元素类型的指针。由于分配的内存不是一个数组类型,因此不能对动态数组调用begin或end。
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。

动态分配一个空数组是合法的
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:

delete p;
delete [] pa;

数组中的元素按逆序销毁。当我们释放一个指向数组的指针时,空方括号对是必须的。

标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号。

unique_ptr<int[]> up(new int[10]);
up.release();

allocator类
new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。

类似vector,allocator是一个模板。为了定义一个allocator对象,必须指明这个allocator可以分配的对象类型。

allocator分配的内存是未构造的。我们按需要在此内存中构造对象。
还未构造对象的情况下就使用原始内存是错误的。
为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数。
我们只能对真正构造了的元素进行destroy操作。
一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成。
我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。

标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。

//这些函数在给定目的位置创建元素,而不是系统分配内存给它们。
uninitialized_copy(b, e, b2);
uninitialized_copy_n(b, n, b2);
uninitialized_fill(b, e, t);
uninitialized_fill_n(b, n, t);

类似copy,uninitialized_copy返回(递增后的)目的位置迭代器。因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。

12.3 使用标准库:文本查询程序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值