【C++ Primer】第12章 动态内存 (1)

本文围绕C++动态内存与智能指针展开。介绍了动态分配对象的生存期特点,以及为安全使用动态对象,C++11标准库提供的两种智能指针:shared_ptr和unique_ptr,还提及了伴随类weak_ptr。同时阐述了直接管理内存的风险,以及智能指针在异常处理中的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


Part II: The C++ Library
Chapter 12. Dynamic Memory


到目前为止,程序中使用的对象都有严格定义的生存期:
①全局对象在程序启动时分配,在程序结束时销毁。
②局部自动对象在进入定义它所在的块时创建,退出块时销毁。
③局部static对象在它第一次使用前分配,在程序结束时销毁。

C++还允许动态地分配对象。动态分配的对象的生存期与它们在哪里创建的无关;它们一直存在,直到它们被显式地释放。

正确释放动态对象在程序中容易出错。为了在使用动态对象时更安全,库定义了 2 中智能指针类型,管理动态分配的内存。智能指针可确保在适当时自动释放它们指向的对象。

到目前为止,程序中使用了静态或栈内存。
静态内存用于局部 static 对象、类 static 数据成员、定义在任何函数外面的变量。
栈内存用于定义在函数内的非static对象。栈对象只存在于定义它们的块运行时。
分配在静态或栈内存中的对象会自动被编译器创建或销毁。

除了静态或栈内存,每个程序有一个可以使用的内存池。这个内存被称为自由存储区 (free store) 或 (heap)。
程序使用堆存储动态分配 (dynamically allocate) 的对象,即程序在运行时分配的对象。
程序控制动态对象的生存期;当不需要这样的对象时,代码必须显式地销毁它们。


12.1 动态内存与智能指针

在C++中,动态内存通过一对运算符管理:new,分配且有选择性的初始化动态内存的对象,返回一个指向该对象的指针;delete,接受一个指向动态对象的指针,销毁该对象,释放关联的内存。

为了令使用动态内存更容易且更安全,C++11标准库提供了两种智能指针 (smart pointer) 类型,管理动态对象。
智能指针的行为类似于常规指针,不同的是它会自动删除其指向的对象。

库定义了两种指针指针,它们在如何管理底层指针的方式上不同:shared_ptr,允许多个指针指向相同的对象;unique_ptr,“占有”它指向的对象。
库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 管理的对象。
上面的三个都定义在 memory 头文件中。

shared_ptr 类

智能指针是模板。因此,当创建智能指针时,必须提供额外信息,即指针可以指向的类型。在尖括号内提供类型。

shared_ptr<string> p1;    // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints

智能指针的使用方式与指针类似。解引用一个智能指针返回该指针指向的对象。当在条件中使用智能指针,作用是测试指针是否为空。

// if p1 is not null, check whether it's the empty string
if (p1 && p1->empty())
	*p1 = "hi";  // if so, dereference p1 to assign a new value to that string

表12.1 shared_ptr 和 unique_ptr 共有的操作

操作说明
shared_ptr<T> sp
unique_ptr<T> up
空智能指针,可以指向类型 T 的对象。
p使用 p 作为一个条件;若 p 指向一个对象则为 true。
*p解引用 p,获取 p 指向的对象。
p->mem等同于 (*p).mem
p.get()返回 p 中的指针。谨慎使用;当智能指针删除其对象时,那么返回的指针指向的对象会消失。
swap(p,q)
p.swap(q)
交换 p 和 q 中的指针。

表12.2 shared_ptr 特有的操作

操作说明
make_shared<T>(args)返回一个 shared_ptr,指向一个动态分配的类型为 T 的对象。使用 args 初始化该对象。
shared_prt<T> p(q)p 是 shared_prt q 的复制;增加 q 中的计数器。q 中的指针必须能转换为 T*。
p = qp 和 p 是 shared_ptr,其中保存的指针可以相互转换。
递减 p 中的引用计数器,递增 q 的计数器;如果 p 的计算器变为 0,删除 p 原有的内存。
p.unique()如果 p.use_count() 是 1,返回 true,否则返回 false。
p.use_count()返回与 p 分享对象的智能指针数目。一个较慢的操作,主要用于调试。

make_shared 函数

分配和使用动态内存最安全的方式是,调用名为 make_shared 的库函数。该函数在动态内存中分配并初始化一个对象,返回一个指向该对象的 shared_ptr。make_shared 定义在 memory 头文件中。

当调用 make_shared 时,必须指定想要创建对象的类型。

// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 points to an int that is value initialized (§ 3.3) to 0
shared_ptr<int> p5 = make_shared<int>();

通常使用 auto 来简化定义包含 make_shared 结果的对象的过程:

// p6 points to a dynamically allocated, empty vector<string>
auto p6 = make_shared<vector<string>>();

shared_ptr 的复制与赋值

当复制或赋值 shared_ptr 时,每个 shared_ptr 记录有多少个其他 shared_ptr 指向同一个的对象。

auto p = make_shared<int>(42); // object to which p points has one user
auto q(p); // p and q point to the same object
           // object to which p and q point has two users

可以认为 shared_ptr 有一个关联的计数器,通常称为引用计数器 (reference count)。
当复制一个 shared_ptr 时,计数会递增。例如,shared_ptr 关联的计数器会递增的情况:使用它去初始化另一个 shared_ptr 时;使用它作为赋值操作的右侧运算对象时;将它传递给函数或作为函数的返回值时等。
当赋值一个新值给 shared_ptr 时,与 shared_ptr 本身被销毁时,计数器递减。例如,局部 shared_ptr 离开其作用域。

一旦 shared_ptr 的计数器变为 0,shared_ptr 自动释放它管理的对象。

auto r = make_shared<int>(42); // int to which r points has one user
r = q;  // assign to r, making it point to a different address
        // increase the use count for the object to which q points
        // reduce the use count of the object to which r had pointed
        // the object r had pointed to has no users; that object is automatically freed

shared_ptr 自动销毁它们的对象 …

当指向一个对象的最后的 shared_ptr 被销毁,shared_ptr 类自动销毁该 shared_ptr 指向的对象。这是通过特殊的成员函数析构函数来实现的。

shared_ptr 的析构函数递减此 shared_ptr 指向的对象的引用计数器。如果计数变为 0,shared_ptr 析构函数销毁该 shared_ptr 指向的对象,释放该对象使用的内存。

… 自动释放关联的内存

当动态对象不被需要时,shared_ptr 类可以自动释放它们。

// factory returns a shared_ptr pointing to a dynamically allocated object
shared_ptr<Foo> factory(T arg) {
	// process arg as appropriate
	// shared_ptr will take care of deleting this memory
	return make_shared<Foo>(arg);
}

void use_factory(T arg) {
	shared_ptr<Foo> p = factory(arg);
	// use p
} // p goes out of scope; the memory to which p points is automatically freed

上面的 p 会被销毁,对象所在的内存会被释放。

如果还有其他 shared_ptr 指向内存,则内存不会被释放。

shared_ptr<Foo> use_factory(T arg) {
	shared_ptr<Foo> p = factory(arg);
	// use p
	return p;  // reference count is incremented when we return p
} // p goes out of scope; the memory to which p points is not freed

因为内存直到最后的 shared_ptr 销毁才被释放,所以要确保 shared_ptr 在不被需要时不再保留。
如果忽略销毁程序不再需要的 shared_ptr,程序会正确允许,但会浪费内存。

注:如果将 shared_ptr 放入容器内,随后只需要一部分而不是全部的元素,记得 erase 不再需要的元素。

使用具有动态生存期的资源的类

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

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

容器类使用动态内存是出于第一个原因,第15章介绍了出于第二个原因的类。本章节定义了一个使用动态内存的类,为了使多个对象分享同样的底层数据。

到目前为止,使用的类分配的资源与其对应的对象生存期一致。例如,每个 vector “拥有”它自己的元素。vector 分配的元素只存在于 vector 本身存在期间。当 vector 销毁时,vector 中的元素也被销毁。

一些对象分配的资源,其生存期独立于原对象。假设定义一个 Blob 类,保存一组数据,其副本与原对象分享同样的元素。
一般来说,如果两个对象分享同一个底层数据,当某个对象被销毁时,不能单方面地销毁数据。

Blob<string> b1;    // empty Blob
{ // new scope
	Blob<string> b2 = {"a", "an", "the"};
	b1 = b2; // b1 and b2 share the same elements
} // b2 is destroyed, but the elements in b2 must not be destroyed
  // b1 points to the elements originally created in b2

定义 StrBlob 类

定义一个管理 string 的类,将其命名为 StrBlob。

实现一个新的集合类型的最容易方式是使用某个库容器管理元素。这样可以让库容器自己管理元素的空间。在本例中,使用 vector 保存元素。

但是,不能在 Blob 对象中直接保存 vector。当对象本身销毁时,对象的成员也被销毁。

为了实现数据分享,为每个 StrBlob 提供一个 shared_ptr,指向动态分配的 vector。此 shared_ptr 成员记录有多少个 StrBlob 分享同一个 vector,当最后的使用 vector 的 StrBlob 被销毁时,删除 vector。

class StrBlob {
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(); }
	// add and remove elements
	void push_back(const std::string &t) {data->push_back(t);}
	void pop_back();
	// element access
	std::string& front();
	std::string& back();
private:
	std::shared_ptr<std::vector<std::string>> data;
	// throws msg if data[i] isn't valid
	void check(size_type i, const std::string &msg) const; 
}; 

StrBlob 构造函数

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

元素访问成员

pop_back、front 和 back 操作访问 vector 中的成员。这些操作在试图访问元素之前必须检查这个元素是否存在。因此,在类内定义一个名为 check 的 private 工具函数,验证给定的索引是否在范围内。

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

除了索引,check 函数还接受一个 string 实参,描述错误信息。

string& StrBlob::front() {
	// if the vector is empty, check will throw
	check(0, "front on empty StrBlob");
	return data->front();
}
string& StrBlob::back() {
	check(0, "back on empty StrBlob");
	return data->back();
}
void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}

front 和 back 成员应在 const 上重载。

string& StrBlob::front() const {
	// if the vector is empty, check will throw
	check(0, "front on empty StrBlob");
	return data->front();
}
string& StrBlob::back() const {
	check(0, "back on empty StrBlob");
	return data->back();
}

StrBlob 的复制、赋值与销毁

StrBlob 使用默认版本的复制、赋值与销毁操作。默认情况下,这些操作复制、赋值与销毁类的数据成员。因此,当复制、赋值或销毁一个 StrBlob 时,它的 shared_ptr 会被复制、赋值或销毁。

直接管理内存

C++本身定义了两种运算符分配和释放动态内存。new 运算符分配内存,delete 释放 new 分配的内存。

相比使用智能指针,使用这些运算符管理内存更容易出错。

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

分配在自由空间上的内存是未命名的,因此 new 无法为它分配的对象命名。new 返回一个指向它分配的对象的指针。

int *pi = new int;      // pi points to a dynamically allocated,
                        // unnamed, uninitialized int

默认情况下,动态分配的对象被默认初始化,这意味着内置或复合类型的对象具有未定义的值,类类型的对象通过其默认构造函数进行初始化。

string *ps = new string;  // initialized to empty string
int *pi = new int;        // pi points to an uninitialized int

可以使用直接初始化方式初始化动态分配的内存。可以使用传统的构造方式(使用圆括号),在C++11标准下,也可以使用列表初始化(使用花括号)。

int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9');   // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9}; 

通过在类型名字后面加上一对空的圆括号,也可以值初始化一个动态分配的对象。

string *ps1 = new string;  // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int;        // default initialized; *pi1 is undefined
int *pi2 = new int();      // value initialized to 0; *pi2 is 0

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

如果在圆括号内提供一个初始值,那么可以使用 auto 从初始值中推断想要分配的对象的类型。然而,因为编译器使用初始值的类型推断分配的类型,所以只能在圆括号内有单一的初始值的情况下使用 auto。

auto p1 = new auto(obj);   // p points to an object of the type of obj
                           // that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer

动态分配的 const 对象

使用 new 分配 const 对象是合法的。

// allocate and initialize a const int
const int *pci = new const int(1024);
// allocate a default-initialized const empty string
const string *pcs = new const string;

动态分配的 const 对象必须初始化。定义了默认构造函数的类类型的动态分配的对象可以隐式地初始化。
因为分配的对象是 const,所以 new 返回的指针是指向 const 的指针。

内存耗尽

虽然现代计算机通常配备大容量内存,但自由空间被耗尽这种情况也有可能发生。一旦程序使用完所有的可用内存,new 表达式会失败。
默认情况下,如果 new 不能分配所要求的空间,它会抛出一个 bad_alloc 类型的异常。
可以使用 new 的不同形式来阻止 new 抛出异常。

// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer

这种形式的 new 称为定位 new (placement new)。定位 new 表达式允许向 new 传递额外的实参。
在本例中,传递了一个名为 nothrow 的对象,它由库定义。当传递 nothrow 给 new 时,告诉 new 不能抛出异常。
bad_alloc 和 nothrow 都定义在 new 头文件中。

释放动态内存

为了防止内存耗尽,在使用完动态分配的内存后要将它还给系统。归还内存通过 delete 表达式
delete 表达式接受一个指向想要释放的对象的指针。

delete p;      // p must point to a dynamically allocated object or be null

delete 表达式执行两个动作:销毁给定指针指向的对象;释放对应的内存。

指针值与 delete

传递给 delete 的指针必须指向动态分配的内存或是一个空指针。删除一个指针指向的不是 new 分配的内存,或多次删除同一个指针值,结果是未定义的。

int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;   // error: i is not a pointer
delete pi1; // undefined: pi1 refers to a local
delete pd;  // ok
delete pd2; // undefined: the memory pointed to by pd2 was already freed
delete pi2; // ok: it is always ok to delete a null pointer

虽然一个 const 对象的值不能被修改,但是对象本身可以被销毁。

const int *pci = new const int(1024);
delete pci;  // ok: deletes a const object

动态分配的对象一直存在,直到它被释放为止

通过内置指针管理的动态对象一直存在,直到它被显式地删除。

返回指向动态内存的指针(不是智能指针)的函数给调用者增加了负担——调用者必须记得删除内存。

// factory returns a pointer to a dynamically allocated object
Foo* factory(T arg) {
	// process arg as appropriate
	return new Foo(arg); // caller is responsible for deleting this memory
}

但调用者常常忘记释放对象。

void use_factory(T arg) {
	Foo *p = factory(arg);
	// use p but do not delete it
} // p goes out of scope, but the memory to which p points is not freed!

应当记得在 use_factory 中释放内存修复这个错误。

void use_factory(T arg) {
	Foo *p = factory(arg);
	// use p
	delete p;  // remember to free the memory now that we no longer need it 
}

或者,如果系统的其他代码需要使用 use_factory 分配的对象,那么应该更改此函数,返回一个指向分配内存的指针。

Foo* use_factory(T arg) {
	Foo *p = factory(arg);
	// use p
	return p;  // caller must delete the memory
}

小心:管理动态内存极易出错

使用 new 和 delete 管理动态内存有 3 个常见问题:

  1. 忘记 delete 内存。这被称为“内存泄漏” (memory leak),因为内存永远不会返回自由空间中。检测内存泄漏很难,因为这种错误通常在应用程序长时间运行后内存耗尽才能检测出。
    进程结束后,申请的内存是会被释放的,但是这要依靠底层操作系统内存管理模块的实现机制。
  2. 使用一个已删除的对象。
  3. 同一个内存被删除两次。

实践:只使用智能指针,可以避免所有这些问题。仅当没有剩余的智能指针指向某内存时,智能指针才会删除该内存。

在 delete 之后重置指针的值 …

当 delete 一个指针时,该指针变为无效的。虽然指针是无效的,但在许多计算机上,指针继续保存(释放的)动态内存的地址。在 delete 之后,指针变成了所谓的悬空指针 (dangling pointer)。悬空指针指向曾保存对象但现在已无效的内存。

悬空指针具有未初始化指针的所有问题。在指针本身离开作用域之前删除指针关联的内存,可以避免悬空指针的问题。
如果需要保留指针,可以在使用 delete 之后将 nullptr 赋值给指针。这样做可以明确指出指针没有指向对象。

… 只提供了有限的保护

动态内存的基本问题是存在多个指针指向相同的内存。重置指针只对删除内存这个特定的指针有效,但对仍然指向(释放的)内存的其他指针没有影响。

int *p(new int(42));  // p points to dynamic memory
auto q = p;           // p and q point to the same memory
delete p;    // invalidates both p and q
p = nullptr; // indicates that p is no longer bound to an object

将 new 与 shared_ptr 结合使用

可以使用 new 返回的指针初始化一个智能指针。

shared_ptr<double> p1; // shared_ptr that can point at a double
shared_ptr<int> p2(new int(42)); // p2 points to an int with value 42

智能指针接受的指针是 explicit。因此,不能将内置指针隐式地转换为智能指针。必须使用直接形式的初始化方式初始化智能指针。

shared_ptr<int> p1 = new int(1024);  // error: must use direct initialization
shared_ptr<int> p2(new int(1024));   // ok: uses direct initialization

shared_ptr<int> clone(int p) {
	return new int(p); // error: implicit conversion to shared_ptr<int>
}

shared_ptr<int> clone(int p) {
	// ok: explicitly create a shared_ptr<int> from int*
	return shared_ptr<int>(new int(p)); 
}

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

表12.3 定义和改变 shared_ptr 的其他方式

操作说明
shared_ptr<T> p(q)p 管理内置指针 q 指向的对象;q 必须指向 new 分配的内存,且必须能转换成 T*。
shared_ptr<T> p(u)p 从 unique_ptr u 中取得所有权;使 u 为空。
shared_ptr<T> p(q,d)p 取得内置指针 q 指向对的象的所有权。q 必须能转换成 T*。p 将使用可调用对象 d 替换 delete 来释放 q。
shared_ptr<T> p(p2,d)如表12.2所示,p 是 shared_ptr p2 的副本,区别是 p 使用可调用对象 d 替换 delete。
p.reset()若 p 是唯一指向其对象的 shared_ptr,reset 释放 p 现存的对象。
p.reset(q)若传递了可选的内置指针 q,则使 p 指向 q,否则使 p 为空。
p.reset(q,d)若提供了 d,则调用 d 释放 q,否则使用 delete 释放 q。

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

shared_ptr 只能与其自身副本的其他 shared_ptr 协调析构。这一事实是建议使用 make_shared 而不是 new 的原因之一。这样就可以在分配对象的同时将 shared_ptr 绑定到对象。不会在无意中将同一内存绑定到多个独立创建的 shared_ptr。

// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr) {
	// use ptr
} // ptr goes out of scope and is destroyed

process 的形参是值传递,所以 process 的实参是复制到 ptr。因此,process 内 ptr 的引用计数器至少是 2。因此,当局部变量 ptr 销毁后,ptr 指向的内存不会被删除。

shared_ptr<int> p(new int(42)); // reference count is 1
process(p); // copying p increments its count; in process the reference count is 2
int i = *p; // ok: reference count is 1 

可以向 process 传递一个临时的 shared_ptr。然而,这样做很可能会出错:

int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x);  // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x;  // undefined: x is a dangling pointer!

当将一个 shared_ptr 绑定到普通指针上时,内存的管理权转移到了 shared_ptr 上。一旦将普通指针的管理权赋予给 shared_ptr,就不应该再使用内置指针访问 shared_ptr 现在指向的内存。

警告:使用内置指针访问智能指针所属的对象是危险的,因为不知道对象何时被销毁了。

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

智能指针类型定义了一个名为 get 的函数,它返回一个内置指针,指向此智能指针管理的对象。
该函数用于这种情况:需要将内置指针传递给无法使用智能指针的代码。使用 get 返回的代码不能 delete 该指针。

尽管编译器不会报错,但是将另一个智能指针绑定到 get 返回的指针是错误的:

shared_ptr<int> p(new int(42)); // reference count is 1
int *q = p.get();  // ok: but don't use q in any way that might delete its pointer
{ // new block
	// undefined: two independent shared_ptrs point to the same memory
	shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed

警告: 使用 get 传递指针的权限给代码,代码中必须确定不会 delete 指针。特别是,不要使用 get 初始化智能指针或赋值给另一个智能指针。

其他 shared_ptr 操作

可以使用 reset 将一个新指针赋值给 shared_ptr。

p = new int(1024);       // error: cannot assign a pointer to a shared_ptr
p.reset(new int(1024));  // ok: p points to a new object

与赋值类似,reset 更新引用计数,并在恰当时删除 p 指向的对象。
reset 成员通常与 unique 一起使用,控制多个 shared_ptr 之间分享对象的更改。在改变底层对象之前,检查是否是唯一的用户。如果不是,在改变前复制一份。

if (!p.unique())
	p.reset(new string(*p)); // we aren't alone; allocate a new copy
*p += newVal; // now that we know we're the only pointer, okay to change this object

智能指针与异常

使用异常处理的程序在异常发生后能够继续运行,这需要确保在异常发生时,资源能被正确释放。确保资源释放的一个简单方法是使用智能指针。

当使用智能指针时,智能指针类确保,当内存不再需要时被释放,即使块提前退出。

void f() {
	shared_ptr<int> sp(new int(42)); // allocate a new object
	// code that throws an exception that is not caught inside f
} // shared_ptr freed automatically when the function ends

当函数退出时,不管是正常处理还是由于异常,所有的局部对象会被销毁。
在上面代码中,销毁 sp 检查其引用计数,而 sp 是唯一一个指向它管理的内存的指针,所以销毁 sp 时内存会被释放。

相反,当异常发生时,直接管理的内存不会被自动释放。如果使用内置指针管理内存,异常发生在 new 之后且在对应 delete 之前,那么内存不会被释放。

void f() {
	int *ip = new int(42);     // dynamically allocate a new object
	// code that throws an exception that is not caught inside f
	delete ip;                 // free the memory before exiting
}

智能指针与哑类

分配资源但没有定义析构函数释放资源的类,可能会遇到与使用动态内存相同的错误:容易忘记释放资源;如果异常发生在资源分配与释放之间,程序会泄漏资源。

通常可以使用与管理动态内存相同的技术来管理没有良好定义的析构函数的类。
例如,假设正在使用C和C++都使用的网络库。使用该库的程序可能包含以下代码:

struct destination;  // represents what we are connecting to
struct connection;   // information needed to use the connection
connection connect(destination*);  // open the connection
void disconnect(connection);       // close the given connection
void f(destination &d /* other parameters */) {
	// get a connection; must remember to close it when done
	connection c = connect(&d);    // use the connection
	// if we forget to call disconnect before exiting f, there will be no way to close c
}

如果 connection 有一个析构函数,当 f 完成时该析构函数可以自动关闭连接。但是,connection 没有析构函数。

可以使用 shared_ptr 确保 connection 被正确关闭。

使用自己的删除代码

为了使用 shared_ptr 管理 connection,必须首先定义一个函数替换 delete。该删除器 (deleter) 函数很有可能被存储在 shared_ptr 内的指针调用。在本例中,删除器必须接受一个 connection* 类型的参数:

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

void f(destination &d /* other parameters */) {
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	// use the connection
	// when f exits, even if by an exception, the connection will be properly closed
}

警告:智能指针陷阱

为了正确地使用智能指针,必须遵守一组约定:

  • 不要使用同样的指针值初始化(或 reset)多个智能指针。
  • 不要 delete 成员函数 get() 返回的指针。
  • 不要使用 get() 初始化或 reset 另一个智能指针。
  • 如果使用 get() 返回的指针,记住:当最后的对应智能指针离开,该指针变为无效的。
  • 如果使用智能指针管理资源,而不是 new 分配的内存,记得传递一个删除器。

unique_ptr

一个 unique_ptr “占有”它指向的对象。当 unique_ptr 被销毁,它指向的对象也被销毁。

表12.4 unique_ptr 操作

操作说明
unique_ptr<T> u1空 unique_ptr,可以指向类型 T 的对象。u1 将使用 delete 释放它的指针;
unique_ptr<T, D> u2u2 使用类型 D 的可调用对象来释放它的指针。
unique_ptr<T, D> u(d)空 unique_ptr,指向类型 T 的对象,使用类型 D 的对象 d 替代 delete。
u = nullptr删除 u 指向的对象;令 u 为空。
u.release()让出 u 持有的指针的控制权;返回 u 持有的指针,令 u 为空。
u.reset()删除 u 指向的对象;
u.reset(p)如果提供了内置指针 p,令 u 指向该对象。
u.reset(nullptr)否则令 u 为空。

与 shared_ptr 不同,没有类似 make_shared 的库函数可返回 unique_ptr。相反,当定义一个 unique_ptr 时,将其绑定到 new 返回的指针上。与 shared_ptr 一样,必须使用直接的初始化形式:

unique_ptr<double> p1;  // unique_ptr that can point at a double
unique_ptr<int> p2(new int(42)); // p2 points to int with value 42 

因为 unique_ptr 占有它指向的对象,所以 unique_ptr 不支持普通的复制或赋值操作。

unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2;                    // error: no assign for unique_ptr

虽然不能复制或赋值一个 unique_ptr,但是可以通过调用 reset 或 release,将所有权从一个 (非const) unique_ptr 转移给另一个。

// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex")); // transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed

调用 release 中断 unique_ptr 与其管理的对象之间的连接。通常,release 返回的指针用于初始化或赋值另一个智能指针。这样,管理内存的责任简单地从一个智能指针转移到另一个。
但是,如果不使用其他智能指针来保存从 release 返回的指针,程序需要负责释放该资源。

p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)

传递与返回 unique_ptr

不能复制 unique_ptr 这条规则有一个例外:可以复制或赋值一个将要被销毁的 unique_ptr。最常见的例子是从一个函数中返回 unique_ptr。

unique_ptr<int> clone(int p) {
	// ok: explicitly create a unique_ptr<int> from int*
	return unique_ptr<int>(new int(p)); }

也可以返回局部对象的副本:

unique_ptr<int> clone(int p) {
	unique_ptr<int> ret(new int (p));
	// . . .
	return ret;
}

向后兼容:auto_ptr

标准库的早期版本包括名为 auto_ptr 的类,它具有 unique_ptr 一部分但不是所有的属性。特别是,不能在容器中存储 auto_ptr,也不能从函数中返回 auto_ptr。

尽管 auto_ptr 仍然是标准库的一部分,但程序中应使用 unique_ptr。

向 unique_ptr 传递删除器

默认情况下,unique_ptr 使用 delete 释放一个 unique_ptr 指向的对象。可以在 unique_ptr 中覆盖默认的删除器。
unique_ptr 管理删除器的方式与 shared_ptr 不同。

覆盖 unique_ptr 的删除器影响 unique_ptr 类型,以及构造(或 reset)对象的方式。必须在尖括号内提供 unique_ptr 指向的类型以及删除器类型。当创建或 reset 特定类型的对象时,提供该类型的可调用对象。

// p points to an object of type objT and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);

使用 unique_ptr 重写连接程序:

void f(destination &d /* other needed parameters */) {
	connection c = connect(&d);  // open the connection
	// when p is destroyed, the connection will be closed
	unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
	// use the connection
	// when f exits, even if by an exception, the connection will be properly closed
}

weak_ptr

weak_ptr 是一种智能指针,它不控制它指向的对象的生存期。weak_ptr 指向有 shared_ptr 管理的对象。将一个 weak_ptr 绑定到 shared_ptr 不会改变该 shared_ptr 的引用计数。

表12.5 weak_ptr

操作说明
weak_ptr<T> w空 weak_ptr,指向类型 T 的对象。
weak_ptr<T> w(sp)weak_ptr 指向与 shared_ptr sp 相同的对象。T 必须能够转换为 shared_ptr 指向的类型。
w = pp 可以是 shared_ptr 或 weak_ptr。赋值后,w 分享 p 的所有权。
w.reset()令 w 为空。
w.use_count()与 w 分享所有权的 shared_ptr 的数目。
w.expired()如果 w.use_count() 为 0,返回 true,否则返回 false。
w.lock()如果 expired 为 true,返回一个空 shared_ptr;否则返回一个 shared_ptr,指向 w 指向的对象。

当创建一个 weak_ptr 时,使用 shared_ptr 来初始化它。

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  // wp weakly shares with p; use count in p is unchanged

因为对象可能不存在,所以不能直接使用 weak_ptr 访问其对象。为了访问此对象,必须调用 lock。

if (shared_ptr<int> np = wp.lock()) { // true if np is not null
	// inside the if, np shares its object with p
}

检查的指针类

实例:定义一个名为 StrBlobPtr 的类,作为 strBlob 类的伴随指针类。它存储一个 weak_ptr,指向初始化用的 StrBlob 类的 data 成员。

StrBlobPtr 有两个成员:wptr,为空或者指向 StrBlob 中的 vector;curr,此对象当前指向元素的下标。
还需要一个 check 成员来验证解引用 StrBlobPtr 是否安全。

// StrBlobPtr throws an exception on attempts to access a nonexistent element
class StrBlobPtr {
public:
	StrBlobPtr(): curr(0) { }
	StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { }
	std::string& deref() const;
	StrBlobPtr& incr();       // prefix version
private:
	// check returns a shared_ptr to the vector if the check succeeds
	std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
	// store a weak_ptr, which means the underlying vector might be destroyed
	std::weak_ptr<std::vector<std::string>> wptr;
	std::size_t curr;      // current position within the array
}; 

注意,不能将一个 StrBlobPtr 绑定到 const StrBlob 上。这是因为构造函数接受类型 StrBlob 的非const 对象的引用。

check 成员必须检查它指向的 vector 是否存在:

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const {
	auto ret = wptr.lock();   // is the vector still around?
	if (!ret)
		throw std::runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw std::out_of_range(msg);
	return ret; // otherwise, return a shared_ptr to the vector
}

指针操作

std::string& StrBlobPtr::deref() const {
	auto p = check(curr, "dereference past end");
	return (*p)[curr];  // (*p) is the vector to which this object points
}

// prefix: return a reference to the incremented object
StrBlobPtr& StrBlobPtr::incr() {
	// if curr already points past the end of the container, can't increment it
	check(curr, "increment past end of StrBlobPtr");
	++curr;       // advance the current state
	return *this;
} 

为了访问 data 成员,StrBlobPtr 必须是 StrBlob 的 friend。在 StrBlob 类中增加 begin 和 end 操作返回一个指向自身的 StrBlobPtr:

// forward declaration needed for friend declaration in StrBlob
class StrBlobPtr;
class StrBlob {
	friend class StrBlobPtr;
	// other members as in § 12.1
	// return StrBlobPtr to the first and one past the last elements
	StrBlobPtr begin() { return StrBlobPtr(*this); }
	StrBlobPtr end(){
		auto ret = StrBlobPtr(*this, data->size());
		return ret;
	}
};

【C++ primer】目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值