C++ 弱指针与 `enable_shared_from_this`:解决循环引用与生命周期

好的,各位观众老爷,今天咱来聊聊 C++ 里的弱指针和 enable_shared_from_this。这俩家伙,听起来好像是武林秘籍,实际上是解决 C++ 智能指针里一个很常见,也很让人头疼的问题——循环引用。

啥是循环引用?

简单来说,就是两个或多个对象互相持有对方的 shared_ptr。这就形成了一个闭环,谁也释放不了谁,最终导致内存泄漏。想象一下,你和你的朋友,你俩都拽着对方的胳膊,谁也不撒手,结果就是谁也走不了,只能原地尬住。

举个栗子:

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
  std::shared_ptr<B> b_ptr;
  ~A() { std::cout << "A destructor called" << std::endl; }
};

class B {
public:
  std::shared_ptr<A> a_ptr;
  ~B() { std::cout << "B destructor called" << std::endl; }
};

int main() {
  std::shared_ptr<A> a = std::make_shared<A>();
  std::shared_ptr<B> b = std::make_shared<B>();

  a->b_ptr = b;
  b->a_ptr = a;

  std::cout << "程序结束" << std::endl;
  return 0;
}

运行这段代码,你会发现 A 和 B 的析构函数都没有被调用。这就是循环引用在作妖! a 持有 bb 持有 a,引用计数永远大于 0,内存永远无法释放。

弱指针:解开循环引用的钥匙

弱指针(std::weak_ptr)就是来解开这个死结的。它就像一根细线,指向一个对象,但不增加对象的引用计数。你可以用它来观察对象是否还活着,如果对象已经被销毁,弱指针就会变成空的。

想想看,如果 A 持有 B 的 shared_ptr,而 B 持有 A 的 weak_ptr,那 A 销毁的时候,B 的 weak_ptr 会自动失效,循环引用就打破了!

我们来改一下上面的例子:

#include <iostream>
#include <memory>

class B;

class A {
public:
  std::shared_ptr<B> b_ptr;
  ~A() { std::cout << "A destructor called" << std::endl; }
};

class B {
public:
  std::weak_ptr<A> a_ptr; // 使用 weak_ptr
  ~B() { std::cout << "B destructor called" << std::endl; }
};

int main() {
  std::shared_ptr<A> a = std::make_shared<A>();
  std::shared_ptr<B> b = std::make_shared<B>();

  a->b_ptr = b;
  b->a_ptr = a;

  std::cout << "程序结束" << std::endl;
  return 0;
}

这次运行,你会发现 A 和 B 的析构函数都被调用了! 循环引用被成功打破。

弱指针的使用方法

弱指针不能直接访问它指向的对象,需要先把它转换成 shared_ptr 才能用。

#include <iostream>
#include <memory>

int main() {
  std::shared_ptr<int> sp = std::make_shared<int>(42);
  std::weak_ptr<int> wp = sp;

  // 尝试访问弱指针指向的对象
  if (auto shared_ptr = wp.lock()) { // lock() 返回 shared_ptr
    std::cout << "Value: " << *shared_ptr << std::endl;
  } else {
    std::cout << "Object is no longer alive." << std::endl;
  }

  sp.reset(); // 销毁 shared_ptr

  // 再次尝试访问弱指针指向的对象
  if (auto shared_ptr = wp.lock()) {
    std::cout << "Value: " << *shared_ptr << std::endl;
  } else {
    std::cout << "Object is no longer alive." << std::endl;
  }

  return 0;
}

wp.lock() 方法会尝试把 weak_ptr 转换成 shared_ptr。如果对象还活着,它会返回一个有效的 shared_ptr;如果对象已经被销毁,它会返回一个空的 shared_ptr。所以,在使用之前,一定要检查 lock() 的返回值。

enable_shared_from_this:优雅地获取自己的 shared_ptr

有时候,我们希望在一个对象内部获取指向自己的 shared_ptr。 这在事件处理、回调函数等场景中非常有用。但是,直接 std::shared_ptr<MyClass> self(this); 是不安全的,因为这会创建新的 shared_ptr,导致多个 shared_ptr 指向同一个对象,引用计数混乱,最终导致重复释放。

这时候,enable_shared_from_this 就派上用场了。它是一个基类,可以让你安全地获取指向自己的 shared_ptr

#include <iostream>
#include <memory>

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
  std::shared_ptr<MyClass> get_self() {
    return shared_from_this();
  }

  void do_something() {
    std::cout << "Doing something... Address: " << this << std::endl;
  }

  ~MyClass() {
    std::cout << "MyClass destructor called. Address: " << this << std::endl;
  }
};

int main() {
  std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
  std::shared_ptr<MyClass> self_ptr = obj->get_self();

  obj->do_something();
  self_ptr->do_something();

  std::cout << "程序结束" << std::endl;
  return 0;
}

注意几点:

  1. 继承 enable_shared_from_this 让你的类继承这个基类。
  2. 使用 shared_from_this() 在类的成员函数中使用 shared_from_this() 方法来获取指向自己的 shared_ptr。这个方法只能在对象已经被 shared_ptr 管理之后才能调用,否则会抛出 std::bad_weak_ptr 异常。
  3. 确保对象被 shared_ptr 管理: enable_shared_from_this 的工作原理依赖于对象已经由 shared_ptr 管理。因此,不要直接在栈上创建对象并调用 shared_from_this()

enable_shared_from_this 的原理

enable_shared_from_this 内部维护了一个 weak_ptr,指向它所关联的对象。当使用 std::make_shared 或其他方式创建一个 shared_ptr 管理的对象时,enable_shared_from_this 会自动将这个 weak_ptr 指向该对象。

shared_from_this() 方法实际上就是调用 weak_ptrlock() 方法,将其转换成 shared_ptr。 如果对象已经被销毁,lock() 会返回一个空的 shared_ptr,从而避免了悬空指针。

使用场景总结

使用场景 解决方案
解决循环引用 使用 weak_ptr 来打破循环,允许对象被销毁。
在对象内部获取指向自己的 shared_ptr 继承 enable_shared_from_this,使用 shared_from_this() 方法。
事件处理/回调函数 使用 enable_shared_from_this 获取 shared_ptr,避免对象在回调期间被销毁。
缓存 使用 weak_ptr 实现缓存,当对象不再被使用时,可以自动从缓存中移除。

高级用法与注意事项

  • 多重继承: 如果你的类已经继承了其他基类,并且还需要使用 enable_shared_from_this,可以使用虚继承来避免二义性问题:

    class Base {};
    class MyClass : public Base, public std::enable_shared_from_this<MyClass> {};
  • 线程安全: shared_from_this() 本身不是线程安全的。如果在多线程环境下使用,需要进行额外的同步处理。

  • 避免裸指针: 尽量避免在代码中使用裸指针,尤其是在涉及到智能指针管理的对象时。使用裸指针容易导致内存泄漏和悬空指针。

  • 优先使用 make_shared std::make_shared 可以一次性分配对象和控制块,减少内存分配次数,提高性能。

  • shared_ptr的别名构造: shared_ptr支持从裸指针构造,并指定析构函数,这使得它可以管理一些非new操作符分配的资源。

    #include <iostream>
    #include <memory>
    
    void delete_file(FILE* fp) {
        if (fp) {
            fclose(fp);
            std::cout << "File closed." << std::endl;
        }
    }
    
    int main() {
        FILE* fp = fopen("test.txt", "w+");
        if (fp == nullptr) {
            perror("Error opening file");
            return 1;
        }
    
        std::shared_ptr<FILE> file_ptr(fp, delete_file); // 使用 shared_ptr 管理 FILE*
        // ... 使用 file_ptr 操作文件 ...
        fprintf(file_ptr.get(), "Hello, world!"); // 使用 file_ptr.get() 获取裸指针
    
        // 当 file_ptr 离开作用域时,delete_file 会被调用,文件会被关闭
        std::cout << "shared_ptr exiting scope." << std::endl;
        return 0;
    }
  • 自定义删除器: shared_ptr的另一个强大的特性是可以自定义删除器。删除器是一个可调用对象,在shared_ptr的最后一个引用消失时会被调用,用于释放资源。这对于管理非new/delete分配的资源非常有用,例如文件句柄、互斥锁、数据库连接等。

    #include <iostream>
    #include <memory>
    #include <mutex>
    
    class MyResource {
    public:
        MyResource(int id) : id_(id) {
            std::cout << "Resource " << id_ << " acquired." << std::endl;
        }
    
        ~MyResource() {
            std::cout << "Resource " << id_ << " released." << std::endl;
        }
    
        int id() const { return id_; }
    
    private:
        int id_;
    };
    
    void delete_my_resource(MyResource* resource) {
        std::cout << "Custom deleter called for resource " << resource->id() << std::endl;
        delete resource;
    }
    
    int main() {
        // 使用自定义删除器创建 shared_ptr
        std::shared_ptr<MyResource> resource_ptr(new MyResource(123), delete_my_resource);
    
        std::cout << "Using the resource..." << std::endl;
        std::cout << "Resource ID: " << resource_ptr->id() << std::endl;
    
        // 当 resource_ptr 离开作用域时,delete_my_resource 会被调用,资源会被释放
        std::cout << "shared_ptr exiting scope." << std::endl;
        return 0;
    }

总结

弱指针和 enable_shared_from_this 是 C++ 智能指针体系中重要的组成部分。 它们可以帮助我们避免循环引用,安全地获取对象的 shared_ptr, 从而编写出更加健壮和可靠的代码。 掌握这些技巧,你就可以在 C++ 的江湖里自由驰骋,不再为内存泄漏而烦恼!

好了,今天的讲座就到这里。 感谢各位的观看! 如果觉得有用,别忘了点赞、收藏、转发,一键三连哦! 我们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注