C++内存管理之智能指针

智能指针

C++ 中的内存管理是错误和错误的常年来源。许多这些错误都是由于使用动态内存分配和指针引起的。在程序中广泛使用动态内存分配并在对象之间传递许多指针时,很难记住在正确的时间对每个指针调用 delete 一次。当多次释放动态分配的内存或使用指向已经释放的内存的指针时,可能会导致内存损坏或致命的运行时错误;当忘记释放动态分配的内存时,就会导致内存泄漏。

智能指针可帮助您管理动态分配的内存,并且是避免内存泄漏的推荐技术。从概念上讲,智能指针可以保存动态分配的资源,例如资源。当智能指针超出范围或被重置时,它可以自动释放它持有的资源。智能指针可用于管理函数范围内动态分配的资源,或作为类中的数据成员。它们还可以用于通过函数参数传递动态分配资源的所有权。标准智能指针 unique_ptr 和 shared_ptr 都在 <memory> 中定义。

默认智能指针应该是 unique_ptr。仅当确实需要共享资源时才使用 shared_ptr。

unique_ptr

unique_ptr 拥有资源的唯一所有权。当 unique_ptr 被销毁或重置时,资源会自动释放。一个优点是内存和资源总是被释放,即使在执行 return 语句或抛出异常时也是如此。例如,当一个函数有多个 return 语句时,这会简化编码,因为你不必在每个 return 语句之前释放资源。

创建 unique_ptrs

考虑以下函数,它通过在堆内存上分配一个 Simple 对象并忽略释放它来泄漏内存:

void leaky() { 
	Simple* mySimplePtr { new Simple{} };
	mySimplePtr->go(); 
}

有时你可能认为你的代码正在正确地释放动态分配的内存。不幸的是,它很可能并非在所有情况下都是正确的。采取以下功能:

void couldBeLeaky() {
	Simple* mySimplePtr { new Simple{} };
	mySimplePtr->go();
	delete mySimplePtr;
}

此函数动态分配一个 Simple 对象,使用该对象,然后正确调用 delete。但是,在这个例子中你仍然可以有内存泄漏!如果 go() 方法抛出异常,则永远不会执行 delete 调用,从而导致内存泄漏。

相反,你应该使用使用 std::make_unique() 辅助函数创建的 unique_ptr。unique_ptr 是一个通用的智能指针,可以指向任何类型的内存。这就是为什么它是一个类模板,而 make_unique() 是一个函数模板。两者都需要一个位于尖括号 < > 之间的模板参数,指定希望 unique_ptr 指向的内存类型。

以下函数使用 unique_ptr 而不是原始指针。 Simple 对象没有明确删除;但是当 unique_ptr 实例超出范围时(在函数末尾,或者因为抛出异常),它会自动在其析构函数中释放 Simple 对象。

void notLeaky() {
	auto mySimpleSmartPtr { make_unique<Simple>() };
	mySimpleSmartPtr->go();
}

make_unique() 使用值初始化。例如,基本类型被初始化为零,对象是默认构造的。如果不需要此值初始化,例如在性能关键代码中,您可以使用 C++20 中引入的新 make_unique_for_overwrite() 函数,它使用默认初始化。对于基本类型,这意味着它们根本没有被初始化并且包含内存中它们所在位置的任何内容,而对象仍然是默认构造的。

还可以通过直接调用其构造函数来创建一个 unique_ptr,如下所示。请注意,现在必须两次提及 Simple:

unique_ptr<Simple> mySimpleSmartPtr { new Simple{} };

在 C++17 之前,你必须使用 make_unique(),不仅因为只需指定一次类型,而且出于安全原因!考虑以下对名为 foo() 的函数的调用:

foo(unique_ptr<Simple> { new Simple{} }, unique_ptr<Bar> { new Bar { data() } });

如果 Simple 或 Bar 的构造函数或 data() 函数抛出异常,这取决于你的编译器优化,可能会泄漏 Simple 或 Bar 对象。使用 make_unique(),什么都不会泄漏:

foo(make_unique<Simple>(), make_unique<Bar>(data()))

从 C++17 开始,对 foo() 的两种调用都是安全的,但我仍然建议使用 make_unique(),因为代码更易于阅读。

使用 unique_ptr

标准智能指针的最大特点之一是它们提供了巨大的好处,而无需用户学习大量新语法。智能指针仍然可以像标准指针一样被解引用(使用 * 或 ->)。例如,在前面的示例中,-> 运算符用于调用 go() 方法。

get() 方法可用于直接访问底层指针。这对于将指针传递给需要原始指针的函数很有用。例如,假设你具有以下函数:

void processData(Simple* simple) { /* Use the simple pointer... */ }

然后你可以这样调用它:

processData(mySimpleSmartPtr.get());

可以释放 unique_ptr 的底层指针,并选择使用 reset() 将其更改为另一个指针。这是一个例子:

mySimpleSmartPtr.reset(); // Free resource and set to nullptr
mySimpleSmartPtr.reset(new Simple{}); // Free resource and set to a new Simple instance

还可以使用 release() 断开底层指针与 unique_ptr 的连接,release() 返回指向资源的底层指针,然后将智能指针设置为 nullptr。实际上,智能指针失去了资源的所有权,因此,当你用完它时,你有责任释放资源!这是一个例子:

Simple* simple { mySimpleSmartPtr.release() }; // Release ownership
// Use the simple pointer...
delete simple;
simple = nullptr;

unique_ptr 和 C 风格数组

unique_ptr 也能存储动态分配的旧 C 风格数组。下面的示例创建一个 unique_ptr,它包含一个由十个整数组成的动态分配的 C 样式数组:

auto myVariableSizedArray { make_unique<int[]>(10) };

与非数组情况一样,make_unique() 对数组的所有元素使用值初始化,类似于 std::vector。对于原始类型,这意味着初始化为零。从 C++20 开始,可以使用 make_unique_for_overwrite() 函数来创建没有默认初始化值的数组,这意味着原始类型未初始化。不过请记住,应尽可能避免使用未初始化的数据,因此请谨慎使用。

尽管可以使用 unique_ptr 来存储动态分配的 C 风格数组,但建议使用标准库容器,例如 std::array 或 vector。

自定义删除器

默认情况下,unique_ptr 使用标准的 new 和 delete 运算符来分配和释放内存。你可以更改此行为来使用自己的分配和释放函数。这是一个例子:

int* my_alloc(int value) {
	return new int { value };
}
void my_free(int* p) {
	delete p;
}
int main() {
	unique_ptr<int, decltype(&my_free)> myIntSmartPtr { my_alloc(42), my_free };
}

此代码使用 my_alloc() 为整数分配内存,unique_ptr 通过调用 my_free() 函数释放内存。 unique_ptr 的这个特性对于管理其他资源也很有用,而不仅仅是内存。例如,它可用于在 unique_ptr 超出范围时自动关闭文件或网络套接字或任何东西。

不幸的是,带有 unique_ptr 的自定义删除器的语法有点笨拙。您需要将自定义删除器的类型指定为模板类型参数,它应该是指向接受单个指针作为参数并返回 void 的函数的指针类型。在此示例中,使用了 decltype(&my_free) 并返回指向函数 my_free() 的指针的类型。使用带有 shared_ptr 的自定义删除器要容易得多。以下有关 shared_ptr 的部分演示了如何使用 shared_ptr 在文件超出范围时自动关闭文件。

shared_ptr

有时,多个对象或代码段需要同一个指针的副本。 unique_ptr 无法复制,因此不能用于此类情况。相反,std::shared_ptr 是一个智能指针,支持可复制的共享所有权。但是,如果有多个 shared_ptr 实例引用同一个资源,它们如何知道何时真正释放资源?这可以通过所谓的引用计数来解决,这是下一节“引用计数的必要性”的主题。但首先,让我们看看如何构造和使用 shared_ptr。

创建和使用 shared_ptr

shared_ptr 的使用方式与 unique_ptr 类似。要创建一个,您可以使用 make_shared(),这比直接创建 shared_ptr 更有效。这是一个例子:

auto mySimpleSmartPtr { make_shared<Simple>() };

从 C++17 开始,shared_ptr 可用于存储指向动态分配的 C 风格数组的指针,就像使用 unique_ptr 一样。此外,从 C++20 开始,您可以为此使用 make_shared(),就像您可以使用 make_unique() 一样。然而,即使现在可以在 shared_ptr 中存储 C 风格的数组,我仍然建议使用标准库容器而不是 C 风格的数组。

shared_ptr 也支持 get() 和 reset() 方法,就像 unique_ptr 一样。唯一的区别是调用 reset() 时,底层资源只有在最后一个 shared_ptr 被销毁或重置时才会被释放。请注意,shared_ptr 不支持 release()。可以使用 use_count() 方法来检索共享同一资源的 shared_ptr 实例的数量。

引用计数的必要性

如前所述,当具有共享所有权的智能指针(例如 shared_ptr)超出范围或被重置时,如果它是引用它的最后一个智能指针,它应该只释放引用的资源。这是如何实现的? shared_ptr 标准库智能指针使用的一种解决方案是引用计数。

作为一般概念,引用计数是一种用于跟踪正在使用的类或特定对象的实例数的技术。引用计数智能指针是一种跟踪已构造了多少智能指针来引用单个实际指针或单个对象的智能指针。每次复制这样的引用计数智能指针时,都会创建一个指向同一资源的新实例,并且引用计数会增加。当这样的智能指针实例超出范围或被重置时,引用计数就会减少。当引用计数降为零时,资源不再有其他所有者,因此智能指针释放资源。

引用计数智能指针解决了很多内存管理问题,比如二次删除。例如,假设有以下两个指向同一内存的原始指针。 Simple 类在本章前面介绍过,它只是在创建和销毁实例时打印出消息。

Simple* mySimple1 { new Simple{} };
Simple* mySimple2 { mySimple1 }; // Make a copy of the pointer.

删除两个原始指针将导致二次删除:

delete mySimple2;
delete mySimple1;

通过使用 shared_ptr 引用计数智能指针,可以避免这种双重删除:

auto smartPtr1 { make_shared<Simple>() };
auto smartPtr2 { smartPtr1 }; // Make a copy of the pointer.

所有这些只有在不涉及原始指针时才能正常工作!例如,假设使用 new 分配一些内存,然后创建两个引用相同原始指针的 shared_ptr 实例:

Simple* mySimple { new Simple{} };
shared_ptr<Simple> smartPtr1 { mySimple };
shared_ptr<Simple> smartPtr2 { mySimple };

这两个智能指针在被销毁时都会尝试删除同一个对象。

转换 shared_ptr

正如某种类型的原始指针可以转换为不同类型的指针一样,存储某种类型的 shared_ptr 也可以转换为另一种类型的 shared_ptr。当然,什么类型可以转换成什么类型​​是有限制的。并非所有转换都有效。可用于转换 shared_ptr 的函数是 const_pointer_cast()dynamic_pointer_cast()static_pointer_cast()reinterpret_pointer_cast()。它们的行为和工作类似于非智能指针转换函数 const_cast()dynamic_cast()static_cast()reinterpret_cast()

请注意,这些转换仅适用于 shared_ptr 而不适用于 unique_ptr。

别名

shared_ptr 支持所谓的别名。这允许 shared_ptr 与另一个 shared_ptr 共享指针(拥有的指针)的所有权,但指向不同的对象(存储的指针)。例如,它可以用于在拥有对象本身的同时让 shared_ptr 指向对象的成员。这是一个例子:

class Foo {
  public:
	Foo(int value) : m_data { value } { }
	int m_data;
};
auto foo { make_shared<Foo>(42) };
auto aliasing { shared_ptr<int> { foo, &foo->m_data } };

Foo 对象仅在两个 shared_ptrs(foo 和 aliasing)都被销毁时才被销毁。拥有的指针用于引用计数,而当解引用指针或在其上调用 get() 时,将返回存储的指针。

weak_ptr

C++中还有一个与 shared_ptr 相关的智能指针类,叫做 weak_ptr。 weak_ptr 可以包含对由 shared_ptr 管理的资源的引用。 weak_ptr 不拥有资源,因此不会阻止 shared_ptr 释放资源。当 weak_ptr 被销毁时(例如,当它超出范围时),weak_ptr 不会销毁指向的资源;但是,它可用于确定资源是否已被关联的 shared_ptr 释放。 weak_ptr 的构造函数需要一个 shared_ptr 或另一个 weak_ptr 作为参数。要访问存储在 weak_ptr 中的指针,您需要将其转换为 shared_ptr。有两种方法可以做到这一点:

  • 在 weak_ptr 实例上使用 lock() 方法,它返回一个 shared_ptr。如果与 weak_ptr 关联的 shared_ptr 被释放,则返回的 shared_ptr 为 nullptr。
  • 创建一个新的 shared_ptr 实例并将 weak_ptr 作为 shared_ptr 构造函数的参数。如果与 weak_ptr 关联的 shared_ptr 已被解除分配,这将引发 std::bad_weak_ptr 异常。

下面的例子演示了 weak_ptr 的使用:

void useResource(weak_ptr<Simple>& weakSimple) {
	auto resource { weakSimple.lock() };
	if (resource) {
		cout << "Resource still alive." << endl;
	} else {
		cout << "Resource has been freed!" << endl;
	}
}
int main() {
	auto sharedSimple { make_shared<Simple>() };
	weak_ptr<Simple> weakSimple { sharedSimple };
	// Try to use the weak_ptr.
	useResource(weakSimple);
	// Reset the shared_ptr.
	// Since there is only 1 shared_ptr to the Simple resource, this 	will // free the resource, even though there is still a weak_ptr alive.
	sharedSimple.reset();
	// Try to use the weak_ptr a second time.
	useResource(weakSimple);
}

这段代码的输出如下:

Simple constructor called!
Resource still alive.
Simple destructor called!
Resource has been freed!

从 C++17 开始,weak_ptr 也支持 C 风格的数组,就像 shared_ptr 一样。

传递给函数

接受指针作为其参数之一的函数只有在涉及所有权转移或所有权共享时才应该接受智能指针。要共享 shared_ptr 的所有权,只需按值接受 shared_ptr 作为参数。同样,要转移 unique_ptr 的所有权,只需按值接受 unique_ptr 作为参数即可。后者需要使用移动语义。

如果既不涉及所有权转移也不涉及所有权共享,那么该函数应该只具有对非常量的引用或对常量的引用参数,或者如果 nullptr 是该参数的有效值,则该函数应具有原始指针。拥有诸如 const shared_ptr& 或 const unique_ptr& 之类的参数类型从来没有多大意义。

从函数返回

得益于(命名的)返回值优化,第 1 章讨论的 (N)RVO 和第 9 章讨论的移动语义,标准智能指针 shared_ptr、unique_ptr 和 weak_ptr 可以轻松高效地按值从函数返回。 此时移动语义的细节并不重要。重要的是,所有这些都意味着从函数返回智能指针是高效的。例如,您可以编写以下 create() 函数并像在 main() 中演示的那样使用它:

unique_ptr<Simple> create() {
	auto ptr { make_unique<Simple>() };
	// Do something with ptr...
	return ptr;
}
int main() {
	unique_ptr<Simple> mySmartPtr1 { create() };
	auto mySmartPtr2 { create() };
}

enable_shared_from_this

std::enable_shared_from_this 派生的类允许在对象上调用的方法安全地返回指向自身的 shared_ptr 或 weak_ptr 。如果没有这个基类,返回有效的 shared_ptr 或 weak_ptr 的一种方法是将 weak_ptr 作为成员添加到类中,然后返回它的副本或返回从它构造的 shared_ptr。enable_shared_from_this 类将以下两个方法添加到从它派生的类中:

  • shared_from_this():返回共享对象所有权的shared_ptr
  • weak_from_this():返回一个跟踪对象所有权的 weak_ptr

这是一个高级特性,没有详细讨论,但下面的代码简要演示了它的使用。 shared_from_this() 和 weak_from_this() 都是公共方法。但是,您可能会发现公共接口中的 from_this 部分令人困惑,因此作为演示,以下 Foo 类定义了自己的方法 getPointer():

class Foo : public enable_shared_from_this<Foo> {
  public:
	shared_ptr<Foo> getPointer() {
		return shared_from_this();
	}
};
int main() {
	auto ptr1 { make_shared<Foo>() };
	auto ptr2 { ptr1->getPointer() };
}

请注意,仅当对象的指针已存储在 shared_ptr 中时,才可以在对象上使用 shared_from_this();否则,将抛出 bad_weak_ptr 异常。在这个例子中,在 main() 中使用 make_shared() 来创建一个名为 ptr1 的 shared_ptr,它包含一个 Foo 的实例。创建 shared_ptr 之后,就可以在该 Foo 实例上调用 shared_from_this() 了。另一方面,始终允许调用 weak_from_this(),但如果在其指针尚未存储在 shared_ptr 中的对象上调用它,它可能会返回一个空的 weak_ptr。

以下是 getPointer() 方法的完全错误的实现:

class Foo {
  public:
	shared_ptr<Foo> getPointer() {
		return shared_ptr<Foo>(this);
	}
};

如果对 main() 使用与前面所示相同的代码,则 Foo 的此实现会导致双重删除。您有两个完全独立的 shared_ptr(ptr1 和 ptr2)指向同一个对象,当它们超出范围时,它们都会尝试删除该对象。

旧的和删除的 auto_ptr

旧的 C++11 之前的标准库包含智能指针的基本实现,称为 auto_ptr。不幸的是,auto_ptr 有一些严重的缺点。这些缺点之一是它在标准库容器(如矢量)中使用时无法正常工作。C++11 和 C++14 正式弃用了 auto_ptr,自 C++17 起,它已从标准库中完全删除。它被替换为 unique_ptr 和 shared_ptr。这里提到 auto_ptr 是为了确保您了解它并确保您永远不会使用它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值