1 背景

在C++中,动态内存分配通常通过new关键字完成,而释放内存则需要使用delete。然而,这种手动管理内存的方式容易引发多种问题,例如:

  • 内存泄漏:忘记调用delete,导致分配的内存无法回收。
  • 野指针:释放内存后,仍然使用指向已释放内存的指针。
  • 重复释放:多次调用delete释放同一块内存,导致未定义行为。

为了解决这些问题,C++11引入了智能指针,它们通过RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制自动管理内存,从而避免了上述问题。

2 定义

智能指针是行为类似于指针的类对象。在C++编程中,内存管理一直是一个重要且容易出错的环节。C++11引入了智能指针的概念,利用对象的生命周期来管理资源,构造函数获取资源,析构函数释放资源,基于RAII机制实现了自动内存管理。

  • 作用:
    解决我们给指针分配堆空间的时候,有可能程序员粗心大意忘记delete,从而导致内存泄漏(可以使用的内存越来越少,不够用)
  • 原理:
    把指针分配堆空间以及释放堆空间操作封装到一个类(模板类)的构造函数和析构函数中,所谓的智能指针就是类的对象,智能指针对象的生命周期跟堆空间的生命周期保持一致

3 种类

std::unique_ptr适用于独占所有权的场景,
std::shared_ptr适用于共享所有权的场景,
std::weak_ptr则用于解决循环引用问题。

3.1 auto_ptr:

已经不用了;实现了独占式拥有的概念,同一时间只能由一个只能指针可以指向该对象;但auto_ptr已经被C++11放弃,其主要问题在于:对象所有权的转移,比如在函数传参的过程中,对象所有权不会返还,从而存在潜在的内存崩溃的问题;不能指向数组,也不能作为STL容器的成员。

模拟实现

template
class auto_ptr {
private:
T* ptr; // 指向动态分配的对象
public:
// 构造函数
auto_ptr(T* p = nullptr) : ptr(p) {}
// 拷贝构造函数(所有权转移)
auto_ptr(auto_ptr& other) : ptr(other.ptr) {
other.ptr = nullptr; // 转移所有权
}
// 拷贝赋值操作(所有权转移)
auto_ptr& operator=(auto_ptr& other) {
if (this != &other) {
delete ptr; // 释放当前管理的对象
ptr = other.ptr; // 转移所有权
other.ptr = nullptr;
}
return *this;
}
// 析构函数
~auto_ptr() {
delete ptr; // 释放管理的对象
}
// 重载解引用操作符
T& operator*() const { return *ptr; }
// 重载箭头操作符
T* operator->() const { return ptr; }
// 释放所有权
void release() {
ptr = nullptr;
}
// 交换两个 auto_ptr 的所有权
void swap(auto_ptr& other) {
std::swap(ptr, other.ptr);
}
// 获取当前管理的指针
T* get() const { return ptr; }
// 重置指针
void reset(T* p = nullptr) {
if (ptr != p) {
delete ptr; // 释放当前管理的对象
ptr = p;
}
}
};

特点:

  • 独占所有权:std::auto_ptr不允许复制,但可以移动。这意味着你不能有两个std::auto_ptr同时指向同一个对象。
  • 自动释放:当std::auto_ptr超出作用域时,它会自动调用delete释放其管理的对象。
  • 轻量级:std::auto_ptr的实现非常轻量级,几乎不增加额外的性能开销。

缺点:

  • 不支持数组:std::auto_ptr不能用于管理动态分配的数组。
  • 不支持自定义删除器:std::auto_ptr不支持自定义删除器,这在某些情况下会限制其灵活性。
  • 不支持移动语义:std::auto_ptr的复制构造函数和赋值操作符会转移所有权,而不是真正地“移动”对象,这在C++11的移动语义中显得不够自然。

3.2 unique_ptr

独占式指针,同一时刻只能有一个指针指向同一个对象;与所指对象的内存绑定紧密,禁止其他智能指针与其他共享同一个对象。也就是同一时间只能有一个智能指针可以指向该对象;独占的意思是不可以复制(拷贝构造和拷贝复制),但是我们可以利用std::move将其转移给其他unique_ptr(可以移动构造和移动赋值)。一旦转移,这个所有权就会失去,除非被显示归还;从实现上来讲,unique_ptr是一个删除了拷贝构造函数,保留了移动构造函数的指针类型。可以使用右值对unique_ptr进行构造。

模拟实现思路 指针:用对象的生命周期管理指针。 独占式:拷贝构造函数和拷贝赋值运算符被禁用(= delete),防止多个 unique_ptr 同时管理同一个对象。 支持移动:移动构造函数和移动赋值运算符转移所有权。 自动释放:在析构函数中释放对象。

template
class unique_ptr {
private:
T* ptr; // 存储指向对象的指针
// 私有释放函数
void release() {
if (ptr) {
delete ptr; // 释放对象
ptr = nullptr; // 置空指针
}
}
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) {
release(); // 释放当前对象
ptr = other.ptr; // 转移所有权
other.ptr = nullptr; // 原对象置空
}
return *this;
}
// 析构函数
~unique_ptr() {
release(); // 自动释放对象
}
// 重载解引用操作符
T& operator*() const { return *ptr; }
// 重载箭头操作符
T* operator->() const { return ptr; }
};

特点:

  • 独占所有权:std::unique_ptr不允许复制,但可以移动。这意味着你不能有两个std::unique_ptr同时指向同一个对象。
  • 自动释放:当std::unique_ptr超出作用域时,它会自动调用delete释放其管理的对象。
  • 轻量级:std::unique_ptr的实现非常轻量级,几乎不增加额外的性能开销。

使用场景:

  • 当你需要独占一个对象的所有权时,例如在单线程环境中管理动态分配的资源。
  • 当你需要将动态分配的对象作为函数返回值时,std::unique_ptr是一个很好的选择。

3.3 shared_ptr

共享式指针,实现了共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象以及相关资源会在其所指对象不再使用之后,自动释放与对象相关的资源;它是使用计数机制来表明资源被几个指针共享;可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr,unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0时,资源会被释放。shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。因为使用shard_ptr仍然需要new来调用,这使得代码出现了不对称,std::make_shared_ptr就可以显示消除这个new;

模拟实现思路 引用计数:通过一个计数器记录有多少个 shared_ptr 指向同一个对象。 自动释放:当最后一个 shared_ptr 被销毁时,释放对象的内存。 拷贝构造和赋值操作:通过引用计数的增加和减少来管理对象的生命周期。

template
class shared_ptr {
private:
T* ptr; // 指向管理的对象
int* count; // 引用计数器
// 私有辅助函数:释放资源
void release() {
if (ptr && --(*count) == 0) {
delete ptr; // 释放对象
delete count; // 释放计数器
}
}
public:
// 构造函数
explicit shared_ptr(T* p = nullptr) : ptr(p), count(new int(1)) {
if (!ptr) {
*count = 0; // 如果指针为空,计数器置为0
}
}
// 拷贝构造函数
shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
++(*count); // 增加引用计数
}
// 赋值运算符
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
release(); // 释放当前对象
ptr = other.ptr;
count = other.count;
++(*count); // 增加引用计数
}
return *this;
}
// 析构函数
~shared_ptr() {
release(); // 释放资源
}
// 重载解引用操作符
T& operator*() const {
if (!ptr) throw std::runtime_error("Dereferencing null pointer");
return *ptr;
}
// 重载箭头操作符
T* operator->() const {
if (!ptr) throw std::runtime_error("Accessing null pointer");
return ptr;
}

特点:

  • 共享所有权:多个std::shared_ptr可以共享同一个对象的所有权。
  • 自动释放:当最后一个std::shared_ptr被销毁时,它会自动调用delete释放其管理的对象。
  • 线程安全:std::shared_ptr的引用计数是线程安全的,适合在多线程环境中使用。

使用场景:

  • 当你需要在多个对象之间共享一个资源时,例如在多线程环境中共享一个动态分配的对象。
  • 当你需要将动态分配的对象作为函数参数传递时,std::shared_ptr可以方便地共享所有权。

3.4 weak_ptr

弱指针,用来解决shared_ptr相互引用导致的死锁问题;shared_ptr相互引用的两个引用计数永远不会下降为0,从而导致死锁问题。而weak_ptr是对对象的一种弱引用,可以绑定到shared_ptr,但不会增加对象的引用计数。

模拟实现 weak_ptr依赖于shared_ptr,与 shared_ptr 共享同一个引用计数器 除了 shared_ptr 的强引用计数,还需要一个弱引用计数,记录有多少个 weak_ptr 指向同一个对象。 提供 lock() 方法,返回一个 shared_ptr,用于安全地访问对象。 提供 expired() 方法,检查对象是否已经被销毁。

template
class weak_ptr {
private:
T* ptr;           // 指向被管理的对象
RefCounter* counter;  // 指向引用计数器
public:
// 默认构造函数,初始化为空的 weak_ptr
weak_ptr() : ptr(nullptr), counter(nullptr) {}
// 从 shared_ptr 构造 weak_ptr
weak_ptr(const shared_ptr& sp) : ptr(sp.ptr), counter(sp.counter) {
++(*counter->weak_count);  // 增加弱引用计数
}
// 拷贝构造函数
weak_ptr(const weak_ptr& wp) : ptr(wp.ptr), counter(wp.counter) {
++(*counter->weak_count);  // 增加弱引用计数
}
// 赋值运算符
weak_ptr& operator=(const weak_ptr& wp) {
if (this != &wp) {  // 避免自赋值
if (--(*counter->weak_count) == 0) {  // 减少当前对象的弱引用计数
delete counter;  // 如果弱引用计数为 0,删除引用计数器
}
ptr = wp.ptr;  // 复制指针
counter = wp.counter;  // 复制引用计数器
++(*counter->weak_count);  // 增加新对象的弱引用计数
}
return *this;
}
// 析构函数
~weak_ptr() {
if (--(*counter->weak_count) == 0) {  // 减少弱引用计数
delete counter;  // 如果弱引用计数为 0,删除引用计数器
}
}
// 尝试将 weak_ptr 转换为 shared_ptr
shared_ptr lock() const {
if (*counter->use_count > 0) {  // 如果对象仍然存在(强引用计数大于 0)
return shared_ptr(ptr);  // 返回一个 shared_ptr
} else {
return shared_ptr();  // 否则返回空的 shared_ptr
}
}
// 检查对象是否已经失效(即没有强引用指向它)
bool expired() const {
return *counter->use_count == 0;  // 如果强引用计数为 0,返回 true
}
};

特点:

  • 弱引用:std::weak_ptr不会增加对象的引用计数,因此不会阻止对象的销毁。
  • 循环引用:两个或多个对象相互引用,导致它们无法被垃圾回收或自动销毁的情况。
  • 解决循环引用:std::weak_ptr可以用于打破std::shared_ptr之间的循环引用,避免内存泄漏。
  • 线程安全:std::weak_ptr的操作也是线程安全的。

使用场景:

  • 当你需要在std::shared_ptr之间建立关系,但又不想增加引用计数时,例如在双向链表或观察者模式中。
  • 当你需要访问一个可能已经被销毁的对象时,std::weak_ptr可以安全地检查对象是否仍然存在。

C++关键字:
nullptr表示空指针,nullptr是新增的关键字
nullptr不会被解释成0,但是NULL会被解释成0
#define NULL 0x00000000

4 疑问

4.1 unique_ptr能否被另一个unique_ptr拷贝呢?

不能,因为它把它的拷贝构造函数private了。但是它提供了一个移动构造函数,所以可以通过std::move将指针指向的对象交给另一个unique_ptr,转交之后自己就失去了这个指针对象的所有权,除非被显示交回

4.2 unique_ptr和shared_ptr的区别

(1)
unique_ptr代表的是专属所有权,不支持复制和赋值。但是可以移动
shared_ptr 代表的是共享所有权,shared_ptr 是支持复制的和赋值以及移动的
(2)资源消耗上
unique_ptr 在默认情况下和裸指针的大小是一样的。
所以 内存上没有任何的额外消耗,性能是最优的,我们大多数场景下用到的应该都是 unique_ptr。
shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。因此相比于 unique_ptr, shared_ptr 的内存占用更高。在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。

4.3 shared_ptr的移动赋值时发生了什么事情

首先它会检查本指针和参数指针是不是同一个对象,如果是,直接返回
然后,先把本指针的引用变量–1,如果发现减到了0,就把参数指针和参数引用变量析构掉并置NULL
最后,本指针和参数指针指向同一个对象以及引用计数,然后引用计数自增1

4.4 shared_ptr是不是线程安全

不是
引用计数的增减是原子操作没问题,但是shared_pytr的读写本身不只包括引用计数操作,还包括资源所有权的操作,这两个操作合起来不是原子的
如果要求线程安全必须加锁

4.5使用std::make_shared和std::make_unique

C++11提供了std::make_shared和C++14提供了std::make_unique,它们可以更安全地创建std::shared_ptr和std::unique_ptr,避免了直接使用new可能带来的异常安全问题。

【C++】智能指针介绍
C/C++面试:什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?
完整教程:C++11智能指针详解

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐