C++ 智能指针

目录

智能指针

std::unique_ptr

std::shared_ptr

深入理解shared_ptr

深入理解weak_ptr

在实际的 C/C++ 开发中,我们经常会遇到诸如 coredump、segmentfault 之类的内存问题,使用指针也会出现各种问题,比如:

野指针:未初始化或已经被释放的指针被称为野指针
空指针:指向空地址的指针被称为空指针
内存泄漏:如果在使用完动态分配的内存后忘记释放,就会造成内存泄漏,长时间运行的程序可能会消耗大量内存。
悬空指针:指向已经释放的内存的指针被称为悬空指针
内存泄漏和悬空指针的混合:在一些情况下,由于内存泄漏和悬空指针共同存在,程序可能会出现异常行为。
 

智能指针

智能指针是一种可以自动管理内存的指针,它可以在不需要手动释放内存的情况下,确保对象被正确地销毁。

这种指针可以显著降低程序中的内存泄漏和悬空指针的风险。

智能指针的核心思想就是 RAII

在C++中,智能指针常用的主要是两个类实现:

std::unique_ptr
std::shared_ptr


std::unique_ptr


std::unique_ptr是一个独占所有权的智能指针,它保证指向的内存只能由一个unique_ptr拥有,不能共享所有权。

当unique_ptr超出作用域时,它所指向的内存会自动释放。

#include <memory>
#include <iostream>

int main() {
    std::unique_ptr<int> ptr(new int(10));
    std::cout << *ptr << std::endl; // 输出10
    // unique_ptr在超出作用域时自动释放所拥有的内存
    return 0;
}

std::shared_ptr


std::shared_ptr是一个共享所有权的智能指针,它允许多个shared_ptr指向同一个对象,当最后一个shared_ptr超出作用域时,所指向的内存才会被自动释放。

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> ptr1(new int(10));
    std::shared_ptr<int> ptr2 = ptr1; // 通过拷贝构造函数创建一个新的shared_ptr,此时引用计数为2
    std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出10 10
    // ptr2超出作用域时,所指向的内存不会被释放,因为此时ptr1仍然持有对该内存的引用
    return 0;
}


总的来说,智能指针可以提高程序的安全性和可靠性,避免内存泄漏和悬空指针等问题。

深入理解shared_ptr

shared_ptr 的一个关键特性是可以共享所有权,即多个 shared_ptr 可以同时指向并拥有同一个对象。

当最后一个拥有该对象的 shared_ptr 被销毁或者释放该对象的所有权时,对象会自动被删除。

这种行为通过引用计数实现,即 shared_ptr 有一个成员变量记录有多少个 shared_ptr 共享同一个对象。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数\n"; }
    ~MyClass() { std::cout << "MyClass 析构函数\n"; }
    void do_something() { std::cout << "MyClass::do_something() 被调用\n"; }
};

int main() {
    {
        std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
        {
            std::shared_ptr<MyClass> ptr2 = ptr1; // 这里共享 MyClass 对象的所有权
            ptr1->do_something();
            ptr2->do_something();
            std::cout << "ptr1 和 ptr2 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
        } // 这里 ptr2 被销毁,但是 MyClass 对象不会被删除,因为 ptr1 仍然拥有它的所有权
        std::cout << "ptr1 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
    } // 这里 ptr1 被销毁,同时 MyClass 对象也会被删除,因为它是最后一个拥有对象所有权的 shared_ptr

    return 0;
}

输出结果:

MyClass 构造函数
MyClass::do_something() 被调用
MyClass::do_something() 被调用
ptr1 和 ptr2 作用域结束前的引用计数: 2
ptr1 作用域结束前的引用计数: 1
MyClass 析构函数

深入理解weak_ptr

std::weak_ptr是C++11引入的一种智能指针,主要与std::shared_ptr配合使用。

它的主要作用是解决循环引用问题、观察std::shared_ptr对象而不影响引用计数,以及在需要时提供对底层资源的访问。

解决循环引用问题:当两个或多个std::shared_ptr对象互相引用时,会导致循环引用。这种情况下,这些对象的引用计数永远不会变为0,从而导致内存泄漏。
std::weak_ptr可以打破这种循环引用,因为它不会增加引用计数。只需要将其中一个对象的std::shared_ptr替换为std::weak_ptr,即可解决循环引用问题。

观察std::shared_ptr对象:std::weak_ptr可以用作观察者,监视std::shared_ptr对象的生命周期。它不会增加引用计数,因此不会影响资源的释放。


深入理解weak_ptr: 资源所有权问题

std::weak_ptr从概念上,它是一个智能指针,相对于std::shared_ptr,它对于引用的对象是“弱引用”的关系。

简单来说,它并不“拥有”对象本身。

如果我们去类比生活中的场景,那么它可以是一个房地产中介。房地产中介并不拥有房子,但是我们有办法找到注册过的房产资源。

在客户想要买房子的时候,它起初并不知道房子是否已经卖出了,它需要找到房主询问后再答复客户。

std::weak_ptr做的事情几乎和房产中介是一模一样的。std::weak_ptr并不拥有对象,在另外一个std::shared_ptr想要拥有对象的时候,它并不能做决定,需要转化到一个std::shared_ptr后才能使用对象。所以std::weak_ptr只是一个“引路人”而已。

说了这么多,那么std::weak_ptr除了解决相互引用的问题,还能做什么?

答案是:一切应该不具有对象所有权,又想安全访问对象的情况。

还是以互相引用的情况为例,通常的场景是:一个公司类可以拥有员工,那么这些员工就使用std::shared_ptr维护。另外有时候我们希望员工也能找到他的公司,所以也是用std::shared_ptr维护,这个时候问题就出来了。但是实际情况是,员工并不拥有公司,所以应该用std::weak_ptr来维护对公司的指针。

再举一个例子:我们要使用异步方式执行一系列的Task,并且Task执行完毕后获取最后的结果。所以发起Task的一方和异步执行Task的一方都需要拥有Task。

但是有时候,我们还想去了解一个Task的执行状态,比如每10秒看看进度如何,这种时候也许我们会将Task放到一个链表中做监控。这里需要注意的是,这个监控链表并不应该拥有Task本身,放到链表中的Task的生命周期不应该被一个观察者修改。所以这个时候就需要用到std::weak_ptr来安全的访问Task对象了。

最后再来聊一个新手使用std::weak_ptr容易被坑的地方:对象资源竞争。

以下代码在多线程程序中是存在很大风险的,因为wp.expired()和wp.lock()运行的期间对象可能被释放:

/ std::weak_ptr<SomeClass> wp{ sp };

if (!wp.expired()) {
    wp.lock()->DoSomething();
}

lock():尝试将weak_ptr转换为shared_ptr,如果所指向的对象仍然存在,则返回一个有效的shared_ptr,否则返回一个空指针。

expired():检查所指向的对象是否已经被释放。

正确的做法是:

auto sp = wp.lock();
if (sp) {
    sp->DoSomething();
}

std::weak_ptrlock函数是一个原子操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值