C++标准库-学习笔记

目录

1 IO类

1.1 IO对象无拷贝或赋值

1.2 条件状态

1.3 查询流的状态

1.4 管理输出缓冲

1.4.1 刷新输出缓冲区

1.4.2 unitbuf操纵符

1.4.3 关联输入和输出流

1.5 文件输入输出

1.5.1 使用文件流对象

1.5.2 文件模式

1.6 string流

2 序容器

2.1 容器库概览

2.2 迭代器

2.2.1 迭代器范围

2.2.2 使用左闭合范围蕴含的编程假定

2.2.3 将一个容器初始化为另一个容器的拷贝

2.2.4 标准库array具有固定大小

2.2.5 赋值和swap

2.3 顺序容器操作

2.3.1 容器元素是拷贝

2.3.2 使用emplace操作

2.3.3 删除元素

2.3.4 Vector对象是如何增长

2.4 容器适配器

2.4.1 定义一个适配器

3 泛型算法

3.1 关键概念:算法永远不会执行容器的操作

3.2 初识泛型算法

3.2.1 算法不检查写操作

3.2.2 back_inserter

3.2.3 拷贝算法

3.2.4 重排容器元素的算法

3.3 定制操作

3.3.1 谓词

3.3.2 lambda表达式

3.4 参数绑定

3.4.1 标准库bind函数

4 关联容器

4.1 使用关联容器

4.1.1 使用map

4.2 关联容器概述

4.2.1 定义关联容器

4.3 关联容器操作

4.3.1 关联容器额外的类型别名

4.3.2 map的下标操作

4.3.3 访问元素

4.4 无序容器

4.4.1 管理桶

5 动态内存

5.1 shared_ptr

5.1.1 make_shared函数

5.1.2 shared_ptr的拷贝和赋值

5.1.3 直接管理内存

5.1.4 shared_ptr和new结合使用

5.1.5 不要使用get初始化另一个智能指针或为智能指针赋值

5.1.6 智能指针和哑类

5.2 unique_ptr

5.3 weak_ptr

5.4 动态数组

5.4.1 new和数组

5.4.2 allocator类


         C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。

1 IO类

1.1 IO对象无拷贝或赋值

        由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

1.2 条件状态

        IO操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。下表列出了IO类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态(condition state)。

C++标准库-学习笔记_第1张图片

        一个流一旦发生错误,其上后续的IO操作都会失败。只有当一个流处于无错状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用:

while (cin >> word) 
    // ok:读操作成功

1.3 查询流的状态

        从上述表中可知,使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而eof和bad操作只能表示特定的错误。

1.4 管理输出缓冲

        每个输出流都管理一个缓冲区,用来保存程序读写的数据。

        文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。

        导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

      • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
      • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
      • 我们可以使用操纵符如endl来显式刷新缓冲区。
      • 在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的
      • 一个输出流可能被关联到另一个流在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。

1.4.1 刷新输出缓冲区

        flush刷新缓冲区,但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区。

1.4.2 unitbuf操纵符

        如果想在每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符。它告诉流在接下来的每次写操作之后都进行一次flush操作。而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。

警告:

    如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。

    当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。

1.4.3 关联输入和输出流

        当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将cout和cin关联在一起。

        tie有两个重载的版本:一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o。

        我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream:

cin.tie(&cout); // 仅仅是用来展示:标准库将cin和cout关联在一起
// old_tie指向当前关联到的cin流(如果有的话) 
ostream *old_tie = cin.tie(nullptr); // cin不再与其他流关联 
// 将cin与cerr关联,这不是一个好主意,因为cin应该关联到
cout cin.tie(&cerr); // 读取cin会刷新cerr而不是cout 
cin.tie(old_tie); // 重建cin和cout间的正常关联

        在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了tie。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream。

1.5 文件输入输出

        头文件fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。

1.5.1 使用文件流对象

        当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。

        创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则open会自动被调用

ifstream in(iFile); // 构造一个ifstream并打开给定文件 
ofstream out; // 输出文件流未关联到任何文件

1.5.1.1 成员函数open和close

        如果调用open失败,failbit会被置位。因为调用open可能失败,进行open是否成功的检测通常是一个好习惯:

ofstream out("a.txt"); 
if (out) 
    // do something

        一旦一个文件流已经打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用open会失败,并会导致failbit被置位。随后的试图使用文件流的操作都会失败。为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。一旦文件成功关闭,我们可以打开新的文件。

        当一个fstream对象被销毁时,close会自动被调用

1.5.2 文件模式

        每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。

C++标准库-学习笔记_第2张图片

        默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,即打开文件同时进行读写操作。

        ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。

1.6 string流

        sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是一个IO流一样。

#include 
#include 
int main()
{
    // default constructor (input/output stream)
    std::stringstream buf1;
    buf1 << 7;
    int n = 0;
    buf1 >> n;
    std::cout << "buf1 = " << buf1.str() << " n = " << n << '\n';
 
    // input stream
    std::istringstream inbuf("-10");
    inbuf >> n;
    std::cout << "n = " << n << '\n';
 
    // output stream in append mode (C++11)
    std::ostringstream buf2("test", std::ios_base::ate);
    buf2 << '1';
    std::cout << buf2.str() << '\n';
}

2 序容器

2.1 容器库概览

        一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。与之相对的,有序和无序关联容器,则根据关键字的值来存储元素。

        一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。即,deque定义在头文件deque中,list定义在头文件list中,以此类推。容器均定义为模板类。例如对vector,我们必须提供额外信息来生成特定的容器类型。对大多数,但不是所有容器,我们还需要额外提供元素类型信息:

list; 
deque;

        顺序容器主要有:

C++标准库-学习笔记_第3张图片

2.2 迭代器

        与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。例如,标准容器类型上的所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作的。类似的,标准库容器的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。

2.2.1 迭代器范围

        一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置(one past the last element)。这两个迭代器通常被称为begin和end,或者是first和last(可能有些误导),它们标记了容器中元素的一个范围。

        虽然第二个迭代器常常被称为last,但这种叫法有些误导,因为第二个迭代器从来都不会指向范围中的最后一个元素,而是指向尾元素之后的位置。迭代器范围中的元素包含first所表示的元素以及从first开始直至last(但不包含last)之间的所有元素。这种元素范围被称为左闭合区间(left-inclusive interval)。

        迭代器begin和end必须指向相同的容器。end可以与begin指向相同的位置,但不能指向begin之前的位置

2.2.2 使用左闭合范围蕴含的编程假定

        标准库使用左闭合范围是因为这种范围有三种方便的性质。假定begin和end构成一个合法的迭代器范围,则

      • 如果begin与end相等,则范围为空;
      • 如果begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素;
      • 我们可以对begin递增若干次,使得begin==end;

        这些性质意味着我们可以像下面的代码一样用一个循环来处理一个元素范围,而这是安全的:

while (begin != end) { 
    *begin = val; 
    ++begin;
}

2.2.3 将一个容器初始化为另一个容器的拷贝

C c1 = c2; // 要求两个容器的类型及元素类型必须核匹配 
C c(begin, end);

        将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者(array除外)拷贝由一个迭代器对指定的元素范围

        为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配(array类型,两者还必须具有相同大小)。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且,新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可(相容即可)。

2.2.4 标准库array具有固定大小

        与内置数组一样,标准库array的大小也是类型的一部分当定义一个array时,除了指定元素类型,还要指定容器大小

array;

        为了使用array类型,我们必须同时指定元素类型和大小:

array::size_type i; //Ok 
array::size_type j; // Error

        虽然不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。

2.2.5 赋值和swap

        与内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型

2.2.5.1 使用assign(仅顺序容器)

        顺序容器(array除外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。assign操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。

2.2.5.2 使用swap

        swap操作交换两个相同类型容器的内容。调用swap之后,两个容器中的元素将会交换:

vector svec1(10);
vector svec2(24); 
swap(svec1, svec2);

        调用swap后,svec1将包含24个string元素,svec2将包含10个string。除array外,交换两个容器内容的操作保证会很快:元素本身并未交换,swap只是交换了两个容器的内部数据结构

        除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。

        与其他容器不同,swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。

        在新标准库中,容器既提供成员函数版本的swap,也提供非成员版本的swap。而早期标准库版本只提供成员函数版本的swap。非成员版本的swap在泛型编程中是非常重要的。统一使用非成员版本的swap是一个好习惯。

2.3 顺序容器操作

2.3.1 容器元素是拷贝

        当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。

2.3.2 使用emplace操作

        C++ 11新标准引入了三个新成员——emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

        当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数

Sales_data c; 
c.emplace_back("987-485", 25); 
c.push_back("987-485", 25)

        其中对emplace_back的调用和push_back调用都会创建新的Sales_data对象。在调用emplace_back时,会在容器管理的内存空间中直接创建对象。而调用push_back则会创建一个局部临时对象,并将其压入容器中。emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。

2.3.3 删除元素

2.3.3.1 pop_front和pop_back成员函数

        pop_front和pop_back成员函数分别删除首元素和尾元素。与vector和string不支持push_front一样,这些类型也不支持pop_front。类似的,forward_list不支持pop_back。与元素访问成员函数类似,不能对一个空容器执行弹出操作。这些操作返回void。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它。

2.3.3.2 从容器内部删除一个元素

        成员函数erase从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,那么erase(i)将返回指向j的迭代器。

2.3.3.3 特殊的forward_list操作

        当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,我们需要访问其前驱,以便改变前驱的链接。但是,forward_list是单向链表。在一个单向链表中,没有简单的方法来获取一个元素的前驱。出于这个原因,在一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,我们总是可以访问到被添加或删除操作所影响的元素。

2.3.4 Vector对象是如何增长

        capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。reserve操作允许我们通知容器它应该准备保存多少个元素。

        在新标准库中,可以调用shrink_to_fit来要求deque、vector或string退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_to_fit也并不保证一定退回内存空间。

2.4 容器适配器

        除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。适配器(adaptor)是标准库中的一个通用概念。容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如,stack适配器接受一个顺序容器(除array或forward_list外),并使其操作起来像一个stack一样。

2.4.1 定义一个适配器

        每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。例如,假定deq是一个deque,我们可以用deq来初始化一个新的stack。

stack stk(deq);

        默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。

        对于一个给定的适配器,可以使用哪些容器是有限制的。所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在array之上。类似的,我们也不能用forward_list来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。stack只要求push_back、pop_back和back操作,因此可以使用除array和forward_list之外的任何容器类型来构造stack。queue适配器要求back、push_back、front和push_front,因此它可以构造于list或deque之上,但不能基于vector构造。priority_queue除了front、push_back和pop_back操作之外还要求随机访问能力,因此它可以构造于vector或deque之上,但不能基于list构造。

3 泛型算法

3.1 关键概念:算法永远不会执行容器的操作

        泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。

        标准库定义了一类特殊的迭代器,称为插入器(inserter)。与普通迭代器只能遍历所绑定的容器相比,插入器能做更多的事情。当给这类迭代器赋值时,它们会在底层的容器上执行插入操作。因此,当一个算法操作一个这样的迭代器时,迭代器可以完成向容器添加元素的效果,但算法自身永远不会做这样的操作。

3.2 初识泛型算法

        标准库提供了超过100个算法。幸运的是,与容器类似,这些算法有一致的结构。除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器

        虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本的方法就是了解它们是否读取元素、改变元素或是重排元素顺序。

3.2.1 算法不检查写操作

        一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。例如,函数fill_n接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。我们可以用fill_n将一个新值赋予vector中的元素:

vector vec;
fill_n(vec.begin(), vec.size(), 0);    // 将所有元素重置为0

        一个初学者非常容易犯的错误是在一个空容器上调用fill_n(或类似的写元素的算法):

vector vec;
fill_n(vec.begin(), 10, 0);

        这个调用是一场灾难。我们指定了要写入10个元素,但vec中并没有元素:它是空的。这条语句的结果是未定义的。

template< class OutputIt, class Size, class T >
OutputIt fill_n( OutputIt first, Size count, const T& value );
/*

first	-	the beginning of the range of elements to modify
count	-	number of elements to modify
value	-	the value to be assigned
return value    Iterator one past the last element assigned if count > 0, first otherwise.
*/

3.2.2 back_inserter

        一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器(insert iterator)。插入迭代器是一种向容器中添加元素的迭代器。通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。

        back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用:

#include 
#include 
#include 
#include 
 
int main()
{
    std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::fill_n(std::back_inserter(v), 3, -1);
    for (int n : v)
        std::cout << n << ' ';
}
// Output: 1 2 3 4 5 6 7 8 9 10 -1 -1 -1

3.2.3 拷贝算法

        拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要

int a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int a2[sizeof(a1) / sizeof(*a1)];    // a2与a1大小一样
auto ret = copy(begin(a1), end(a1), a2);    // 把a1的内容拷贝至a2中

        copy返回的是其目的位置迭代器(递增后)的值。即,ret恰好指向拷贝到a2的尾元素之后的位置

        多个算法都提供所谓的“拷贝”版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。

        例如,replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:

// 将所有值为0的元素更改为42
replace(ilist.begin(), ilist.end(), 0, 42);

        此调用将序列中所有的0都替换为42。如果我们希望保留原序列不变,可以调用replace_copy。此算法接受额外第三个迭代器参数,指出调整后序列的保存位置

// 使用back_inserter按需要增长目标序列
replace_copy(ilist.begin(), ilist.end(), back_inserter(ivec), 0, 42);

        此调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。

3.2.4 重排容器元素的算法

        某些算法会重排容器中元素的顺序,一个明显的例子是sort。调用sort会重排输入序列中的元素,使之有序,它是利用元素类型的<运算符(小于为true)来实现排序的。

3.2.4.1 消除重复单词

        为了消除重复单词,首先将vector排序,使得重复的单词都相邻出现。一旦vector排序完毕,我们就可以使用另一个称为unique的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。由于算法不能执行容器的操作,我们将使用vector的erase成员来完成真正的删除操作。

void elimDumps(vector &words)
{
    // 按字典序排序words,以便查找重复单词
    // 以非降序对范围 [first, last) 中的元素进行排序,保证保留等效元素的顺序。
    sort(words.begin(), words.end());
    // unique重排输入范围,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    // 使用向量操作erase删除重复单词
    words.erase(end_unique, words.end());
}

3.3 定制操作

3.3.1 谓词

        谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unary predicate,意味着它们只接受单一参数)和二元谓词(binary predicate,意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此元素类型必须能转换为谓词的参数类型

3.3.2 lambda表达式

        我们可以向一个算法传递任何类别的可调用对象(callable object)。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表

        到目前为止,我们使用过的仅有的两种可调用对象是函数和函数指针。还有其他两种可调用对象:重载了函数调用运算符的类以及lambda表达式(lambda expression);lambda表达式最简单的语法为:

[captures] {body}

C++标准库-学习笔记_第4张图片

        lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符。

        与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。

3.3.2.1 隐式捕获

        除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。

        如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获。当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值。当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。

3.3.2.2 lambda返回类型

        默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其他返回void的函数类似,被推断返回void的lambda不能返回值。当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型。

3.4 参数绑定

3.4.1 标准库bind函数

        函数模板为 f 绑定生成一个转发调用包装器。 调用此包装器等效于调用 f 并将其一些参数绑定到 args

template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args);

        f:将绑定到某些参数的可调用对象(函数对象、函数指针、函数引用、成员函数指针或数据成员指针)

        args:要绑定的参数列表,未绑定的参数由命名空间 std::placeholders 的占位符 _1、_2、_3... 替换

        当调用指向非静态成员函数的指针或指向非静态数据成员的指针时,第一个参数必须是指向将访问其成员的对象的引用或指针(可能包括智能指针,例如 std::shared_ptr 和 std: :unique_ptr)。

        bind 的参数被复制或移动,并且永远不会通过引用传递,除非包装在 std::ref 或 std::cref 中。函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。与bind一样,函数ref和cref也定义在头文件functional中。

        允许在同一个绑定表达式中使用重复的占位符,但只有在相应的参数是左值或不可移动的右值时,结果才能得到很好的定义。

#include 
#include 
#include 
#include 
 
void f(int n1, int n2, int n3, const int& n4, int n5)
{
    std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}
 
int g(int n1)
{
    return n1;
}
 
struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};
 
int main()
{
    using namespace std::placeholders;  // for _1, _2, _3...
 
    std::cout << "1) argument reordering and pass-by-reference: ";
    int n = 7;
    // (_1 and _2 are from std::placeholders, and represent future
    // arguments that will be passed to f1)
    auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
    n = 10;
    f1(1, 2, 1001); // 1 is bound by _1, 2 is bound by _2, 1001 is unused
                    // makes a call to f(2, 42, 1, n, 7)
 
    std::cout << "2) achieving the same effect using a lambda: ";
    n = 7;
    auto lambda = [ncref=std::cref(n), n=n](auto a, auto b, auto /*unused*/) {
        f(b, 42, a, ncref, n);
    };
    n = 10;
    lambda(1, 2, 1001); // same as a call to f1(1, 2, 1001)
 
    std::cout << "3) nested bind subexpressions share the placeholders: ";
    auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
    f2(10, 11, 12); // makes a call to f(12, g(12), 12, 4, 5);
 
    std::cout << "4) bind a RNG with a distribution: ";
    std::default_random_engine e;
    std::uniform_int_distribution<> d(0, 10);
    auto rnd = std::bind(d, e); // a copy of e is stored in rnd
    for(int n=0; n<10; ++n)
        std::cout << rnd() << ' ';
    std::cout << '\n';
 
    std::cout << "5) bind to a pointer to member function: ";
    Foo foo;
    auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
    f3(5);
 
    std::cout << "6) bind to a mem_fn that is a pointer to member function: ";
    auto ptr_to_print_sum = std::mem_fn(&Foo::print_sum);
    auto f4 = std::bind(ptr_to_print_sum, &foo, 95, _1);
    f4(5);
 
    std::cout << "7) bind to a pointer to data member: ";
    auto f5 = std::bind(&Foo::data, _1);
    std::cout << f5(foo) << '\n';
 
    std::cout << "8) bind to a mem_fn that is a pointer to data member: ";
    auto ptr_to_data = std::mem_fn(&Foo::data);
    auto f6 = std::bind(ptr_to_data, _1);
    std::cout << f6(foo) << '\n';
 
    std::cout << "9) use smart pointers to call members of the referenced objects: ";
    std::cout << f6(std::make_shared(foo)) << ' '
              << f6(std::make_unique(foo)) << '\n';
}

/* Output:
1) argument reordering and pass-by-reference: 2 42 1 10 7
2) achieving the same effect using a lambda: 2 42 1 10 7
3) nested bind subexpressions share the placeholders: 12 12 12 4 5
4) bind a RNG with a distribution: 0 1 8 5 5 2 0 7 7 10 
5) bind to a pointer to member function: 100
6) bind to a mem_fn that is a pointer to member function: 100
7) bind to a pointer to data member: 10
8) bind to a mem_fn that is a pointer to data member: 10
9) use smart pointers to call members of the referenced objects: 10 10
*/

4 关联容器

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

        关联容器支持高效的关键字查找和访问。两个主要的关联容器(associative-container)类型是map和set。map中的元素是一些关键字-值(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素只包含一个关键字;set支持高效的关键字查询操作:检查一个给定关键字是否在set中。例如,在某些文本处理过程中,可以用一个set来保存想要忽略的单词。字典则是一个很好的使用map的例子:可以将单词作为关键字,将单词释义作为值。

        允许重复关键字的容器的名字中都包含单词multi;不保持关键字按顺序存储的容器的名字都以单词unordered开头。因此一个unordered_multi_set是一个允许重复关键字,元素无序保存的集合,而一个set则是一个要求不重复关键字,有序存储的集合。无序容器使用哈希函数来组织元素。

4.1 使用关联容器

        map类型通常被称为关联数组(associative array)。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。我们通过一个关键字而不是位置来查找值

        与之相对,set就是关键字的简单集合。当只是想知道一个值是否存在时,set是最有用的。

4.1.1 使用map

        一个经典的使用关联数组的例子是单词计数程序:

// 统计每个单词在输入中出现的次数
map word_count;
set exclude = {"The", "But", "And"};
string word;
while (cin >> word) {
    // 只统计不在exclude中的单词
    if (exclude.find(word) == exclude.end())
        ++word_count[word];
}
for (const auto &w : word_count) {
    // 打印结果
    cout << w.first << " occurs" << w.second
        << ((w.second > 1) ? "times" : "time") << endl;
}

        while循环每次从标准输入读取一个单词。它使用每个单词对word_count进行下标操作。如果word还未在map中,下标运算符会创建一个新元素,其关键字为word,值为0。不管元素是否是新创建的,我们将其值加1。

        当从map中提取一个元素时,会得到一个pair类型的对象。简单来说,pair是一个模板类型,保存两个名为first和second的(公有)数据成员map所使用的pair用first成员保存关键字,用second成员保存对应的值

4.2 关联容器概述

        关联容器的迭代器都是双向的。

4.2.1 定义关联容器

        当定义一个map时,必须既指明关键字类型又指明值类型,我们将每个关键字-值对包围在花括号中:

{key, value}

        而定义一个set时,只需指明关键字类型,因为set中没有值。

4.3 关联容器操作

4.3.1 关联容器额外的类型别名

类型别名

说明

key_type

此容器类型的关键字类型

mapped_type

每个关键字关联的类型,

只适用于map

value_type

对于set,与key_type相同;

对于map,为pair

        对于set类型,key_type和value_type是一样的;set中保存的值就是关键字。在一个map中,元素是关键字-值对。即,每个元素是一个pair对象,包含一个关键字和一个关联的值。由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的

        与顺序容器一样,我们使用作用域运算符来提取一个类型的成员。例如,map::key_type。

        必须记住,一个map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字成员的值。虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改

4.3.2 map的下标操作

        map和unordered_map容器提供了下标运算符和一个对应的at函数,set类型不支持下标,因为set中没有与关键字相关联的“值”。

        map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作

        at函数,返回对元素的映射值的引用。 如果不存在这样的元素,则抛出 std::out_of_range 类型的异常

4.3.3 访问元素

        如果我们所关心的只不过是一个特定元素是否已在容器中,可能find是最佳选择。对于不允许重复关键字的容器,可能使用find还是count没什么区别。但对于允许重复关键字的容器,count还会做更多的工作:如果元素在容器中,它还会统计有多少个元素有相同的关键字。如果不需要计数,最好使用find。

4.4 无序容器

        新标准定义了4个无序关联容器(unordered associative container)。这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function)和关键字类型的==运算符。在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的。在某些应用中,维护元素的序代价非常高昂,此时无序容器也很有用。

        虽然理论上哈希技术能获得更好的平均性能,但在实际中想要达到很好的效果还需要进行一些性能测试和调优工作。因此,使用无序容器通常更为简单(通常也会有更好的性能)。

        无序关联容器实现了可以快速搜索的未排序(散列)数据结构【O(1) 至O(n) 复杂度】。

C++标准库-学习笔记_第5张图片

4.4.1 管理桶

        无序容器在存储上组织为一组桶,每个桶保存零个或多个元素无序容器使用一个哈希函数将元素映射到桶为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小

        对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。

5 动态内存

        为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象

        标准库还定义了一个名为weak_ptr的智能指针,它持有对std::shared_ptr 管理的对象的非拥有(“弱”)引用。 必须将其转换为 std::shared_ptr 才能访问引用的对象。这三种类型都定义在memory头文件中。

5.1 shared_ptr

        智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息指针可以指向的类型

5.1.1 make_shared函数

        最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。

        当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型。

5.1.2 shared_ptr的拷贝和赋值

        可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr,计数器都会递增。

        当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数(形参)以及作为函数的返回值时,它所关联的计数器就会递增

      • 递增是因为此对象没有被销毁;

        当我们给shared_ptr赋予一个新值(赋值操作)或是shared_ptr被销毁时,计数器就会递减

auto a = make_shared(28);    // 新创建一个对象,引用计数为1
auto b(a);    // a和b指向同一个对象,引用计数为2
auto c = make_shared(12);    // // 新创建一个对象,引用计数为1
c = a;    // c被指派指向a指向的对象,原对象make_shared(12)将被销毁, a指向的对象引用计数为3;

5.1.3 直接管理内存

        虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的内存,new表达式就会失败默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常。

int *p = new(nothrow)int; // 如果分配内存失败,则返回nullptr

        为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式(delete expression)来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象。我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的

int i = 3;
char *p1 = &i;
delete i;    // Error
char *p2 = new char;
delete p2;    // ok
delete p2;    // error
char *p3 = nullptr;
delete p3;    // ok

        当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针(dangling pointer),即,指向一块曾经保存数据对象但现在已经无效的内存的指针。未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

5.1.4 shared_ptr和new结合使用

        接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针

shared_ptr p1 = new int(1024); // 错误,必须使用直接初始化方式 
shared_ptr p2 (new int(2048)); // ok

        p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针,我们必须将shared_ptr显式绑定到一个想要返回的指针上

shared_ptr clone(int p) { 
    // return new int(p); ---> error. 
    return shared_ptr(new int(p)); 
}

        默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。

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

void process(shared_ptr ptr) {
    //使用ptr
}    // ptr离开作用域,被销毁

int *x(new int(1024));
process(shared_ptr (x));    // 合法的,但内存会被释放!
int j = *x;    // 未定义:x是一个空悬指针

        当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

5.1.5 不要使用get初始化另一个智能指针或为智能指针赋值

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

shared_ptr p(new int(42));
int *q = p.get()    // 正确;但使用q时要注意,不要让它管理的指针被释放
{
    shared_ptr(q);
}    // q被销毁,它指向的内存被释放
int foo = *p;    // 未定义:p指向的内存已经被释放了

5.1.6 智能指针和哑类

        包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。

        那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误:程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。

        与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的:

struct destination;    // 表示我们正在连接什么
struct connection;    // 使用连接所需的信息
connection connect(destination *p);    // 打开连接
void disconnect(connection);    // 关闭给定的连接
void f(destination &d){
    // 获取一个连接,记住使用完成后要关闭它
    connection c = connect(&d);
    // 如果我们在f退出前忘记调用disconnect,就无法关闭c了
}

        如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。但是,connection没有析构函数。这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的。使用shared_ptr来保证connection被正确关闭,已被证明是一种有效的方法。

        默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作

void end_connection(connection *p) {disconnect(*p);}
void f(destination &d){
    // 获取一个连接,记住使用完成后要关闭它
    connection c = connect(&d);
    shared_ptr p (&c, end_connection);
}

        当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。接下来,end_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被关闭。

5.2 unique_ptr

        一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁

        与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new或new T[]返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式

template<
    class T,
    class Deleter = std::default_delete
> class unique_ptr;    // 管理单个由new分配的对象
template <
    class T,
    class Deleter
> class unique_ptr;    // 管理由new[]分配出来的数组

C++标准库-学习笔记_第6张图片

void std::unique_ptr::reset(pointer ptr)
/*
给定由 *this 管理的指针 current_ptr,按以下顺序执行以下操作:
    1. 保存当前指针的副本:old_ptr = current_ptr
    2. 用参数ptr覆盖当前指针:current_ptr = ptr
    3. 如果旧指针非空,则删除先前管理的对象:if(old_ptr) get_deleter()(old_ptr)
        (old_ptr),为()操作符重载;
*/

// 返回将用于销毁托管对象的删除器对象。
Deleter& std::unique_ptr::get_deleter()

5.3 weak_ptr

        weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。

        当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它

        由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在返回空指针。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。例如:

#include 
#include 
 
void observe(std::weak_ptr weak) 
{
    if (auto observe = weak.lock()) {
        std::cout << "\tobserve() able to lock weak_ptr<>, value=" << *observe 
            << "\n";
    } else {
        std::cout << "\tobserve() unable to lock weak_ptr<>\n";
    }
}
 
int main()
{
    std::weak_ptr weak;
    std::cout << "weak_ptr<> not yet initialized\n";
    observe(weak);
 
    {
        auto shared = std::make_shared(42);
        weak = shared;
        std::cout << "weak_ptr<> initialized with shared_ptr.\n";
        observe(weak);
    }
 
    std::cout << "shared_ptr<> has been destructed due to scope exit.\n";
    observe(weak);
}

/*
Output:
weak_ptr<> not yet initialized
observe() unable to lock weak_ptr<>
weak_ptr<> initialized with shared_ptr.
observe() able to lock weak_ptr<>, value=42
shared_ptr<> has been destructed due to scope exit.
observe() unable to lock weak_ptr<> 
*/

5.4 动态数组

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

5.4.1 new和数组

        为了让new分配一个对象数组,我们要在类型名(Type)之后跟一对方括号,在其中指明要分配的对象的数目。new分配要求数量的对象并(假定分配成功后)返回一个指向数组初始元素的指针。方括号中的大小必须是整型,但不必是常量。

int getSize() {return 10;} 
int *p = new [getSize())];

        虽然我们通常称new T[]分配的内存为“动态数组”,但这种叫法某种程度上有些误导。当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。

        由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end,因为这些函数使用数组维度(维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素

char a[0]; // 非法,不能定义长度为0的数组 
char *p = new char[0]; // 合法,但p不能解引 
delete [] p;

        对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用,毕竟它不指向任何元素。

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

        标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。另一方面,当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素

        与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素。

unique_ptr a(new int[10]);
for (int i = 0; i < 10; i++) a[i] = i;
a.release();
{
    shared_ptr b(new int[10], [](int *p) {delete [] p;});
    for (int i = 0; i < 10; i++) *(b.get() + i) = i;   
}

5.4.2 allocator类

        标准库allocator类定义在头文件memory中,它帮助我们将内存分配对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置

// 指向 T 类型的 n 个对象数组的第一个元素,其元素尚未构造。
// T* std::allocator::allocate(std::size_t n);
allocator str;
str.allocate(n);    // 分配n个未初始化的string

        allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。

        还未构造对象的情况下就使用原始内存是错误的

        当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数。我们只能对真正构造了的元素进行destroy操作

        一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成。我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值

// 使用new,在p指向的已分配未初始化存储中构造一个类型为 T 的对象
// void std::allocator::construct(pointer p, const_reference val)
// template< class U, class... Args >
//void std::allocator::constructconstruct( U* p, Args&&... args );
auto q = p;    // q指向最后已构造的元素之后的位置
str.construct(q++);
std::cout << *P << std:endl;    //合法
//std::cout << *q << std:endl;    //灾难,q指向未构造的内存
while (q != p) {
   str.destory(--q);
}
str.deallocate(p, n)

你可能感兴趣的:(C++,Primer,5th,c++)