(最常见的C++11新特性)智能指针(unique_ptr、shared_ptr、weak_ptr)——线程安全?强弱引用计数?move函数?弱指针辅助?共享比独占开销大?

作者:求一个demo

版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

内容通俗易懂,没有废话,文章最后是面试常问内容(建议通过标题目录学习)

废话不多说,我们直接开始------>>>>>>

#include <memory>//智能指针头文件

一、独占指针(unique_ptrr)

        1、特点:

        ①同一时间只能有一个unique_ptr指向某个对象(不能进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值)。

        ②不能使用其他unique_ptr对象的值初始化另一个unique_ptr,也不能将一个unique_ptr赋值给另一个unique_ptr。因为这样会导致两个独占指针共享相同的对象。

        ③可以使用move()方法,将对象的所有权从一个独占指针转移到另一个独占指针。

        ④不能动态分配(new)一个智能指针,而是应该像声明函数的局部变量那样声明智能指针(智能指针是自动管理内存,如果new手动管理内存容易内存泄漏)。当unique_ptr离开作用域时,它管理的对象也将被删除(unique_ptr有独占所有权的特性,unique_ptr离开作用域,析构函数被调用,自动释放内存,防止内存泄漏)。如果要删除unique_ptr管理的对象,又要保留智能指针在作用域中,则可以将其值设置为nullptr,或者调用reset()函数(置nullptr则释放unique_ptr管理对象的所有权并释放对象内存,之后unique_ptr不指向任何对象,仍在作用域中。reset()函数作用和置nullptr类似)。

        ⑤智能指针指向对象的内存一般是堆内存,因为智能指针是帮助我们管理动态分配的内存,避免手动管理出现的内存泄漏等问题。

        2、底层及是否线程安全:

        (1)底层:unique_ptr本质是裸指针+删除器(deleter),析构时调用delete或自定义删除器。

        (2)是否线程安全:线程不安全(可能导致数据竞争和未定义行为。unique_ptr的独占性,unique_ptr不能进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值。在多线程环境中,多个线程访问一个unique_ptr对象,一个线程正使用unique_ptr访问其管理的对象,另一个线程试图移动该unique_ptr,导致结果不确定性)。

        3、使用场景:

        ①替代new/delete,管理动态分配的单个对象或数组。

        ②明确资源独占的场景(如工厂模式返回的资源)。

        4、示例:

#include <memory>
using namespace std;
// 分配一个 int
unique_ptr<int> p1 = make_unique<int>(42);
// 分配数组(C++14 支持 make_unique<T[]>)
unique_ptr<int[]> arr = make_unique<int[]>(10);
arr[0] = 100;
// 转移所有权
unique_ptr<int> p2 = move(p1); // p1 变为 nullptr

二、共享指针(shared_ptr)

        1、特点:

        ①多个shared_ptr可以指向同一个对象,通过(强)引用计数管理生命周期,自动管理内存。

        ②开销较大,因为需要维护引用计数和控制块。

        ③shared_ptr是强引用计数,每当有一个新的shared_ptr指向相同对象或拷贝构造、拷贝赋值运算符从一个已有的shared_ptr创建一个新的shared_ptr,强引用计数加1。每当一个shared_ptr被销毁(例如离开其作用域),或通过赋值操作让它指向其他对象时,强引用计数减1。强引用计数减为0时,shared_ptr会自动释放对象所占的内存,并调用对象的析构函数

        2、底层及是否线程安全:

        (1)底层:包含两个指针,一个指向对象,一个指向控制块(含强弱引用计数、删除器等)。make_shared将对象和控制块分配在连续内存中,提高缓存命中率。

        (2)是否线程安全:部分线程安全。

                ①安全的部分:引用计数的增减是原子的,因此多线程同时拷贝或销毁同一个shared_ptr是安全的(引用计数的变化不会引起竞争)。

                ②不安全的部分:shared_ptr指向的对象本身的访问需要用户自行同步。shared_ptr只保证引用计数的安全,不保证对象的线程安全。如果多个线程通过不同shared_ptr访问同一个对象,且至少有一个线程执行写操作,需要同步机制(如互斥锁)。

        3、使用场景:

        ①需要多个指针共享同一对象的场景(如多线程缓存、图结构等)。

        4、环引用

        (1)原理:当两个或多个shared_ptr相互引用时,每个对象的引用计数都不会减为0,即使这些对象都没有外部引用,他们的内存也无法被释放,造成内存泄漏。

        (2)产生原因:

        两个shared_ptr对象a和b分别指向A和B类型的对象,从此时A和B强引用计数都为1。通过让A对象中b_ptr指向B对象,B对象中a_ptr指向A对象,A和B强引用计数都变为2。当main函数结束(a、b超出作用域被销毁),它们对A和B的强引用计数变为1。但A和B对象相互引用,它们的计数仍为1,未降为0。因此A和B对象的内存不会被释放,析构函数也不会被调用。

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

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() {
        std::cout << "B destructor" << 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;
    return 0;
}

        (3)解决循环引用:weak_ptr是弱引用计数,不会增加对象的引用计数,不会影响对象的生命周期。将类A与B中shared_ptr改为weak_ptr,当A对象中b_ptr指向B对象,B对象中a_ptr指向A对象,A和B对象的强引用计数不会增加。当main函数结束后,a和b超出作用域被销毁,A和B对象的引用计数降为0,内存被释放,析构函数被调用。

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

class B {
public:
    std::weak_ptr<A> a_ptr;
    ~B() {
        std::cout << "B destructor" << 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;

    return 0;
}

        5、示例:

#include <memory>
using namespace std;
auto p1 = make_shared<int>(42); // 引用计数=1
{
    auto p2 = p1; // 引用计数=2
} // p2 析构,引用计数=1
// p1 析构时引用计数=0,自动释放内存

三、弱指针(weak_ptr)

        1、特点:

        ①weak_ptr改变弱引用计数,观察shared_ptr管理的对象,但不影响其生命周期(类似shared_ptr的辅助)。

        ②当需要访问weak_ptr所管理的对象时,需要将其转换为shared_ptr,通常调用lock()函数来实现(weak_ptr是弱引用,不拥有对象的所有权,所以weak_ptr不能直接解引用 * 访问对象,因为对象可能已经被销毁了)。

        ③weak_ptr一般不单独使用,通常和shared_ptr一起使用,可以解决共享指针的循环引用问题。

        ④当weak_ptr和shared_ptr指向对象相同时,弱引用计数加1,当一个weak_ptr被销毁(离开作用域),若引用计数减1。但是,弱引用计数不影响对象内存空间的释放。

        2、底层及是否线程安全:

        (1)底层:依赖shared_ptr的控制块,但不增加引用计数。

        (2)是否线程安全:部分线程安全。

                ①安全的部分:weak_ptr是原子的,多线程同时操作一个weak_ptr的拷贝是安全的。

                ②不安全的部分:通过weak_ptr的lock()函数升级为shared_ptr时,如果与其他的shared_ptr操作竞争,可能需要同步。

        3、使用场景:

        ①解决shared_ptr的循环引用问题(如双向链表、观察者模式)。

        ②缓存场景(避免缓存对象长期未被释放)。

        4、示例:

auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

if (auto tmp = weak.lock()) { // 转换为 shared_ptr
    // 对象仍存在
} else {
    // 对象已被释放
}

四、对比(智能指针和裸指针)

        ①所有权管理:智能指针是自动(RAII)管理;裸指针需要手动管理。

        ②内存泄漏风险:智能指针风险低;裸指针风险高。

        ③线程安全:shared_ptr计数安全(对象访问方面需要同步);裸指针没有保障。

        ④开销:shared_ptr较高(要维护计数和控制块),unique_ptr较低;裸指针无额外开销。

        ⑤循环引用:需要weak_ptr解决;无此问题(但易泄漏)。

五、校招面试常问内容

1、为什么需要智能指针?

        ①解决指针内存泄漏问题(如忘记delete)。

        ②避免悬空指针(指向已释放的内存)。

        ③自动管理资源的生命周期,符合RAII原则。

2、如何转移unique_ptr的所有权?

        ①使用move()函数。

3、shared_ptr的线程安全性?

        ①引用计数是线程安全的(原子操作)。

        ②指向的对象的访问需要用户自行同步(如加锁)。

4、shared_ptr循环引用问题?如何解决?

(1)循环引用:多个共享指针互相引用,导致强引用计数永远不降为0,造成内存泄漏。

(2)解决:将其中一个共享指针改为weak_ptr。

5、weak_ptr的作用和底层原理?

(1)作用:

        ①解决shared_ptr循环引用问题。

        ②观察shared_ptr管理的对象是否存活(不增加强引用计数)。

(2)底层:依赖shared_ptr的控制块,通过若引用计数跟踪观察者的数量(看若引用计数的数量)。

6、shared_ptr的控制块何时创建?

        ①通过make_shared一次性分配内存(对象+控制块),更高效。

        ②直接构造(shared_ptr<T>(new T)):先分配对象,再分配控制块,可能抛出异常导致内存泄漏。

最后,如有不足和错误的地方,期待私信指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值