《C++Primer(第5版)》第十二章笔记

Posted qq_34132502

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《C++Primer(第5版)》第十二章笔记相关的知识,希望对你有一定的参考价值。

到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期。全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。

除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

动态对象的正确释放被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

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

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

12.1 动态内存与智能指针

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

12.1.1 shared_ptr类

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

shared_ptr<string> p1;
shared_ptr<list<int>> p2;

默认初始化的智能指针中保存一个空指针

下表列出了shared_ptrunique_ptr都支持的操作与只支持shared_ptr的操作
在这里插入图片描述
make_shared函数

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

shared_ptr<int>p3 = make_shared<int>(42);
shared_ptr<string>p4 = make_shared<string>(10, '9');
shared_ptr<int>p5 = make_shared<int>();

类似顺序容器的emplace成员(与push_back对应,前者插入构造元素,后者插入拷贝元素。所以在调用emplace成员函数时,是将参数传递给元素类型的构造函数),make_shared用其参数来构造给定类型的对象。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配,调用make_shared<int>时传递的参数必须能用来初始化一个int,依此类推。如果我们不传递任何参数,对象就会进行值初始化。

当然,我们通常用auto定义一个对象来保存make_ shared的结果,这种方式较为简单:

auto p6 = make_shared<vector<string>>();

shared_ptr的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42);	// p指向的对象只有p一个引用者
atuo q(p);	// p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。

一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:

auto r = make_shared<int>(42);
r = q;
// 给r赋值,令它指向另一个地址
// 递增q指向的对象的引用计数
// 递减r原来指向的对象的引用计数
// r原来指向的对象已经没有引用者,会自动释放

此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样, 析构函数控制此类型的对象销毁时做什么操作。

shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

shared_ptr还会自动释放相关联的内存

// factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){
	// 恰当地处理arg
	// shared_ptr负责释放内存
	return make_shared<Foo>(arg);
}

由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。share_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr 元素。

使用了动态生存期的资源的类

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

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

容器类是出于第一种原因而使用动态内存的典型例子。

vector<string> vs;

void func() {
    vector<string> vs2 = { "a", "an", "the" };
    vs = vs2;
}

此例中,vs与vs2共享相同的元素,当vs2离开作用域时,这些元素必须保留,因为vs仍在使用它们

定义StrBlob类

但是,我们不能在一个Blob对象内直接保存vector,因为一个对象的成员在对象销毁时也会被销毁。例如,假定bl和b2是两个Blob对象,共享相同的vector。如果此vector保存在其中一个Blob中——例如b2中,那么当b2离开作用域时,此vector也将被销毁,也就是说其中的元素都将不复存在。为了保证vector中的元素继续存在,我们将vector保存在动态内存中。

为了实现我们所希望的数据共享,我们为每个StrBlob设置一个shared_ptr来管理动态分配的vector。此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector。
我们还需要确定这个类应该提供什么操作。当前,我们将实现一个vector操作的小的子集。我们会修改访问元素的操作(如frontback);在我们的类中,如果用户试图访问不存在的元素,这些操作会抛出一个异常。
我们的类有一个默认构造函数和一个构造函数,接受单一的initializer_list<string>类型参数。此构造函数可以接受一个初始化器的花括号列表。

class StrVlob {
public:
	typedef std::vector<std::string>::size_type size_type;
	StrBlob();
	StrBlob(std::initializer_list<std::string> il);
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	// 添加删除元素
	void push_back(const std::string &t) { data->push_back(t); }
	void pop_back();
	// 元素访问
	std::string& front();
	std::string& back();
private:
	std::shared_ptr<std::vector<std::string>> data;
	// 如果data[i]不合法,抛出一个异常
	void check(size_type i, const std::string &msg) const;
}

在此类中,我们实现了size、empty和push_back 成员。这些成员通过指向底层vector的data成员来完成它们的工作。例如,对一个StrBlob对象调用size()会调用data->size(),依此类推。

StrBlob构造函数

StrBlob::StrBlob(): data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il):
	data(make_shared<vector<string>>(il)) {}

接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数。次构造函数通过拷贝列表中的值来初始化vector元素

元素访问成员函数

pop_back、front和back操作访问vector中的元素。这些操作在试图访问元素之前必须检查元素是否存在。由于这些成员函数需要做相同的检查操作,我们为strBlob定义了一个名为check的private工具函数,它检查一个给定索引是否在合法范围内。除了索引,check还接受一个string参数,它会将此参数传递给异常处理程序,这个string描述了错误内容:

void StrBlob::check(size_type i, const string &msg) const {
	if (i >= data->size()) 
		throw out_of_range(msg);
}

pop_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作:

string& StrBlob::front() {
	// 如果vector为空,check会抛出一个异常
	check(0, "front on empty StrBlob");
	return data->front();
}
string& StrBlob::bakc() {
	check(0, "back on empty StrBlob");
	return data->back();
}
void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}

StrBlob的拷贝、赋值和销毁

类似Sales_data类,StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。我们的StrBlob类只有一个数据成员,它是shared_ptr类型。因此,当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。
如前所见,拷贝一个shared_ptr会递增其引用计数:将一个shared_ptr赋予另一个shared_ptr:会递增赋值号右侧shared_ptr的引用计数,而递减左侧shared_ptr的引用计数。如果-一个shared_ptr的引用计数变为0,它所指向的对象会被自动销毁。因此,对于由StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。

12.1.2 直接管理内存

使用new动态分配和初始化对象

new返回一个指向该对象的指针:

int *pi = new int;

可以直接初始化方式初始化一个对象:

int *pi1 = new int();
int *pi2 = new int(1024);
vector<int> *pv = new vector<int> {0,1,2,3};

如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:

auto p1 = new auto(obj);		// p指向一个与obj类型相同的对象,该对象用obj进行初始化
auto p2 = new auto(a, b, c);	// 错误:括号中只能有单个初始化器

动态分配的const对象

new分配const对象是合法的:

const int *pci = new const int(1024);
cibst string *pcs = new const string;

new返回的是一个指向const的指针

内存耗尽

一单一个程序用光了它所有可用的内存,new表达式就会失败,抛出bad_alloc的异常。我们可以改变使用new的方式来阻止他抛出异常:

int *p1 = new int;	// 如果失败抛出异常
int *p2 = new (nothrow) int;	// 如果失败,返回空指针

我们称这种形式的new为定位new(placement new)。定位new表达式允许我们向new传递额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_allocnothrow都定义在头文件new中。

释放动态内存

delete p;

销毁给定的指针指向的对象,释放对应内存

指针值和delete

我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要delete指向它的指针即可

动态对象的生存期直到被释放为止

shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。

返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了个额外负担一调用者必须记得释放内存
特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。

delete之后重置指针值

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

仍然存在的问题

动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。例如:

int *p(new int(42));	// p指向动态内存
auto q = p;				// p和q指向相同的内存
delete p;				// p和q均无效
p = nullptr;			// p不绑定任何对象

12.1.3 shared_ptr和new结合使用

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

我们可以用new返回的指针初始化智能指针:

shared_ptr<double> p1;
shared_ptr<int> p2(new int(42));
shared_ptr<int> p3 = new int(42);	// 错误:必须使用直接初始化形式

我们不能进行内置指针到智能指针间的隐式类型转换,所以这条语句是错误的

不要混合使用普通指针和智能指针

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

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

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

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

虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:

shared_ptr<int> p(new int(42));
int *q = p.get;
{
	// 新程序块
	// 未定义:两个独立的shared_ptr指向相同的内存
	shared_ptr<int>(q);
}	// 程序块结束,q被销毁,它指向的内存被释放
int foo = *p;	// 未定义:p指向的内存已经被释放

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

其他shared_ptr操作

shared_ptr还定义了其他一些操作。我们可以使用reset来将一个新的指针赋予一个shared_ptr:

p = new int(1024);	// 错误:不能将一个指针赋予shared_ptr
p.reset(new 1024);	// 正确:p指向一个新对象

与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset 成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

if (!p.unique())
	p.reset(new string(*p));	// 我们不是唯一用户;分配新的拷贝
*p += newVal;	// 现在我们知道自己是唯一的用户

12.1.4 智能指针和异常

之前介绍了使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

void f() {
	shared_ptr<int> sp(new int(42));	// 分配一个新对象
	// 这段代码抛出一个异常,且在f中未被捕获
}	// 在函数结束时shared_ptr自动释放内存

函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针, 因此内存会被释放掉。
与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放

智能指针和哑类

包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误一程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。

struct destinaion;
struct connection;
connection connect(destination*);
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中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数:

void end_connection(connection *p) { disconnect(*p); }

当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:

void f(destionation &d /* 其他参数 */) {
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	// 使用;连接
	// 当f退出时(即使是由于异常而退出),connection会被正确关闭
}

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

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

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

12.1.5 unique_ptr

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

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

unique_ptr<double> p1;	// 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42));	// p2指向一个值为42的int

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作
在这里插入图片描述
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用releasereset将指针的所有权从一个(非constunique_ptr转移给另一个unique:

// 所有权从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原来指向的内存

调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

p2.release();	// 错误:p2不会释放内存,且我们丢失了指针
auto p = p2.release();	// 正确,但我们必须记得delete(p)

传递unique_ptr参数和返回unique_ptr

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

unique_ptr<int> clone(int p) {
	// 正确:从int*创建一个unique_ptr<int>
	return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
```cpp
unique_ptr<int> clone(int p) {
	unique_ptr<int> ret(new int (p));
	// ...
	return ret;
}

对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”

向unique_ptr传递删除器

类似shared_ptrunique_ptr默认情况下用delete释放它指向的对象。与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。但是,unique_ptr管理删除器的方式与shared_ptr不同。
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):

// p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
// 它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> p(new objT, fcn);

我们重写连接程序,用unique_ptr代替shared_ptr:

void f(destination &d) {
	connection c = connect(&d);
	// 当p被销毁时,连接将会关闭
	unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
	// 使用连接
	// 当f退出时(即使是由于异常而退出),connection会被正确关闭
}

在本例中我们使用了decltype来指明函数指针类型。由于decltype(end_connection)返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类型的一个指针。

12.1.6 weak_ptr

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

在这里插入图片描述
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);

由于对象可能不存在,我

以上是关于《C++Primer(第5版)》第十二章笔记的主要内容,如果未能解决你的问题,请参考以下文章

C++primer第十二章读书笔记---动态内存与智能指针

C Primer Plus(第六版)第十二章 编程练习答案

《C++Primer(第5版)》第十一章笔记

《C++Primer(第5版)》第十三章笔记

C++Primer 第十二章

数据库系统概念笔记——第十二章:查询处理