ZBLOG

如何选择 unique_ptr 和 shared_ptr? 如何解决 shared_ptr 的循环引用? 智能指针在多线程中的安全性?

好的,这是关于智能指针几个核心高级问题的详细解答。


1. 如何选择 unique_ptrshared_ptr

这是一个关于所有权模型的问题。所有权的概念是智能指针的核心。

特性 std::unique_ptr std::shared_ptr
所有权模型 独占所有权 共享所有权
拷贝语义 ❌ 不可拷贝,只可移动 ✅ 可以拷贝
性能开销 几乎为零(与裸指针无异) 有开销(控制块、引用计数原子操作)
适用场景 明确知道只有一个所有者;父对象拥有子对象;工厂模式返回指针 需要多个独立对象共同管理同一个资源的生命周期;缓存中需要共享数据

选择指南:

  • 默认使用 unique_ptr 这是 C++11/14/17 以来的最佳实践。它清晰地表达了“我是这个资源的唯一主人”的语义,没有额外开销,并且能防止意外的资源分享。绝大多数场景下,一个资源在任一时刻都只有一个明确的拥有者。

    // 场景1: 在函数内部动态创建对象,且不想手动管理内存
    void process() {
        auto data = std::make_unique<MyData>();
        data->doSomething();
        // data 离开作用域,自动删除 MyData
    }
    
    
    // 场景2: 作为类的成员变量,表示“组合”关系(我拥有它,我的生命周期就是它的生命周期)
    class Game {
    private:
        std::unique_ptr<Renderer> m_renderer; // Game *拥有* Renderer
    public:
        Game() : m_renderer(std::make_unique<Renderer>()) {}
    };
    
    
    // 场景3: 工厂函数返回资源
    std::unique_ptr<Connection> createConnection() {
        return std::make_unique<Connection>();
    }
    
  • 当且仅当需要共享所有权时,才使用 shared_ptr 当你无法确定哪个对象会最后使用这个资源时,就需要共享所有权。所有持有该 shared_ptr 的对象共同决定资源的生命周期。

    // 场景1: 多个对象需要共享同一份数据
    class Node {
    public:
        std::vector<std::shared_ptr<Node>> children; // 父子节点共享 ownership?
        // ... (但注意这可能导致循环引用!)
    };
    
    
    // 场景2: 缓存系统
    std::map<std::string, std::shared_ptr<Config>> cache;
    auto config = cache["app"]; // 多个部分都可能持有这个缓存的配置
    
    
    // 场景3: 异步/多线程任务中,任务和主线程都不知道对方会先结束
    void asyncTask(std::shared_ptr<NetworkSession> session) { 
        // asyncTask 和创建它的函数都可能比对方存活得更久
        // shared_ptr保证了只要有一方在用,session就有效。
    }
    

2. How to Solve the Circular Reference Problem of shared_ptr?

循环引用是 shared_ptr 最经典的问题

What is a Circular Reference?

#include <memory>
class B; // Forward declaration

class A {
public:
	std::shared_ptr<B> b_ptr;
	~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
	std::shared_ptr<A> a_ptr;
	~B() { std::cout << "B destroyed\n"; }
};

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

	a->b\_ptr = b; // A ref-count of B: 2
	b->a\_ptr = a; // B ref-count of A: \033[0;31m2\033[0m

	return O;
	// a ref-count -> \033[0;31m1\033[0m (because b->a\_ptr still holds)
	// b ref-count -> \033[0;31m1\033[0m (because a->b\_ptr still holds)
	// Memory Leak! No destructors are called.
}

The Solution: Use std::weak_ptr

将其中一个 shared_prt member variable替换为 weak_prt.
A weak_prt observes an object managed by shared_prt but does not increase its reference count.

#include <memory>
class B;

class A {
public:
	std: : shared_prt<B> b_prt;
	~A( ) { srd: :cout « "A destroyed\n" ; } 
} ;

class B (
public:
	srd: :weak_prt<A> a_prt; / / Changed to weak_prtl!
	-B( ) ( srd: :cout « "B destroyed\n" ; )
} ;

int main( ) (
	auto a = srd: : make_ sharedcA>( );
	auto b = srd: : make_ sharedcB>( );

	a->b\_prt - b;
	b->a\_prt - a;

	return O;
	// Ref-count of 'a' goes from Z -> l, then O -> destroyed.
	// When 'a' is destroyed, it releases its hold on 'b'.
	// Ref-count of 'b' goes from Z -> l, then O -> destroyed.
}

Why Does This Work?

  • When you use a weak pointer, it doesn’t own the object.
  • To use the object observed by weak pointer, you must convert it to shared pointer temporarily (using .lock() method), which increases reference count only for that scope.
void B:useA( ) (
	if (auto tmp = a\_prt.lock( )) { / / Creates a temporary shared\_prt
	    tmp->doSomething( );         / / Safe to use 'tmp' here
	} else (
	    / / The object 'A' has already been destroyed.
	}
}

Are Smart Pointers Thread-Safe?

This question needs to be broken down because different aspects have different safety guarantees.

Thread Safety Rules:

  1. Same Object: Multiple threads can read/destroy DIFFERENT shared_ptrs that point to the SAME object safely.
    Example: Copying shared ptr in one thread while another thread destroys its copy - this is fine.

  2. Control Block: The internal reference count inside control block is atomic. So copying/destroying shared ptrs pointing to same raw pointer from multiple threads is safe regarding reference counting operations themselves being race-free.

  3. BUT Data Itself Is Not Automatically Protected:
    Just like raw pointers, smart pointers don’t automatically synchronize access to the actual object they point at!

  4. Unique ptr Has Strict Rules:
    Unique ptr should generally NOT be shared between threads because its unique ownership model means only one thread should own/manage it at any time.

Example Showing Lack Of Data Protection:

auto globalPtr = srd: : make_ sharedcint>(O);

void thread_func( )(
	for(int i=O;i<100000;++i){
	    *globalPtr += l;           / / DATA RACE!
	    globalPtr.reset(new int); / / MANAGEMENT RACE! (But control block ops are safe)
	}
}

int main(){
	srd: :thread tl(thread_func);
	srd: :thread tZ(thread_func);
	tl.join();
	tZ.join();
}

Even though we’re using share pr here and control block operations might be safe individually when resetting or copying etc., there’s absolutely no protection against concurrent modifications of our integer data through dereferencing operator (*).

You still need mutexes or other synchronization primitives around your actual data accesses even when using smart pointers!


内容由零声教学AI助手提供,问题来源于学员提问

本站部分文章来源于网络,版权归原作者所有,如有侵权请联系站长删除。
转载请注明出处:https://golang.0voice.com/?id=24401

分享:
扫描分享到社交APP
上一篇
下一篇
发表列表
游客 游客
此处应有掌声~
评论列表

还没有评论,快来说点什么吧~

联系我们

在线咨询: 点击这里给我发消息

微信号:3007537140

上班时间: 10:30-22:30

关注我们
x

注册

已经有帐号?