1、引言
在 C++
编程中,内存管理历来是复杂且容易出错的部分。手动管理动态分配的内存不仅会导致内存泄漏,还会引发悬空指针和双重释放等问题。如何有效地管理动态内存,避免内存泄漏和未定义行为,往往是困扰初学者和资深开发者的难题。为了解决这些问题,C++
逐步引入了智能指针。智能指针通过 RAII
模式实现自动化的内存管理,使开发者能更加专注于业务逻辑,而非担心内存的分配与释放。
智能指针包括了 std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
三种主要类型,它们分别解决了不同场景下的内存管理问题。接下来我们将逐步深入,探讨智能指针的演变历程、实现细节以及它们的应用场景。
智能指针的核心思想是利用 RAII(Resource Acquisition Is Initialization)
模式来管理资源的生命周期,确保动态分配的对象在不再使用时自动释放。智能指针包括了 std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
三种主要类型,它们分别解决了不同场景下的内存管理问题。接下来我们将逐步深入,探讨智能指针的演变历程、实现细节以及它们的应用场景。
2、智能指针的演变历程
2.1、手动内存管理时代
在早期的 C
和 C++
编程中,动态内存的分配和释放依赖开发者手动管理,通常通过 new
和 delete
来进行操作。例如:
int* ptr = new int(10);
// ...
delete ptr;
尽管这种方式看似简单,但它对开发者提出了严格的要求,即确保每次分配的内存都被正确释放。如果程序执行路径出现异常或者疏忽,未能释放的内存将造成内存泄漏。此外,重复释放同一个指针也会导致程序崩溃,增加了调试的复杂性。
2.2、智能指针的基本概念
智能指针的概念源自于传统指针的不足。普通指针指向堆上的动态内存,但需要手动管理对象的释放。如果开发者忘记释放内存或者多次释放内存,便会导致内存泄漏和未定义行为。而智能指针则将内存管理交给 C++
的自动化机制来处理。
RAII (Resource Acquisition Is Initialization)
是一种利用对象生命周期来控制程序资源,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样做法有两大好处:
- 不需要显示的释放资源
- 采用这种方式,对象所需的资源在其生命周期内始终保持有效
namespace Lenyiin
{
template <class T>
class Smart_ptr
{
public:
Smart_ptr(T *ptr)
: _ptr(ptr)
{
std::cout << "create: " << _ptr << std::endl;
}
~Smart_ptr()
{
if (_ptr)
{
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
通过使用类的特性,可以让指针在生命周期内和原生指针一样,在生命周期外通过析构函数自动释放。
int main()
{
// 智能指针 意义
// 无论是在函数正常结束, 还是抛异常, 都会导致 sp 对象的生命周期到了以后, 调用析构函数
std::cout << "---- 智能指针之前 ----" << std::endl;
{
Smart_ptr<int> sp1(new int);
*sp1 = 10;
Smart_ptr<std::pair<int, int>> sp2(new std::pair<int, int>);
sp2->first = 20;
sp2->second = 30;
}
std::cout << "---- 智能指针之后 ----" << std::endl;
return 0;
}
运行结果:
但是单纯的利用对象生命周期来控制程序资源,会产生严重的问题。
int main()
{
Smart_ptr<int> sp1(new int);
Smart_ptr<int> sp2 = sp1; // error 浅拷贝, 重复析构, 析构两次, 导致同一块内存被释放两次
return 0;
}
可能有人会有疑问,为什么智能指针不像 string、vector 等容器一样进行深拷贝?这是因为:智能指针只是托管资源空间,可以访问空间,模拟的是原生指针的行为,只是比原生指针多的是,可以在生命周期结束时自动释放资源。
由这个问题,衍生出了三种解决的方法:
- 管理权转移
auto_ptr
- 防拷贝
unique_ptr
- 引用计数
shared_ptr
2.3、C++98 和 auto_ptr 的引入
为了解决内存管理问题,C++98
引入了 auto_ptr
,这是 C++
最早期的智能指针,旨在简化动态内存的管理。
auto_ptr
的语法与其他智能指针类似,它封装了一个普通的指针,并在 auto_ptr
对象的生命周期结束时自动调用 delete
,释放动态分配的内存。然而,auto_ptr
存在一些严重的缺陷,例如它使用复制语义来转移所有权,这在共享资源的场景下极不安全。
std::auto_ptr<int> ptr1(new int(10));
std::auto_ptr<int> ptr2 = ptr1; // ptr1 不再拥有资源
在上述代码中,ptr1
的所有权被转移到 ptr2
,导致 ptr1
失去对对象的控制。这种设计使得 auto_ptr
在实际开发中较为不便,因此在 C++11 中被弃用。
2.4、C++11 引入 unique_ptr、shared_ptr、weak_ptr
随着 C++11
的到来,新的智能指针 unique_ptr
、shared_ptr
和 weak_ptr
被引入,取代了 auto_ptr
。它们通过现代 C++
的移动语义、引用计数等机制,解决了 auto_ptr
的缺陷,并提供了更加安全、灵活的内存管理方式。
unique_ptr
提供了独占所有权,避免了不必要的资源共享问题;shared_ptr
支持多对象共享同一资源,并通过引用计数控制资源释放;而 weak_ptr
则帮助开发者避免循环引用的问题。
3、auto_ptr :所有权转移智能指针
auto_ptr
是 C++98 标准中引入的智能指针,旨在通过 RAII 自动管理动态分配的内存。尽管它为 C++ 提供了自动化的内存管理机制,但由于其设计中的一些缺陷,后来被 unique_ptr
所取代,并最终在 C++11 中被标记为废弃(deprecated
),在 C++17 中被完全移除。
虽然 auto_ptr
已经被遗弃了,为了帮助理解 auto_ptr
的历史、设计、缺陷,以及为什么最终被废弃,本章我们将对其进行详细的讨论。
3.1、auto_ptr 的引入
auto_ptr
是 C++98
标准库中的一种智能指针,设计的目的是帮助程序员自动释放动态分配的内存,避免常见的内存泄漏问题。在早期的 C++
中,手动管理内存(通过 new
和 delete
)常常会引发严重的内存管理问题,特别是在异常处理和多分支逻辑中,程序员容易忘记释放内存。
3.1.1、auto_ptr 的使用
auto_ptr
的语法与其他智能指针类似,它封装了一个普通的指针,并在 auto_ptr
对象的生命周期结束时自动调用 delete
,释放动态分配的内存。
简单的 auto_ptr
使用示例:
#include <memory>
#include <iostream>
int main() {
std::auto_ptr<int> ptr(new int(10)); // 分配内存并初始化
std::cout << *ptr << std::endl; // 输出: 10
// 离开作用域时,ptr 自动释放所指向的内存
return 0;
}
在上面的例子中,auto_ptr
封装了一个动态分配的 int
,当 ptr
离开作用域时,它会自动释放内存,不需要手动调用 delete
。
3.1.2 auto_ptr 的工作原理
auto_ptr
的基本原理是通过 RAII(Resource Acquisition Is Initialization)
来管理资源。它在构造时接管动态分配的内存,并在析构时自动调用 delete
。这样可以确保即使发生异常,内存也能被正确释放,减少内存泄漏的风险。
3.2、auto_ptr 的缺陷
尽管 auto_ptr
的引入为自动化内存管理提供了一种解决方案,但它存在严重的缺陷,尤其是在所有权语义和复制行为上。auto_ptr
的最主要问题是它的 所有权转移语义 和 复制行为,这导致了许多意外的错误和内存问题。
3.2.1、所有权转移(Move Semantics)
auto_ptr
的核心问题在于它的复制语义。当一个 auto_ptr
对象被复制时,所有权会被转移到新的 auto_ptr
,而原来的 auto_ptr
将不再持有该指针。这种所有权的隐式转移在实际使用中引发了很多问题。
示例代码:
std::auto_ptr<int> ptr1(new int(10));
std::auto_ptr<int> ptr2 = ptr1; // 所有权从 ptr1 转移到 ptr2
std::cout << (ptr1.get() == nullptr) << std::endl; // 输出: 1 (ptr1 为空)
std::cout << *ptr2 << std::endl; // 输出: 10
在上面的代码中,ptr2
复制了 ptr1
,但实际上所有权发生了转移,导致 ptr1
不再拥有该资源。虽然这在某些场景下是有用的(比如在传递所有权时),但它违反了传统 C++ 中的复制语义。通常情况下,复制一个对象意味着两个对象共享相同的值或资源,但 auto_ptr
的行为不同,这导致了难以预料的错误。
3.2.2、不适用于标准容器
由于 auto_ptr
的复制行为,它无法存储在 C++
的标准容器中(如 std::vector
、std::list
)。标准容器依赖于复制语义来管理其元素,而 auto_ptr
的所有权转移特性与此不兼容。
示例代码:
#include <memory>
#include <vector>
int main() {
std::vector<std::auto_ptr<int>> vec;
vec.push_back(std::auto_ptr<int>(new int(10))); // 错误!所有权转移破坏了容器的语义
}
在这个例子中,auto_ptr
无法在标准容器中使用,因为在 push_back
操作时会发生所有权转移,破坏了容器的元素管理机制。这也是 auto_ptr
最终被废弃的原因之一。
3.3、模拟实现 auto_ptr
namespace Lenyiin
{
// C++98 auto_ptr
// 1. 管理权转移,早起设计缺陷,一般公司都明令禁止使用它
// 缺陷:ap2 = ap1 场景下 ap1 就悬空了,访问就会报错,如果不熟悉它的特性就会被坑
template <class T>
class Auto_ptr
{
public:
// 默认构造
Auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{
}
// 拷贝构造
Auto_ptr(Auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
std::cout << "拷贝构造, 管理权转移" << std::endl;
ap._ptr = nullptr;
}
// ap1 = ap2
Auto_ptr<T>& operator=(const Auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
{
std::cout << "赋值拷贝, delete: " << _ptr << std::endl;
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
// 析构函数
~Auto_ptr()
{
if (_ptr)
{
std::cout << "析构 delete: " << _ptr << std::endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
4、unique_ptr:独占所有权智能指针
由于 auto_ptr
的设计缺陷,C++11
标准引入了 unique_ptr
,并逐渐取代了 auto_ptr
。unique_ptr
解决了 auto_ptr
的所有权转移问题,并通过 移动语义 提供了更加安全和高效的内存管理方式。
std::unique_ptr
具有独占所有权的特性。它意味着在任何时刻,只有一个 unique_ptr
能够管理某个资源,这确保了资源不会被多个对象意外共享。
4.1、unique_ptr 的优势
- 防止内存泄漏:
unique_ptr
能在对象超出作用域时自动释放内存。 - 严格的独占所有权:
unique_ptr
仅允许一个智能指针持有资源的所有权,任何复制行为都被禁用。只有通过 移动 才能转移所有权,这样的设计避免了auto_ptr
的隐式所有权转移问题。 - 支持标准容器:由于
unique_ptr
禁用了复制,标准容器可以安全地存储unique_ptr
,因为标准容器依赖于移动语义,而不是复制语义。 - 性能优化:与
auto_ptr
不同,unique_ptr
支持更高效的内存管理,特别是在多线程环境中。 - 线程安全:由于
unique_ptr
没有共享资源的概念,因此天然是线程安全的。
4.2、unique_ptr 的基本特性
unique_ptr
实现了独占所有权语义,任何时候只能有一个 unique_ptr
实例管理某个资源。它支持 移动语义,允许通过 移动构造 或 移动赋值 将资源的所有权从一个 unique_ptr
转移到另一个 unique_ptr
。
4.2.1、unique_ptr 的构造与析构
unique_ptr
的构造函数允许传递一个原始指针来初始化它。当 unique_ptr
对象超出作用域时,它的析构函数会自动释放持有的资源(通过 delete
或 delete[]
调用)。
示例代码:
#include <iostream>
#include <memory>
int main()
{
{
std::unique_ptr<int> p1(new int(10)); // p1 独占一个 int 资源
std::cout << *p1 << std::endl; // 输出 10
}
// p1 超出作用域,资源被自动释放
return 0;
}
在这个示例中,unique_ptr
在超出作用域时自动调用其析构函数,释放动态分配的内存。
4.2.2、禁用复制语义
unique_ptr
禁用了复制构造函数和复制赋值运算符,因此无法通过复制操作共享所有权。这一设计确保了资源的独占所有权,避免了内存错误。
示例代码:
std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1; // 错误:不能复制 unique_ptr
尝试复制 unique_ptr
会导致编译错误,避免了隐式的所有权转移问题
4.2.3、移动语义支持
尽管 unique_ptr
禁用了复制操作,但它支持 移动语义,允许通过移动构造或移动赋值将所有权从一个 unique_ptr
转移到另一个。
示例代码:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // 所有权从 p1 转移到 p2
if (p1 == nullptr)
{
std::cout << "p1 为空" << std::endl; // p1 不再拥有资源
}
std::cout << *p2 << std::endl; // 输出 10
return 0;
}
在这个例子中,std::move(p1)
将 p1
中的资源所有权转移给 p2
,之后 p1
变为空指针,且不再拥有该资源。
#include <memory>
#include <vector>
int main()
{
{
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10)); // 安全!使用移动语义
}
// 超出作用域,资源被自动释放
}
在这个例子中,unique_ptr
可以安全地存储在 std::vector
中,并且通过移动语义来传递所有权,不会导致 auto_ptr
的那种隐式所有权转移问题。
4.2.4、自定义删除器
在某些特殊场景下,资源的释放方式可能需要自定义。unique_ptr
允许通过自定义删除器来管理资源的释放。自定义删除器可以是函数指针或函数对象。
struct Deleter {
void operator()(int* p) const
{
std::cout << "Deleting int pointer: " << *p << std::endl;
delete p;
}
};
int main() {
std::unique_ptr<int, Deleter> ptr(new int(100)); // 使用自定义删除器
}
自定义删除器的作用是在 unique_ptr
释放资源时调用特定的逻辑,适用于需要特殊清理操作的资源管理场景。
4.2.5、数组形式的 unique_ptr
除了管理单一对象,unique_ptr
还可以用于管理动态分配的数组。在这种情况下,需要在声明 unique_ptr
时使用数组版本的 unique_ptr
,并且删除器会自动调用 delete[]
。
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10); // 动态分配一个数组
arr[0] = 1;
arr[1] = 2;
std::cout << arr[0] << ", " << arr[1] << std::endl;
在数组形式的 unique_ptr
中,访问数组元素的方式与常规指针相同,但内存管理是自动化的。
4.2.6、空指针安全
与原始指针不同,unique_ptr
是空指针安全的。即使 unique_ptr
不指向任何对象,它仍然是安全的,不会导致崩溃或未定义行为。
std::unique_ptr<int> ptr; // 空指针
if (!ptr) {
std::cout << "Pointer is null." << std::endl;
}
这使得 unique_ptr
的使用更加健壮,减少了空指针引发的程序崩溃的风险。
4.3、unique_ptr 的使用场景
由于其独占所有权和自动管理资源的特性,unique_ptr
适用于不需要共享资源的场景。例如,某个类内部的资源管理可以通过 unique_ptr
来实现,以确保对象的生命周期结束时,资源被正确释放。unique_ptr
适用于以下场景:
4.3.1、动态内存管理
当需要管理动态分配的内存并确保其自动释放时,unique_ptr
是非常合适的选择。例如在函数内部分配的临时对象,需要在函数结束时释放,可以使用 unique_ptr
来确保正确管理内存。
4.3.2、管理资源句柄
除了管理动态内存外,unique_ptr
也可以用于管理其他需要手动释放的资源,如文件句柄、网络连接、锁等。通过自定义删除器(deleter)
,可以灵活地释放不同类型的资源。
示例代码:
#include <iostream>
#include <memory>
#include <cstdio>
struct FileDeleter {
void operator()(FILE* file) const
{
if (file)
{
std::fclose(file);
std::cout << "文件已关闭" << std::endl;
}
}
};
int main() {
std::unique_ptr<FILE, FileDeleter> file(std::fopen("example.txt", "r"));
// 当 file 超出作用域时,FileDeleter 会自动关闭文件
return 0;
}
在这个例子中,自定义的 FileDeleter
确保在 unique_ptr
超出作用域时,自动调用 fclose
关闭文件。
4.3.3、用于标准容器中
由于 unique_ptr
支持移动语义,因此它可以安全地存储在标准容器(如 std::vector
)中。相比于 auto_ptr
,unique_ptr
不会在存储过程中发生所有权隐式转移,确保了容器的正确性。
示例代码:
#include <iostream>
#include <memory>
#include <vector>
int main() {
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10)); // 安全地存储 unique_ptr
for (const auto& elem : vec) {
std::cout << *elem << std::endl; // 输出 10
}
return 0;
}
通过 std::make_unique
创建 unique_ptr
,并将其存储在容器中,既能安全管理内存,又能避免复杂的所有权问题。
4.3.4、内存池与 unique_ptr 的结合
在高性能需求的场景中,内存池技术可以与 unique_ptr
结合使用,以减少频繁的内存分配与释放带来的开销。通过自定义分配器,我们可以将 unique_ptr
的内存管理交给内存池,从而提高性能。
4.3.5、函数返回值
当一个函数返回动态分配的对象时,使用 unique_ptr
可以避免手动管理资源,并明确表示返回值的所有权转移。
std::unique_ptr<int> createInt(int value)
{
return std::make_unique<int>(value); // 返回 unique_ptr,自动管理内存
}
这种方式比返回原始指针要更加安全,因为调用者不需要显式管理资源释放。
4.4、unique_ptr 的基本实现
为了更好地理解 unique_ptr
的工作原理,我们可以分析其内部实现。unique_ptr
依赖于模板特性,并使用了移动语义来确保其独占所有权。
unique_ptr
是一个模板类,它封装了一个指针,并在析构时自动调用删除器来释放该指针所指向的资源。下面是一个简化的 unique_ptr
实现:
namespace Lenyiin
{
template <class T, class Deleter = std::default_delete<T>>
class Unique_ptr {
private:
T* ptr; // 原始指针
Deleter deleter; // 自定义删除器
public:
// 构造函数
explicit Unique_ptr(T* p = nullptr)
: ptr(p)
{}
// 禁用拷贝构造函数和赋值操作符
Unique_ptr(const Unique_ptr&) = delete;
Unique_ptr& operator=(const Unique_ptr&) = delete;
// 支持移动构造函数和移动赋值
Unique_ptr(Unique_ptr&& other) noexcept
: ptr(other.ptr)
{
other.ptr = nullptr;
}
// 移动赋值运算符
Unique_ptr& operator=(Unique_ptr&& other) noexcept
{
if (this != &other) {
reset();
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 析构函数
~Unique_ptr()
{
deleter(ptr); // 调用删除器释放资源
}
// 重载 * 和 -> 操作符
T& operator*() const
{
return *ptr;
}
T* operator->() const
{
return ptr;
}
// 获取底层指针
T* get() const
{
return ptr;
}
// 释放所有权
T* release()
{
T* oldPtr = ptr;
ptr = nullptr;
return oldPtr;
}
// 重置指针
void reset(T* p = nullptr)
{
delete ptr;
ptr = p;
}
};
}
unique_ptr
的核心是原始指针 ptr
和删除器 deleter
。它禁用了复制语义,通过移动语义来传递所有权。reset()
方法允许用户手动重置指针并释放旧资源,而 release()
方法则可以释放所有权,并让用户手动管理指针的生命周期。
unique_ptr
的移动语义通过移动构造函数和移动赋值操作来实现。当资源的所有权被转移时,原来的智能指针会变成空指针,而新的智能指针则接管资源。移动构造函数和移动赋值操作都会将原智能指针中的资源指针设为 nullptr
,确保原智能指针不再持有资源。这样可以避免重复释放资源的风险。
5、shared_ptr:共享所有权智能指针
std::shared_ptr
是 C++11
引入的另一种智能指针,允许多个 shared_ptr
对象共享同一个资源。其内部通过引用计数来管理资源的生命周期,当引用计数归零时,资源将被自动释放。简化了动态内存的管理并显著降低了内存泄漏的风险。
本章将深入探讨 shared_ptr
的使用场景、实现原理、性能特点,以及常见的陷阱和最佳实践。
5.1、什么是 shared_ptr
shared_ptr
是 C++
标准库中的一种智能指针,允许多个指针对象共同拥有动态分配的对象。它通过维护一个引用计数器来跟踪有多少个 shared_ptr
指向同一个对象。当最后一个 shared_ptr
离开作用域时,引用计数归零,自动删除指向的对象。这种机制确保了对象在不再被使用时得到正确销毁,避免了内存泄漏。
shared_ptr
的核心特性
- 引用计数:
shared_ptr
使用引用计数器来记录有多少个指针拥有同一个对象。当引用计数变为零时,shared_ptr
会自动销毁该对象。 - 共享所有权:与
unique_ptr
的独占所有权不同,shared_ptr
允许多个指针共享对同一对象的所有权。 - 线程安全:
shared_ptr
的引用计数是线程安全的,确保在多线程环境下可以安全地共享同一对象。
5.2、shared_ptr 的创建与使用
要创建一个 shared_ptr
,我们可以直接使用构造函数,或者更推荐使用 std::make_shared
函数。这种方式不仅更加简洁,还能提升性能,因为它减少了内存分配次数。
5.2.1、创建 shared_ptr
#include <memory>
#include <iostream>
int main()
{
// 使用 make_shared 创建
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::cout << "Shared pointer value: " << *sp1 << std::endl;
return 0;
}
在上面的例子中,std::make_shared
函数为我们创建了一个管理动态内存的 shared_ptr
。它的内部机制可以更高效地分配内存,因为它将对象本身和引用计数器一起分配,而不是分开分配。
5.2.2、引用计数的示例
#include <memory>
#include <iostream>
int main()
{
std::shared_ptr<int> sp1 = std::make_shared<int>(10); // 引用计数为 1
{
std::shared_ptr<int> sp2 = sp1; // 引用计数为 2
std::cout << "Reference count inside block: " << sp1.use_count() << std::endl;
} // 离开作用域,sp2 销毁,引用计数变为 1
std::cout << "Reference count outside block: " << sp1.use_count() << std::endl;
return 0;
}
在这个例子中,当 sp2
复制了 sp1
后,引用计数从 1 变为 2。当 sp2
离开作用域时,引用计数会自动减少回 1。shared_ptr
通过 use_count()
函数可以查询当前的引用计数。
5.3、shared_ptr 的实现原理
在本节中,我们将深入探讨如何手动实现一个简化版的 shared_ptr
。通过这个实现,我们可以更好地理解 shared_ptr
的内部机制和运作原理。请注意,下面的实现只是一个简化版本,真实的标准库实现可能包含更多的细节和优化。
5.3.1、shared_ptr 的内部结构
一个典型的控制块可能包含以下成员:
- 对象指针:指向动态分配的对象。
- 引用计数器:跟踪
shared_ptr
的数量。 - 互斥锁:多线程高并发时保证线程安全
控制块的设计使得 shared_ptr
可以灵活地管理资源,不论是简单的对象还是自定义的资源(如文件句柄、网络连接)。
5.3.2、 shared_ptr 的生命周期
shared_ptr
的生命周期由引用计数决定。当引用计数器归零时,shared_ptr
会自动调用对象的析构函数并释放所管理的内存资源。以下是 shared_ptr
生命周期的几个重要阶段:
- 创建:当
shared_ptr
创建时,控制块中的引用计数初始化为 1。 - 复制:当
shared_ptr
被复制时,引用计数增加。 - 销毁:当
shared_ptr
被销毁时,引用计数减少。当引用计数为 0 时,控制块会调用对象的析构函数。
5.3.3、shared_ptr 的实现
shared_ptr
的实现包括构造函数、析构函数、拷贝构造函数、赋值操作符等。下面是简化版的 shared_ptr
实现:
namespace Lenyiin
{
// C++11 Shared_Ptr
// 引用计数,可以拷贝
// 缺陷:循环引用
template <class T>
class Shared_ptr
{
public:
// 普通构造
Shared_ptr(T *ptr = nullptr)
: _ptr(ptr), _pcount(new int(1)), _pmtx(new std::mutex)
{}
// 拷贝构造
Shared_ptr(const Shared_ptr<T>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx)
{
add_ref_count();
}
// 赋值操作
// sp1 = sp2
Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
{
if (this != &sp)
{
// 减减引用计数,如果我是最后一个管理资源的对象,则释放资源
release();
// 我开始和你一起管理资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
add_ref_count();
}
return *this;
}
void add_ref_count()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void release()
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)
{
if (_ptr)
{
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
_pmtx = nullptr;
}
}
// 析构函数
~Shared_ptr()
{
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
T* get_ptr() const
{
return _ptr;
}
private:
T* _ptr;
// 记录有多少个对象一起共享管理资源, 最后一个析构释放资源
int* _pcount;
//
std::mutex* _pmtx;
};
}
以上代码实现了一个简化版的 shared_ptr
,包括基本的引用计数管理、控制块和指针管理。这个实现演示了 shared_ptr
的核心概念和工作原理。实际的标准库实现可能会有更多的细节和优化,但本实现提供了一个理解 shared_ptr
内部机制的良好基础。
5.3.4、shared_ptr 的功能测试
为了验证我们的 shared_ptr
实现的正确性,我们可以编写一些测试代码来检查基本的功能,包括构造、拷贝、赋值和析构。
#include <cassert>
int main() {
// 测试构造函数
shared_ptr<int> sp1(new int(10));
assert(*sp1 == 10);
// 测试指针计数
std::cout << "sp1: " << sp1.use_count() << std::endl;
// 测试拷贝构造函数
shared_ptr<int> sp2(sp1);
assert(*sp2 == 10);
assert(*sp1 == 10);
std::cout << "sp2: " << sp2.use_count() << std::endl;
// 测试赋值操作符
shared_ptr<int> sp3;
sp3 = sp1;
assert(*sp3 == 10);
assert(*sp1 == 10);
std::cout << "sp3: " << sp3.use_count() << std::endl;
return 0;
}
5.4、shared_ptr 的最佳实践
为了充分发挥 shared_ptr
的优势,同时避免其潜在的陷阱,以下是一些最佳实践:
5.4.1、复杂对象的生命周期管理
shared_ptr
的典型应用场景是在复杂的系统中,多个对象需要共享同一个资源时。例如,在 GUI 应用程序中,多个控件可能共享同一个数据模型。当某个控件被销毁时,数据模型应该继续存在,直到最后一个控件不再需要它。这种场景中,shared_ptr
可以确保数据模型的生命周期正确管理。
5.4.2、与 weak_ptr 结合使用
在某些场景下,可能需要打破对象之间的循环引用。例如,两个对象 A
和 B
相互拥有对方的指针,如果都使用 shared_ptr
,将导致引用计数永远不会归零,资源得不到释放。为了解决这种问题,C++
提供了 weak_ptr
(弱指针)来管理弱引用。weak_ptr
不会增加引用计数,可以用来打破循环依赖。
#include <memory>
#include <iostream>
struct B; // 前向声明
struct A {
std::shared_ptr<B> bptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
struct B {
std::weak_ptr<A> aptr; // 使用 weak_ptr 打破循环依赖
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->bptr = b;
b->aptr = a; // 若使用 shared_ptr,会造成循环依赖
return 0;
}
在这个例子中,A
和 B
相互引用。通过将 B
的指针声明为 weak_ptr
,打破了循环依赖,从而允许正确释放对象。
5.4.3、使用 std::make_shared 创建 shared_ptr
std::make_shared
是创建 shared_ptr
的推荐方式。它不仅能减少内存分配次数,还能提高性能和安全性。避免直接使用 new
创建 shared_ptr
。
std::shared_ptr<int> sp = std::make_shared<int>(42); // 推荐方式
5.4.4、避免不必要的复制
尽量避免在函数参数和返回值中使用 shared_ptr
的复制操作。如果需要传递或返回 shared_ptr
,考虑使用 const std::shared_ptr<T>&
或 std::shared_ptr<T>&&
(右值引用)。
void process(const std::shared_ptr<int>& sp)
{
// 只读访问,避免不必要的复制
}
5.4.5、理解 shared_ptr 的生命周期
要清楚 shared_ptr
的生命周期规则。在 shared_ptr
的生命周期内,不应访问已被销毁的对象。对 shared_ptr
的所有权和生命周期有清晰的理解,可以避免常见的错误和资源泄漏。
5.4.6、weak_ptr 的锁定机制
weak_ptr
的一个重要特性是它不会直接访问资源,而是需要通过 lock()
方法将自身转化为 shared_ptr
,以安全地访问资源。这样可以确保资源在使用时仍然有效,而当资源已经被释放时,lock()
返回的 shared_ptr
将为空。
std::shared_ptr<int> sptr = std::make_shared<int>(42);
std::weak_ptr<int> wptr = sptr;
if (std::shared_ptr<int> locked = wptr.lock()) {
std::cout << "Resource is still alive: " << *locked << std::endl;
} else {
std::cout << "Resource has been released" << std::endl;
}
上面的代码通过 lock()
方法来获取资源,并且检查资源是否已经被释放。如果资源已经被释放,locked
将为 nullptr
,从而避免了访问已释放内存的风险。
5、智能指针总结
在 C++ 中,智能指针提供了一个管理动态内存的安全机制,避免了内存泄漏和悬挂指针问题。C++ 标准库中提供了多种智能指针,其中最常用的包括 auto_ptr
、unique_ptr
和 shared_ptr
。每种智能指针都有其独特的特性和适用场景。以下是对这三种智能指针的总结。
5.1、auto_ptr
auto_ptr
是 C++98
标准引入的智能指针,用于自动管理动态分配的内存。它实现了所有权转移,但由于其不符合现代 C++ 的设计理念和安全要求,在 C++11
中被标记为废弃,并在 C++17
中被移除。
特点:
- 所有权转移:
auto_ptr
支持通过拷贝构造和赋值操作符转移所有权。原智能指针在转移后变为nullptr
。 - 不安全:由于所有权转移的特性,
auto_ptr
可能导致意外的资源管理错误和潜在的悬挂指针问题。 - 废弃:
auto_ptr
已被C++11
标准标记为废弃,建议使用unique_ptr
或shared_ptr
替代。
5.2、unique_ptr
unique_ptr
是 C++11
引入的智能指针,提供了独占所有权的管理机制。它保证了在任何时刻只有一个 unique_ptr
拥有对象的所有权,从而避免了资源泄漏。
特点:
- 独占所有权:
unique_ptr
只允许一个unique_ptr
拥有对象的所有权,不能被拷贝,只能通过move
操作转移所有权。 - 资源释放:当
unique_ptr
被销毁时,它会自动释放所管理的对象。 - 高效:
unique_ptr
不需要额外的内存来存储引用计数,性能开销较小。
5.3、shared_ptr
shared_ptr
是 C++11
引入的智能指针,允许多个 shared_ptr
实例共享同一个对象的所有权。它使用引用计数机制来管理对象的生命周期,确保在最后一个 shared_ptr
被销毁时才释放对象。
特点:
- 共享所有权:多个
shared_ptr
可以共享同一个对象的所有权,通过引用计数来跟踪对象的生命周期。 - 线程安全:引用计数的更新是线程安全的。
- 性能开销:需要额外的内存来存储引用计数,性能开销较
unique_ptr
大。 - 循环引用:需要小心处理循环引用问题,可以使用
weak_ptr
来打破循环引用。
5.4、总结
auto_ptr
:- 优点:简单易用,早期
C++
的智能指针实现。 - 缺点:不安全,所有权转移可能导致悬挂指针。
- 状态:已废弃,不建议使用。
- 优点:简单易用,早期
unique_ptr
:- 优点:独占所有权,资源管理简单高效,无需引用计数。
- 缺点:不能被拷贝,只能通过
move
转移所有权。 - 适用场景:需要独占所有权的场景,例如局部对象和工厂模式。
shared_ptr
:- 优点:支持共享所有权,通过引用计数管理对象生命周期,线程安全。
- 缺点:性能开销较大,可能存在循环引用问题。
- 适用场景:需要多个对象共享同一个资源的场景,例如观察者模式和缓存系统。
智能指针是 C++
内存管理中的重要工具,它们通过 RAII
模式和引用计数机制极大地简化了内存的分配和释放。在现代 C++ 中,unique_ptr
、shared_ptr
和 weak_ptr
分别解决了独占所有权、共享所有权和循环引用等问题,成为了 C++
编程中不可或缺的部分。
了解这些智能指针的特性和使用场景,可以帮助开发者选择合适的智能指针,提高代码的安全性和可维护性。在实际开发中,选择合适的智能指针来管理资源,可以有效地避免内存泄漏和资源管理错误。
通过本文的详细讲解和代码示例,我们不仅介绍了智能指针的基础知识,还探讨了其内部实现和自定义实现。希望读者在阅读本文后,能够对智能指针有一个更加全面和深入的理解,并能够在实际开发中灵活运用智能指针管理内存。
希望这篇博客对您有所帮助,也欢迎您在此基础上进行更多的探索和改进。如果您有任何问题或建议,欢迎在评论区留言,我们可以共同探讨和学习。更多知识分享可以访问我的个人博客网站 : https://blog.lenyiin.com/ 。