1.基础介绍

在 C++ 中,使用new分配内存后,如果忘记使用delete释放,内存泄漏就会悄然而至。比如在一个复杂的函数中:

void complexFunction() {
    int* ptr = new int(10);
    // 这里有大量的代码逻辑
    // 很可能因为疏忽而忘记释放ptr
}

随着程序的不断运行,这样的内存泄漏逐渐积累,最终会耗尽系统资源,导致程序崩溃。
而野指针的问题同样棘手。当指针指向的内存被释放后,指针却没有被置为nullptr,就形成了野指针。例如:

int* ptr = new int(10);
delete ptr;
// 此时ptr成为了野指针
// 如果后续不小心再次使用ptr,就会导致未定义行为
int value = *ptr; 

还有一种情况是重复释放内存。当多个指针指向同一块内存时,如果不小心对同一块内存进行多次释放,程序也会崩溃。
为了解决这些问题,C++ 引入了智能指针。智能指针利用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制,将内存的管理和对象的生命周期绑定在一起。当智能指针对象创建时,它获取资源(即分配内存);当智能指针对象销毁时,它自动释放所指向的内存,无需手动调用delete。这就像是给内存分配和释放过程加上了一个可靠的 “管家”,极大地降低了内存管理出错的风险 。

2.三种智能指针

2.1 独占指针 std::unique_ptr

独占所有权的智能指针,同一时刻只能有一个unique_ptr指向某个对象。

#include <memory>

// 创建
std::unique_ptr<int> ptr1(new int(10));
auto ptr2 = std::make_unique<int>(20);  // C++14推荐方式

// 所有权转移(移动语义)
std::unique_ptr<int> ptr3 = std::move(ptr2);  // ptr2变为nullptr

// 访问
*ptr1 = 30;
int value = *ptr1;

// 自定义删除器
auto deleter = [](int* p) { 
    std::cout << "删除" << *p << std::endl; 
    delete p; 
};
std::unique_ptr<int, decltype(deleter)> ptr4(new int(40), deleter);

特点:

  • 不能复制,只能移动(std::move)
  • 零开销(与原始指针大小相同)
  • 适用于明确的所有权场景

2.2 共享指针std::shared_ptr

共享所有权的智能指针,使用引用计数管理对象生命周期。

// 创建
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);  // 推荐
std::shared_ptr<int> ptr2(new int(20));

// 共享所有权
std::shared_ptr<int> ptr3 = ptr1;  // 引用计数+1
std::cout << ptr1.use_count();  // 输出: 2

// 重置
ptr3.reset();  // 引用计数-1
ptr1 = nullptr;  // 引用计数-1,对象被销毁

// 自定义删除器
auto deleter = [](int* p) { delete p; };
std::shared_ptr<int> ptr4(new int(30), deleter);

特点:

  • 可以复制和赋值
  • 有额外开销(控制块+引用计数)
  • 线程安全的引用计数
  • 可能产生循环引用问题

2.3 弱引用指针std::weak_ptr

弱引用智能指针,不增加引用计数,用于解决shared_ptr的循环引用问题。

std::shared_ptr<int> sptr = std::make_shared<int>(10);
std::weak_ptr<int> wptr = sptr;  // 不增加引用计数

// 使用前需要检查对象是否还存在
if (auto locked = wptr.lock()) {  // 转换为shared_ptr
    std::cout << *locked << std::endl;
} else {
    std::cout << "对象已销毁" << std::endl;
}

// 检查是否过期
if (wptr.expired()) {
    std::cout << "对象不存在" << std::endl;
}

循环引用问题描述

#include <iostream>
#include <memory>

class B;
class A {
public:
    std::shared_ptr<B> pb_;
    ~A() {
        std::cout << "A delete" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> pa_;
    ~B() {
        std::cout << "B delete" << std::endl;
    }
};

void fun() {
    std::shared_ptr<A> pa(new A()); // A 计数为 1
    std::shared_ptr<B> pb(new B()); // B 计数为 1
    pa->pb_ = pb; // B 计数为 2
    pb->pa_ = pa; // A 计数为 2
    std::cout << "pa use_count: " << pa.use_count() << std::endl; 
    std::cout << "pb use_count: " << pb.use_count() << std::endl; 
}

int main() {
    fun();
    return 0;
}

// 开始析构 pa、pb
// 注意,此时调用的析构函数是std::shared_ptr<A>和std::shared_ptr<B>
// 侧重在std::shared_ptr,而std::shared_ptr仅仅减少计数

在上述代码中,AB相互引用,形成了循环引用,导致papb的引用计数永远不会为 0,对象无法被释放 。如果将A中的std::shared_ptr<B> pb_;改为std::weak_ptr<B> pb_;,就可以打破循环引用,使对象能够正常释放 。使用weak_ptr时,需要通过lock函数将其提升为shared_ptr,才能访问对象 。如果对象已经被释放,lock函数会返回一个空的shared_ptr 。例如:

#include <iostream>
#include <memory>

class B;
class A {
public:
    std::shared_ptr<B> pb_;
    ~A() {
        std::cout << "A delete" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> pa_;
    ~B() {
        std::cout << "B delete" << std::endl;
    }
};

void fun() {
    std::shared_ptr<A> pa(new A()); // A Shared 计数为 1
    std::shared_ptr<B> pb(new B()); // B Shared 计数为 1
    pa->pb_ = pb; // B Shared 计数为 2
    pb->pa_ = pa; // A Shared 计数为 1, A Weak 计数为 1
    std::cout << "pa use_count: " << pa.use_count() << std::endl; 
    std::cout << "pb use_count: " << pb.use_count() << std::endl; 
}

int main() {
    fun();
    return 0;
}
// pb 析构,B Shared 计数为 2 -> 1
// pa 析构,A Shared 计数为 1 -> 0,开始销毁内部 A 示例
// 析构 A -> 内部析构 pb_ , B Shared 计数为 1 -> 0,开始销毁 pa_
// 最后 A 、B都被回收了。

还有代码如下

#include <iostream>
#include <memory>
int main() {
    std::shared_ptr<int> s1(new int(10)); // Shared 计数为 1
    std::weak_ptr<int> w1 = s1;           // Shared 计数为 1, Weak 计数为 1
    if (std::shared_ptr<int> s2 = w1.lock()) { // lock 提升,Shared计数为2
        std::cout << *s2 << std::endl; 
    } else {
        std::cout << "object has been deleted" << std::endl;
    } // s2 释放,Shared计数为 1
    s1.reset(); // Shared计数为 0
    if (std::shared_ptr<int> s3 = w1.lock()) { // Shared计数为 0,内存已经被释放,得到nullptr
        std::cout << *s3 << std::endl; 
    } else {
        std::cout << "object has been deleted" << std::endl;
    }
    return 0;
}

使用建议:

  1. 优先使用unique_ptr:默认选择,除非需要共享所有权
  2. make_shared/make_unique创建:更安全、更高效
  3. 避免循环引用:类成员使用weak_ptr打破循环
  4. 不要混用:不要将原始指针和智能指针混用同一对象
  5. 传递参数
    • 传值:转移所有权时使用unique_ptr
    • 传引用:不转移所有权时传const unique_ptr&shared_ptr&
    • 传原始指针:仅观察时使用.get()

3.std::enable_shared_from_this

参考:详解std::enable_shared_from_this_std enable shared from this-CSDN博客

3.1 基础认识

std::enable_shared_from_this 是 C++11 引入的一个模板基类,定义在 <memory> 头文件中。它的主要作用是:允许一个被 std::shared_ptr 管理的对象,安全地生成指向自身的其他 std::shared_ptr实例。
听起来有点绕,我们来分解一下:

  • 前提:一个对象 A 的生命周期已经由一个或多个 std::shared_ptr 管理着。
  • 问题:在对象 A 的成员函数内部,如何获得一个指向 A 自身的 std::shared_ptr?
  • 直接做法的问题:如果直接使用 std::shared_ptr<A>(this),会创建一个新的、独立的 shared_ptr 实例。这个新实例会认为它是第一个管理 this 指向的对象的指针,因此会有自己的引用计数。当这个新 shared_ptr 被销毁时,它会尝试删除 this,即使其他 shared_ptr 还在使用该对象,这会导致双重释放(double free)或悬垂指针(dangling pointer)的严重问题。例如
#include <memory>
#include <iostream>

class BadExample {
public:
    std::shared_ptr<BadExample> get_self() {
        // 错误!创建了一个新的 shared_ptr,其引用计数独立于外部的 shared_ptr
        return std::shared_ptr<BadExample>(this);
    }
    ~BadExample() {
        std::cout << "BadExample destructor called." << std::endl;
    }
};

int main() {
    std::shared_ptr<BadExample> ptr1 = std::make_shared<BadExample>();
    std::shared_ptr<BadExample> ptr2 = ptr1->get_self(); // 危险!

    // 当 main 函数结束时,ptr1 和 ptr2 都会被销毁。
    // ptr2 的销毁会调用一次析构函数。
    // ptr1 的销毁会再次调用析构函数,导致程序崩溃或未定义行为。
    return 0;
}

运行上述代码很可能会导致程序崩溃或打印两次析构信息(这取决于编译器和运行时库的实现,但无论如何都是错误的)。
std::enable_shared_from_this 的解决方案:通过让你的类继承自 std::enable_shared_from_this<T>(其中 T 是你的类名),你就可以在类的成员函数中调用 shared_from_this()方法。这个方法会返回一个 std::shared_ptr<T>,它共享了原本管理该对象的那个(或那些)shared_ptr 的引用计数。

3.2 实现原理

std::enable_shared_from_this<T> 的内部机制大致如下:

  • 它内部维护了一个 std::weak_ptr<T> 成员变量(我们可以称之为 weak_this)。
    当一个 std::shared_ptr<T> 被创建来管理一个 T 类型的对象时(且 T 继承自 enable_shared_from_this<T>),shared_ptr 的构造函数会检查这个对象是否是 enable_shared_from_this的派生类。
  • 如果是 shared_ptr 会将 weak_this 成员变量初始化,使其指向该对象,并共享同一个控制块(control block,用于管理引用计数和对象销毁)。
  • 当你在成员函数中调用 shared_from_this() 时,它会调用 weak_this.lock()。lock() 会尝试从 weak_ptr 提升(promote)为一个 shared_ptr。
  • 如果对象仍然存在(即 weak_ptr 没有过期),lock() 会成功并返回一个有效的 shared_ptr,其引用计数会被正确地增加。
  • 如果对象已经被销毁(weak_ptr 已过期),lock() 会返回一个空的 shared_ptr。
    关键点:shared_from_this() 返回的 shared_ptr 与最初创建对象的 shared_ptr 以及其他通过 shared_from_this() 创建的 shared_ptr 都共享同一个引用计数

3.3 使用条件和限制

为了正确使用 std::enable_shared_from_this,你必须遵守以下几个重要条件:

  • 必须公有继承:你的类必须以公有方式继承 std::enable_shared_from_this<T>。这是为了让shared_ptr 的构造函数能够访问到基类中的 weak_this 成员。
  • 对象必须被 std::shared_ptr 管理:在调用 shared_from_this() 之前,必须已经有一个 std::shared_ptr 实例持有了该对象。
    • 绝对不要在构造函数中调用 shared_from_this()!因为在构造函数执行期间,shared_ptr 还没有完全创建好,weak_this 也还没有被初始化。此时调用会导致未定义行为(通常是返回一个空的 shared_ptr)。
    • 绝对不要对一个栈上分配的对象调用 shared_from_this()!例如:
      MyClass obj; // 在栈上
      obj.shared_from_this(); // 错误!obj 的生命周期不由 shared_ptr 管理。

      这会导致 weak_this 未被初始化,调用 lock() 会失败。

  • 避免循环引用:enable_shared_from_this 本身不会直接导致循环引用,但它使得创建循环引用变得更容易。如果你有两个对象 A 和 B,它们都通过 shared_ptr 互相指向对方,那么它们的引用计数永远不会降到零,导致内存泄漏。解决这个问题的方法是在循环的一端使用 std::weak_ptr

添加新评论