C++ Primer 12 动态内存

动态内存

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作 自由空间。 程序用堆来存储 动态分配 的对象,即,那些在程序运行时分配的对象。

动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式的释放时,这些对象才会销毁。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。


1 动态内存和智能指针

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

如果忘记释放内存,会产生内存泄漏;如果尚有指针引用内存的情况下就释放了它,会产生引用非法内存的指针。

新的标准库提供了两种智能指针类型来管理动态对象。新标准库提供的这两种智能指针的区别在于管理低层指针的方式:shared_ptr 允许多个指针指向同一个对象;unique_ptr 则“独占”所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在 memory 头文件中。

1.1 shared_ptr 类

智能指针是模板,因此,创建智能指针时,必须提供指针指向的类型。

shared_ptr<string> p1;  // shared_ptr 可以指向 string
shared_ptr<list<int>> p1;  // shared_ptr 可以指向 int 的 list

默认初始化的智能指针中保存着一个空指针,使用智能指针的方式与普通指针类似,解引用一个智能指针返回它指向的对象。

下表为 shared_ptr 和 unique_ptr 都支持的操作:

操作含义
p -> mem等价于 (*p).mem
p.get()返回 p 中保存的指针。
swap(p, q)交换 p 和 q 中的指针
p.swap(q)交换 p 和 q 中的指针

下表为 shared_ptr 独有操作:

操作含义
make_shared<T> (args)用 args 初始化 shared_ptr
shared_ptr<T> p(q)p 时 shared_ptr q 的拷贝;此操作会递增 q 中的计数器。q 中的指针必须能转换为 T**
p = q此操作会递减 p 的引用计数,递增 q 的引用计数;若 p 的引用计数变为 0.则将其管理的原内存释放
p.unique()若 p.use_count() 为 1,返回 true,否则 返回 false
p.use_count()返回与 p 共享对象的智能指针数量
make_shared 函数

make_shared 函数在动态内存中分配一个对象并初始化它,返回指向此对象的 share_ptr。

// 指向一个数值为 42 的 int 的 shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// 指向一个数值为"9999999999"的 string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5 指向一个值初始化的 int,即,值为 0
shared_ptr<int> p5 = make_shared<int>();

类似顺序容器的 emplace 成员,make_shared 用其参数来构造指定类型的对象;例如,调用 make_shared<string> 时传递的参数必须与 string 的某个构造函数相匹配。如果我们不传递任何参数,对象就会进行值初始化。

通常使用 auto 来定义一个对象来保存 make_shared 的结果:

// p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr 的拷贝和赋值

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

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

每个 shared_ptr 都有一个关联的计数器,称其为 引用计数。无论何时拷贝一个 shared_ptr,计数器都会增加。例如,用一个 shared_ptr 初始化另一个 shared_ptr,将其作为一个参数传递给一个函数以及或作为函数返回值时,都会使关联计数器递增。

当给 shared_ptr 赋一个新的数值或者销毁一个局部的 shared_ptr(局部 shared_ptr 离开作用域),都会递减计数器。一旦一个 shared_ptr 计数器的数值变成0的时候,就会自动释放所管理的对象 。

注: 到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个 shared_ptr 指向相同的对象,并能在恰当的时候自动释放对象。

shared_ptr 自动销毁所管理的对象,并释放相关联的内存

当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会通过 析构函数 自动销毁此对象,并释放它占用的内存。

注: 析构函数一般用来释放对象所分配的资源。

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

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

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

  • 程序不知道自己需要使用多少对象
  • 程序不知道对象的准确类型
  • 程序需要在多个对象之间共享数据

如果分配的资源都与对应对象生存周期一致:

vector<string> v1;  // 空 vector
{  // 新作用域
	vector<string> v2 = {"a", "an", "the"};
	v1 = v2;  // 从 v2 拷贝元素到 v1 中
}  // v2 被销毁,其中元素也被销毁
// v1 有三个元素,是原来 v2 中元素的拷贝

如果分配资源和元对象有独立的生存周期时:

Blob<string> b1;  // 空 Blob
{  // 新作用域
	Blob<string> b2 = {"a", "an", "the"};
	b1 = b2;  // b1 和 b2 共享相同元素
}  // b2 被销毁,但 b2 中的元素不能被销毁
// b1 指向最初由 b2 创建的元素

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

1.2 直接管理内存

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

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

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

int *pi = new int;  // pi 指向一个动态分配的、未初始化的无名对象
int *pi = new int(1024);  // pi 指向的对象的值为 1024
string *ps = new string(10, '9');  // *ps 为 10个9 的字符串

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

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

用 new 分配 const 对象是合法的:

// 分配并初始化一个 const int
const int *pci = new const int(1024);
// 分配并默认初始化一个 const 的空 string 
const string *pcs = new const string;

一个动态分配的 const 对象必须进行初始化,对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针。

内存耗尽

一旦一个程序用光了它所有可用的内存,new 表达式就会失败,此时它会抛出一个类型为 bad_alloc 的异常。可以使用 定位 new 的方式来阻止它抛出异常:

// 如果分配失败,new 返回一个空指针
int *p1 = new int;  // 如果分配失败,new 抛出 std::bad_alloc
int *p2 = new (nothrow) int;  // 如果分配失败,new 返回一个空指针

定位 new 表达式允许我们向 new 传递额外的参数,如果将 nothrow 传递给 new,我们的意图是告诉它不能排除异常。

delete 释放动态内存

为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。可以通过 delete 表达式 来将动态内存归还给系统。delete 接受一个指针,指针指向我们想要释放的对象:

delete p;  // p 必须指向一个动态分配的对象或是一个空指针
指针值和 delete

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

编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些 delete 表达式,大多数编译器会编译通过,尽管它们是错误的。

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

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

对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担,调用者必须记得释放内存:

// factory 返回一个指针,指向一个动态分配的对象
Foo* factory(T arg) {
	// 视情况处理 arg
	return new Foo(arg);  // 调用者负责释放此内存
} 

void use_factory(T arg) {
	Foo *p = factory(arg);
	// 使用 p 但不 delete 它
}  // p 离开了它的作用域,但它所指向的内存没有被释放

内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
注: 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

delete 之后重置指针

当 delete 一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。指针虽然存在,但是和内存空间脱离了关系。在 delete 之后,指针就变成了** 空悬指针**,即,指向一块曾经保存数据对象但现在己经无效的内存的指针。

未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果需要保留指针,可以在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象。

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

1.3 shared_ptr 和 new 结合使用

如果不初始化一个智能指针,它就会被初始化为一个空指针。当然,也可以用new返回的指针来初始化智能指针:

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

接受指针参数的智能指针构造函数是 explicit 的(必须使用直接初始化的形式,不可以使用拷贝初始化的方式)。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

shared_ptr<int> p1 = new int(1024);  // 错误:必须使用直接初始化形式 
shared_ptr<int> p2(new int(1024));  // 正确:使用了直接初始化形式

p1 的初始化隐式地要求编译器用一个 new 返回的 int* 来创建一个 shared_ptr,由于不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错的。

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

操作含义
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
shared_ptr<T> p(p2, d)p 是 shared_ptr p2 的拷贝,唯一的区别是 p 将用可调用对象 d 来代替 delete
p.reset()若 p 是唯一指向其他对象的 shared_ptr,reset 会释放此对象
p.reset(q)令 p 指向 q
p.reset(q, d)调用 d 而不是 delete 释放 q
不要混用普通指针和智能指针

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

// 在函教被调用时 ptr 被创建并初始化
void process(shared_ptr<int> ptr)
{
	// 使用 ptr
} // ptr 离开作用域,被销毁

process 的参数是传值方式传递的,因此实参会被拷贝到 ptr 中。拷贝一个 shared_ptr 会递增其引用计数,因此,在 process 运行过程中,引用计数值至少为2(局部一个,外界一个)。当 process 结束时,ptr 的引用计数会递减,但不会变为 0。因此,当局部变量 ptr 被销毁时,ptr 指向的内存不会被释放。

使用此函数的正确方法是传递给它一个 shared_ptr:

shared_ptr<int> p(new int(42));  // 引用计数为1
process(p);  // 拷贝 p 会递增它的引用计数;在 process 中引用计数值为2
inti = *p;  // 正确:引用计数值为1

虽然不能传递给 process 一个内置指针,但可以传递给它一个(临时的) shared_ptr,这个 shared_ptr 是用一个内置指针显式构造的。但是,这样做很可能会导致错误:

int *x(new int(1024));  // 危险:x 是一个普通指针,不是一个智能指针
process(x);  // 错误:不能将 int* 转换为一个 shared_ptr<int>
process(shared_ptr<int>(x));  // 合法的,但内存会被释放!
intj=*x;  // 未定义的:x 是一个空悬指针!

在上面的调用中,将一个临时 shared_ptr 传递给 process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为 0 了。因此,当临时对象被销毁时,它所指向的内存会被释放。

但 x 继续指向(已经释放的)内存,从而变成一个空悬指针。如果试图使用 x 的值,其行为是未定义的。

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

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

智能指针类型定义了一个名为 get 的函数,它返回一个内置指针,指向智能指针管理的对象。使用 get 返回的指针的代码不能 delete 此指针。

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

shared_ptr<int> p(new int(42));  // 引用计数为 1
	int *q = p.get();  // 正确:但使用 q 时要注意,不要让它管理的指针被释放 {
	// 新程序块
	// 未定义:两个才独立的 shared_ptr 指向相同的内存
	shared_ptr<int>(q);
} // 程序块结束,q 被销毁,它指向的内存被释放 
int foo = *p;  // 未定义:p 指向的内存已经被释放了

在本例中,p 和 q 指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是 1。当 q 所在的程序块结束时,q 被销毁,这会导致 q 指向的内存被释放。从而 p 变成一个空悬指针,意味着当我们试图使用 p 时,将发生未定义的行为。而且,当 p 被销毁时,这块内存会被第二次 delete。
注: get 用来将指针的访问权限传递给代码,你只有在确定代码不会 delete 指针的情况下,才能使用 get。特别是,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。

reset

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

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

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

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

reset 包含两个操作。当智能指针中有值的时候,调用 reset 会使引用计数减 1。当调用 reset(new xxx()) 重新赋值时,智能指针首先是生成新对象,然后将旧对象的引用计数减1(如果发现引用计数为0时,则析构旧对象),然后将新对象的指针交给智能指针保管。

1.4 智能指针和异常

函数的退出有两种可能,正常处理结束 或者 发生了异常,无论哪种情况,局部对象都会被销毁。但是,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在 new 之后对应的 delete 之前发生了异常,则内存不会被释放。

智能指针和哑类

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

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

struct destination; // 表示我们正在连接什么
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 来处理一个 connection,我们必须首先定义一个函数来代替 delete。这个 删除器 函数必须能够完成对 shared_ptr 中保存的指针进行释放的操作。

在本例中,我们的删除器必须接受单个类型为 connection* 的参数:

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

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

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

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

智能指针使用规范

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

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

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<T> u1空 unique_ptr,可以指向类型为 T 的对象,使用 delete 来释放它的指针
unique_ptr<T, D> u2使用类型为 D 的可调用对象来释放它的指针
unique_ptr<T, D> d(d)用类型为 D 的对象 d 代替 delete
u = nullptr释放 u 指向的对象,将 u 置为空
u.release()u 放弃对指针的控制权,返回指针,并将 u 置空
u.reset()释放 u 指向的对象
u.reset(q)令 u 指向 内置指针 q
u.reset(nullptr)

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

// 将所有权从 p1 (指向string Stegosaurus)转移给 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 当前保存的指针并将其置为空。因此,p2 被初始化为 p1 原来保存的指针,而 p1 置为空。

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

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

虽然不能拷贝 unqiue_ptr 但是有个例外,我们可以拷贝或赋值一个将要被销毁的 unqiue_ptr,最常见的例子时从函数返回一个 unique_ptr:

unique_ptr<int> clone(int p) {
	// 正确:从 int* 创建一个 unique_ptr<int> 
	return unique_ptr<int>(new int(p));
}

还可以返回一个局部对象的拷贝:

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

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

向 unique_ptr 传递删除器

重载一个 unique_ptr 中的删除其会影响到 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) 返回一个函数类型,所以我们必须添加一个 * 来指出我们正在使用该类形的一个指针。

1.6 weak_ptr

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

操作含义
weak_ptr<T> w空 weak_ptr 可以指向类型为 T 的对象
weak_ptr<T> w(sp)与 shared_ptr sp 指向相同对象的 weak_ptr
w = pp 可以是一个 shared_ptr 或一个 weak_ptr。赋值后 w 与 p 共享对象
w.reset()将 w 置空
w.use_count()与 w 共享对象的 weak_ptr 的数量
w.expired()若 w.use_count() 为 0,返回 true,否则 返回 false
w.lock()如果 expired 为 true,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr

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

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

由于是弱共享,创建 wp 不会改变 p 的引用计数;wp 指向的对象可能被释放掉。由于对象可能不存在,我们不能使用 weak_ptr 直接访问对象,而必须调用 lock 来访问对象。 lock 函数检测 weak_ptr 指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的 shared_ptr。与任何其他 shared_ptr 类似,只要此 shared_ptr 存在,它所指向的底层对象也就会一直存在。

if (shared_ptr<int> np = wp.lock()){ // 如果 np 不为空则条件成立  
	// 在 if 中,np 与 p 共享对象  
}

在这段代码中,只有当 lock 调用返回 true 时我们才会进入 if 语句体。在 if 中,使用 np 访问共享对象是安全的。


2 动态数组

解决问题: 某些应用需要以此为很多对象分配内存的功能,例如,vector 和 string 都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。

C++ 和标准库提供了两种一次分配一个对象数组的方法。C++ 语言定义了另一个 new 表达式语法,可以分配并初始化一个对象数组。标准库中包含了一个名为 allocator 的类,允许我们将分配和初始化分离。使用 allocator 通常会提供更好的性能和更灵活的内存管理。但是,大多数应用都没有直接访问动态数组的需求,
注: 大多数情况下,应该使用标准库容器而不是动态分配的数组,因为容器更为简单、不容易出现内存管理的错误,并且有很强的性能。

2.1 new 和数组

为了让 new 分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

// 调用 get_size 确定分配多少个 int
int *pia = new int[get_size()];  // pia 指向第一个 int

方括号中的大小必须是整数,但不必是常量。也可以用一个表示类型别名来分配一个数组,这样,new 表达式中就不需要方括号了:

typedef int arrT[42];  // arrT 表示 42 个 int 的数组类型
int *p = new arrT;  // 分配一个42个 int 的数组;p 指向第一个 int

等价于

int *p new int[42];
分配一个数组会得到一个元素类型的指针

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

由于分配的内存并不是一个数组类型,因此不能对动态数组调用 begin 或 end,也不能用范围 for 语句来处理 (所谓的)动态数组中的元素。
注: 动态数组并不是数组类型。

初始化动态分配对象的数组

默认情况下,new 分配的对象,都是默认初始化的(未初始化的)。 可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。在新标准中,我们还可以提供一个元素初始化器的花括号列表(如果初始化器数目小于元素数目,剩余元素将进行值初始化,如果初始化器数目大于元素数目,则 new表达式失败,不会分配任何内存。):

int *pia = new int[10];   // 10个未初始化的 int
int *pia2 = new int[10]();  // 10个值初始化为0 的 int
// 5个 int 分别用列表中对应的初始化器初始化
int *pia3 = new int[5]{0, 1, 2, 3, 4};

虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用 auto 分配数组。

动态分配一个空数组是合法的

虽然不能创建一个大小为 0 的静态数组对象,但是调用 new[0] 是合法的:

char arr[0];  // 错误:不能定义长度为 0 的数组
char *cp = new char[0];  // 正确:但 cp 不能解引用

对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后指针一样来使用这个指针,但不能解引用这个指针。

释放动态数组

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

delete p;  // p 必须指向一个动态分配的对象或为空
delete [] pa;  // pa 必须指向一个动态分配的数组或为空

第二条语句销毁 pa 指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁, 即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。

当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在 delete 一个指向数组的指针时忽略了方括号 (或者在 delete 一个指向单一对象的指针时使用了方括号),其行为是未定义的,而且编译器很可能不会给出警告,我们的程序可能在执行过程中在没有任何警告的情况下行为异常。

智能指针和动态数组

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

// up 指向一个包含 10 个未初始化 int 的数组
unique_ptr<int[]> up(new int[10]);
up.release();  // 自动用 delete[] 销毁其指针

类型说明符中的方括号(<int[]>)指出 up 指向一个 int 数组而不是一个 int,由于 up 指向一个数组,当 up 销毁它管理的指针时,会自动使用 delete[ ]。

当一个 unique_ptr 指向一个数组时,我们不能使用点和箭头成员运算符。毕竟 unique_ptr 指向的是一个数组而不是单个对象,因此这些运算符是无意义的。另一方面,当一个 unique_ptr 指向一个数组时,我们可以使用下标运算符来访问数组中的元素

for(size_t i = 0; i != 10; ++i)
	up[i] = i;  // 为每个元素赋予一个新值

与 unique_ptr 不同,shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器:

// 为了使用 shared_ptr,必须提供一个删除器
shared_ptr<int> sp(new int[10], 
	[](int *p) {
		delete []p;
	}
);
sp.reset();  // 使用我们提供的 lambda 释放数组,它使用 delete[]

如果未提供删除器,这段代码将是未定义的。默认情况下,shared_ptr 使用 delete 销毁它指向的对象。如果此对象是一个动态数组,对其使用 delete 产生的问题与释放一个动态数组指针时忘记 [ ] 产生的问题一样。

shared_ptr 不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:

// shared_ptr 未定义下标运算符,并且不支持指针的算术运算
for(size_t i = 0; i != 10; ++i)
	*(sp.get() + i) = i;  // 使用 get 获取一个内置指针

shared_ptr 未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组面的元素,必须用 get 获取一个内置指针,然后用它来访问数组元素。

2.2 allocator 类

解决问题: 将内存分配和对象构造分离,先分配大块内存,在真正需要时才执行对象创建操作。

标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

allocator 是一个模板。为了定义一个 allocator 对象,我们必须指明这个 allocator 可以分配的对象类型。当一个 allocator 对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

allocator<string> alloc;  // 可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n);  // 分配 n 个未初始化的 string  

这个 allocate 调用为 n 个 string 分配了内存。

操作含义
allocator<T> a定义了一个名为 a 的 allocator 对象
a.allocate(n)分配了一段原始的、为构造的内存,保存了 n 个类型为 T 的对象
a.deallocate(p, n)释放从 T* 指针 p 中地址开始的内存,这块内存保存了 n 个类型为 T 的对象;p 必须是一个由 allocate 返回的指针,且 n 必须是 p 创建时所要求的大小。
a.construct(p, args)p 必须是一个类型为 T* 的指针,指向一块原始内存;arg 被传递给类型为 T 的构造函数
a.destroy§p 为类型 T* 的指针,此算法对 p 指向的对象执行析构函数
allocator 分配为构造的内存

allocator 分配的内存是未构造的。我们按需要在此内存中构造对象。在新标准库中,construct 成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。这些额外参数必须是与构造的对象的类型相无配的合法的初始化器:

allocator<string> alloc;  // 可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n);  // 分配 n 个未初始化的 string 

auto q = p;  // q 指向最后构造的元素之后的位置
alloc.construct(q++);  // *q 为空字符串
alloc.construct(q++, 10, zcr);  // *q 为 cccccccccc
alloc.construct(q++, "hi"); // *q 为 hi!

在早期版本的标准库中,construct 只接受两个参数:指向创建对象位置的指针和一个元素类型的值。因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的任何其他构造函数来构造一个元素。

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

cout << *p << endl;  // 正确:使用 string 的输出运算符
cout << *q << endl;  // 灾难:q 指向未构造的内存

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

while(q != p) 
	alloc.destroy(--q);  // 释放我们真正构造的 string

在循环开始处,q指向最后构造的元素之后的位置。我们在调用 destroy 之前对 q 进行了递减操作。因此,第一次调用 destroy 时,q 指向最后一个构造的元素。最后一步循环中我们 destroy 了第一个构造的元素,随后 q 将与 p 相等,循环结束。
注: 只能对真正构造了的元素进行 destroy 操作。

一旦元素被销毁后,就可以重新使用这部分内存来保存其他 string,也可以将其归还给系统。释放内存通过调用 deallocate 来完成:

alloc.deallocate(p, n);

我们传递给 deallocate 的指针不能为空,它必须指向由 allocate 分配的内存。而且, 传递给 deallocate 的大小参数必须与调用 allocated 分配内存时提供的大小参数具有一样的值。

拷贝和填充未初始化内存的算法

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

操作含义
uninitialized_copy(b, e, b2)从迭代器 b 和 e 指出的输入范围中拷贝元素到迭代器 b2 指定的为构造的原始内存中。
uninitialized_copy_n(b, n, b2)从迭代器 b 指向的元素开始,拷贝 n 个元素到 b2 开始的内存中
uninitialized_fill(b, e, t)在迭代器 b 和 e 指定的原始内存范围中创建,对象的值均为 t 的拷贝
uninitialized_fill_n(b, n, t)从迭代器 b 指向的内存地址开始创建 n 个对象,b 必须指向足够大的为构造的原始内存

作为一个例子,假定有一个 int 的 vector,希望将其内容拷贝到动态内存中。我们将分配一块比 vector 中元素所占用空间大一倍的动态内存,然后将原 vector 中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充:

// 分配比 vi 中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
// 通过拷贝 vi 中的元素来构造从 p 开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
// 将剩余元素初始化为42
uninitialized_fill_n(q, vi.size(), 42);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C Primer是一系列的编程教材,C Primer 5和C Primer 6是其中的两个版本。这两个版本都是以C语言为基础,旨在帮助初学者掌握C编程的基本知识和技巧。 首先,C Primer 5是早期的版本,它覆盖了C语言的基础知识并逐步介绍了更高级的编程概念和技术。这本书适合初学者,无论是学习C语言作为第一门编程语言还是已经有其他编程经验的人想要了解C语言。C Primer 5的内容涵盖了变量、数据类型、运算符、控制流、数组、指针、字符串等基本概念和操作。此外,该版本还介绍了C语言的面向对象编程概念,并提供了一些实例来帮助读者理解应用。 而C Primer 6是更新的版本,它在C Primer 5的基础上进行了更新和完善。这本书扩展了对C语言的深入介绍,并增加了一些更高级的主题,如动态内存分配、多线程编程、网络编程等。此外,C Primer 6还提供了更多的示例和练习,以帮助读者更好地掌握各种概念和技巧。 总的来说,C Primer 5和C Primer 6都是优秀的编程教材,它们分别适合不同层次的学习者。对于初学者来说,C Primer 5提供了一个很好的起点,并为他们打下了坚实的基础。而对于那些已经掌握了C基础知识的人来说,C Primer 6则是更好的选择,它深入探讨了C语言更高级的主题,帮助读者扩展和提升他们的编程能力。无论选择哪个版本,掌握C语言的基本概念和技巧对于学习其他编程语言和进行系统编程都至关重要。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值