C++标准库

《C++ Primer》第二部分

Posted by ZhouSh on February 24, 2023

八、IO库

IO对象不能拷贝或赋值,所以不能将形参或返回类型设置为IO类型,进行IO操作的函数通常以引用方式传递和返回流。

文本串可能立即打印出来,但也可能被操作系统保存在缓冲区中,随后再打印。如下代码将先等待3s再打印aaabbb,而不是立即打印aaa再等待3s。

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <chrono>
#include <thread>

int main(){
    std::cout << "aaa";
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "bbb";
}

操作符endl换行并刷新缓冲区,flush刷新缓冲区但不输出任何额外字符,ends向缓冲区插入一个空字符再刷新缓冲区。

默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。

unitbuf操作符告诉流在接下来的每次写操作后都进行一次flush操作。而nounitbuf则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。

默认情况下,cin和cerr都关联到cout,因此读cin或写cerr都会导致cout的缓冲区被刷新。

如果程序异常终止,输出缓冲区不会被刷新,数据很可能停留在缓冲区中等待打印。所以调试程序时最好在输出语句后都跟上endl。

九、顺序容器

标准库中的顺序容器:

string和vector将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时。

list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。

与内置数组相比,array是一种更安全、更容易使用的数组类型。

通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。

如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中。

如果你不确定应该使用哪种容器,那么可以在程序中只使用vector和list公共的操作:使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用vector或list都很方便。

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

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

assign操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。这段代码中对assign的调用将names中的元素替换为迭代器指定的范围中的元素的拷贝。

1
2
3
list<string> names;
vector<const char*> oldstyle;
names.assign(oldstyle.cbegin(), oldstyle.cend());

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

1
2
3
vector<string> svec1(10);
vector<string> svec2(24);
swap(svec1, svec2);

与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效

与其他容器不同,swap两个array会真正交换它们的元素。

十一、关联容器

顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。关联容器中的元素是按关键字来保存和访问的。

标准库提供8个关联容器。map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。

当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中:{key,value}

pair是一种标准库类型,它定义在头文件utility中,一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。创建pair对象的函数可以使用列表初始化或make_pair:

1
2
pair<string, int> a = {"aaa", 1};
pair<string, int> b = make_pair("bbb", 1);

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

对map而言,value_type是一个pair类型,其first成员保存const的关键字key,second成员保存值value。

set的迭代器是const的。

关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素。我们可以用泛型find算法来查找一个元素,但此算法会进行顺序搜索。使用关联容器定义的专用的find成员会比调用泛型find快得多。

insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。

关联容器定义了三个版本的erase:

map和unordered_map容器提供了下标运算符和一个对应的at函数。与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。

set类型不支持下标,因为set中没有与关键字相关联的“值”。

通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但对map则不然:当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。

有时只是想知道一个元素是否已在map中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符,应该使用find:

1
2
if(word_count.find("foobar") == word_count.end())
    cout << "foobar is not in the map" << endl;

如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。

如果关键字在容器中,lower_bound返回的迭代器将指向第一个具有给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。如果元素不在multimap中,则lower_bound和upper_bound会返回相等的迭代器——指向一个不影响排序的关键字插入位置。因此,用相同的关键字调用lower_bound和upper_bound会得到一个迭代器范围,表示所有具有该关键字的元素的范围。但是返回的迭代器可能是容器的尾后迭代器。

equal_range函数接受一个关键字,返回一个迭代器pair。若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。

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

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

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

允许重复关键字的容器的名字中都包含multi;而使用哈希技术的容器的名字都以unordered开头。

有序容器的迭代器通过关键字有序访问容器中的元素。无论在有序容器中还是在无序容器中,具有相同关键字的元素都是相邻存储的。

十二、动态内存

全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。

动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或(heap)。程序用来存储动态分配(dynamically allocate)的对象,即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

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

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。

每个shared_ptr都有一个关联的计数器,通常称其为引用计数。拷贝一个shared_ptr时,计数器递增;给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器递减。计数器变为0,它就会自动释放自己所管理的对象。

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

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

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针: int *pi = new int;

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

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

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

  1. 不使用相同的内置指针值初始化(或reset)多个智能指针。
  2. 不delete get()返回的指针。
  3. 不使用get()初始化或reset另一个智能指针。
  4. 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  5. 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

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

与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。 unique_ptr<int> p1(new int(42));

虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

1
2
3
4
5
// 将所有权从p1转移给p2
unique_ptr<string> p2(p1.release()); // release将p1置为空
unique_ptr<string> p3(new string("Trex"));
// 将所有权从p3转移给p2
p2,reset(p3.release()); // reset释放了p2原来指向的内存

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

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

1
2
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p,p的引用计数未改变

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

1
2
3
4
if(shared_ptr<int> np = wp.lock())
{
    // 在if中,np与p共享对象
}