C++智能指针

一、什么是智能指针

智能指针是C++11标准引入的一种高级指针类型,用于管理动态分配的内存资源,旨在减少内存泄漏和悬挂指针等问题的风险。

在传统的指针使用中,我们使用new来动态分配内存,并使用指针管理地址:

{
    int* p = new int(5); //动态分配一个int类型的内存
    int* arr = new int[10]; //动态分配一个int型数组的内存

    delete p;
    delete[] arr;
}

但是这样分配内存,一旦程序在指针释放前发生异常或我们不小心忘了释放指针,或者使用了已经delete的指针,都会很容易造成程序崩溃。

所以引入智能指针,由C++标准库自动管理动态内存的生命周期,避免由于程序员对指针的管理疏忽造成的问题。

什么是内存泄漏?

内存泄漏是指程序在使用完动态分配内存(如通过new、malloc分配的内存)后,没有及时地释放,导致这些内存块在程序运行期间无法再被利用,最终程序会随着的运行占用越来越多的内存,甚至崩溃。

虽然这些内存会在程序退出后被操作系统回收,但是内存泄漏问题会造成系统资源利用率降低,影响操作系统或其他应用程序的运行,在高负载或长时间运行时,会导致程序性能下降、产生崩溃,十分影响程序稳定性。

例如下面代码就会造成内存泄漏

int func(){
    int* p = new int(5); //动态分配一个int类型的内存
    return *p;
}

什么是悬挂指针、野指针?

指针悬挂是指 指针指向的内存已经被释放,但是指针仍然指向这块地址,这样的指针可以叫悬挂指针,也可以叫野指针。

野指针指的是指针变量不为nullptr,但是指向的地址是未被初始化或者已经被释放(不清楚是什么数据的一块内存)的指针。

对悬挂指针和野指针指向的内存进行的操作,都有可能造成篡改未知数据,可能产生数据损坏、程序崩溃或其他无法预测的错误。

void func(){
    int* p = new int(5);
    delete p; // 释放p指向的内存
    *p = 6; // 错误行为!p指向的内存已经被释放,p是悬挂指针,不可以再修改*p的数据!
    p = nullptr; // 正确行为!及时置空,避免指针悬挂
}

为了避免悬挂指针,当指针被释放后,要及时将该指针置为 nullptr,避免随后该指针被误用。

二、智能指针的使用

1、unique_ptr

unique_ptr是一种独占所有权的智能指针,表示一个指针只能被一个unique_ptr所有,不能被复制,只能被移动

unique_ptr的创建

指向单个对象的unique_ptr

1)使用make_unique创建unique_ptr(见下面代码A)

2)使用new创建unique_ptr(见下面代码A)

3)使用unique_ptr管理已经存在的指针(见下面代码A)

代码A 使用unique_ptr

#include <iostream>
#include <algorithm>
using namespace  std;

class A
{
public:
    A(int i)
    {
        id = i;
        cout << "A Constructor. id=" << id <<endl;
    }

    ~A(){
        cout << "A Destructor. id=" << id <<endl;
    }

    void show(){
        cout << "This is A. id=" << id << endl;
    }

private:
    int id = -1;
};

void func()
{
    //1) 使用make_unique创建unique_ptr
    unique_ptr<A> p1 = std::make_unique<A>(1);
    p1->show();
    //2) 使用new创建unique_ptr -- 不推荐
    unique_ptr<A> p2(new A(2));
    p2->show();
    //3) unique_ptr管理已经存在的指针
    A *pa = new A(3); //new出一个新的对象
    unique_ptr<A> p3(pa); //使用unique_ptr进行管理
    //注意!这之后及时将pa置空,不要再使用
    pa = nullptr;
    p3->show();
}

int main(){
    cout << "main begin..." << endl;
    func();
    cout << "main end..." << endl;
    return 0;
}

输出:

从结果看,三个unique_ptr管理的对象,在超出unique_ptr作用于后都正确进行了释放。

如果,不使用只能指针呢?我们简单使用new来动态构建一个A的对象,并且退出函数不进行释放,看看会发生什么

代码B:不使用智能指针

在代码A的基础上增加funcc函数,并调用

void funcc()
{
    A* p = new A(0);
    p->show();
}

int main(){
    cout << "main begin..." << endl;
    funcc();
    cout << "main end..." << endl;
    return 0;
}

输出:

从输出看,退出funcc后,我们new出来的对象都没有被及时释放,而指针p是临时变量,已经丢失,这样就发生了内存泄漏。

调用new操作符时都发生了什么?

第一步:new会请求系统分配一块适当大小的内存(通常通过malloc或底层内存分配函数实现)。如果系统无法分配足够内存,new将抛出一个std::bad_alloc的异常,内存不会被分配;

第二步:如果内存分配成功,new会调用类构造函数初始化对象。如果在构造函数中发生了异常,则该异常被抛出,new将不会返回一个有效指针。但因为第一步已经分配了内存,所以将造成内存泄漏。

为什么不推荐使用2)使用new来创建unique_ptr?

使用unique_ptr<A> p2(new A(2));这种方式创建unique_ptr并不推荐,主要是因为它会导致一些内存管理和资源管理的复杂性,make_unique更加安全简洁,可读性更高。

make_unique具有异常安全的特性,若发生异常,make_unique会确保不会分配内存,不会造成内存泄漏;而调用new后,如果内存已经分配后发生异常,有可能导致内存泄漏。

我们将上述A类的构造函数中抛出一个异常,模拟动态分配内存时发生异常的情况

A(int i){
    id = i;
    cout << "A Constructor. id=" << id <<endl;
    throw runtime_error("throw exeption...");
}
void func1()
{
    unique_ptr<A> p1; //将p1声明在外部,使能在catch块能访问到
    try {
        p1 = std::make_unique<A>(1);
        cout << "p1=" << p1.get() <<endl; //unique_ptr的get()函数可以获取其所管理的地址
        //unique_ptr<A> p2(new A(2));
        //cout << "p2=" << p2.get() <<endl;
    }catch (const exception& e) {
        cout << "p1=" << p1.get() <<endl;
        cout << e.what() << endl;
    }
}

int main(){
    cout << "main begin..." << endl;
    func1();
    cout << "main end..." << endl;
    return 0;
}

输出:

从结果看,由于A的构造函数中会抛出异常,p1的指针是0,并没有建立成功。

指向数组的unique_ptr

unique_ptr支持指向数组类型,但是要注意使用数组类型的特化版本声明指针 unique_ptr<T[]> 而不是 unique_ptr<T>。这样在其销毁时调用的将是delete[],而不是delete。

示例代码:

void func()
{
    //1) 使用make_unique创建unique_ptr
    unique_ptr<int[]> p1 = std::make_unique<int[]>(5); //创建指向数据(具有5个元素)的智能指针
    for(int i = 0; i < 5; ++i)
    {
        p1[i] = i; // 可以通过[]访问数组
    }
    //2) 使用new创建unique_ptr -- 同样不推荐
    unique_ptr<int[]> p2(new int[5]);
    cout << p1[3] <<endl;
}

unique_ptr 的拷贝和移动

上面我们就提到过,unique_ptr是一种独占所有权的指针。同一时刻,一个unique_ptr独占其指向的内存。其独占性体现在其不可以被复制,但可以被移动。

int main(){
    unique_ptr<A> p1 = std::make_unique<A>(1);
    p1->show();
    cout << "p1 -> " << p1.get() << endl;
    //unique_ptr<A> p2 = p1; //错误。无法编译通过,unique_ptr不可以被复制!
    unique_ptr<A> p2 = std::move(p1); //正确。unique_ptr支持被移动,转移所有权
    cout << "\n move after...\n";
    p2->show();
    cout << "p1 -> " << p1.get() << endl; //此时会发现p1的值已经为0
    cout << "p2 -> " << p2.get() << endl;

    return 0;
}

输出:

但是unique_ptr并不能完美控制它管理的内存块一定没有其他指针指向了。如下代码,编译并不会报错,也可以正常运行。但从输出结果可见这块内存被二次释放,这就会产生无法预测的问题,所以我们编写代码一定要小心!

int main(){
    A* tmp = new A(0);
    unique_ptr<A> p1(tmp);
    unique_ptr<A> p2(tmp);
    cout << "p1 -> " << p1.get() << endl;
    cout << "p2 -> " << p2.get() << endl;

    return 0;
}

输出:

unique_ptr获取原始底层指针

unique_ptr可以通过get()获取原始底层指针,即如果是unique_ptr<int>的智能指针,通过get可以获得其对应的 int* 的指针,并可以当成 int* 指针使用。

void show_a(A* ap)
{
    ap->show();
}

int main(){
    unique_ptr<A> p = make_unique<A>(6);
    //show_a(p); //错误!类型不匹配
    show_a(p.get()); //正确
    cout << p.get() <<endl; //可以输出所指向的地址

    return 0;
}

输出:

unique_ptr的释放

unique_ptr除了在生命周期结束前自动释放所指资源外,还能调用reset()函数进行主动释放

int main(){
    unique_ptr<A> p = make_unique<A>(1);
    cout << p.get() <<endl;
    p.reset(); //主动释放资源
     cout << "p reseted!!!" <<endl;
    cout << p.get() <<endl;
    return 0;
}

unique_ptr释放所有权

如果一个资源已经交给unique_ptr管理,想要释放该内存的管理权,我们可以使用release释放指针的所有权。

int main(){
    unique_ptr<A> p = make_unique<A>(1);
    cout << p.get() <<endl;
    auto p1 = p.release(); //释放unique_ptr的所有权
    cout << endl << "after release..." << endl;
    cout << "p->" << p.get() <<endl;
    cout << "p1->" << p1 <<endl;
    delete p1; //由于是普通指针,注意使用完释放内存

    return 0;
}

unique_ptr自定义删除器

std::unique_ptr可以使用自定义删除器,以支持不同的资源释放策略。

通俗的说,有的资源只让unique_ptr自动调用delete或者delete[] 无法完全释放。这时,我们可以自定义释放策略,传给unique_ptr,让其释放资源的时候自动执行。

unique_ptr支持传入函数指针和函数对象作为删除器。注意,make_unique不支持自定义删除器的使用,所以

1)函数指针作为删除器
//定义删除器函数
void func_deleter(A* a)
{
    cout << "func_deleter run..." << endl;
    delete a;
}

int main(){
    unique_ptr<A, void(*)(A*)> p(new A(6), func_deleter);
    p->show();
    cout << "main finished" << endl;

    return 0;
}

输出

2)函数对象作为删除器

我们还可以使用重载了()的函数对象作为删除器,可以用来构造更加复杂的释放操作。

//定义删除器类
struct ClassDeleter
{
    void operator()(A* a) const{
        cout << "ClassDeleter run..." << endl;
        delete a;
    }
};

int main(){
    unique_ptr<A, ClassDeleter> p(new A(6), ClassDeleter());
    p->show();
    cout << "main finished" << endl;

    return 0;
}

输出:

2、shared_ptr

shared_ptr和引用计数

shared_ptr是提供了共享所有权的共享指针。也就是说,和unique_ptr不同的是,shared_ptr可以进行复制,并且会自动管理计数,并在最后一个共享同一对象的指针被销毁后才对所指对象进行释放。

shared_ptr是基于引用计数管理共享指针。引用计数初始为0,每当有一个新的shared_ptr实例指向同一资源时,引用计数就会+1,当有一个shared_ptr被销毁时,引用计数-1.当引用计数为0时,资源将被释放。

shared_ptr类会为每组指向相同内存的shared_ptr维护一个“控制块”对象,用于存储引用计数、原始指针等数据,每个shared_ptr实例都维护一个指向“控制块”的指针。

shared_ptr的使用

shared_ptr的使用大部分可以参考unique_ptr,只是由于shared_ptr是可共享的,所以会在相关使用上有所不同。

下面代码简单写下shared_ptr的创建、销毁以及引用计数的打印

使用shared_ptr类的额use_count()函数,可以获取当前指针的引用计数数值

int main(){
    shared_ptr<A> p1 = make_shared<A>(1);
    cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
    shared_ptr<A> p2 = p1; // p2由p1复制而来,指向相同资源
    shared_ptr<A> p3 = make_shared<A>(2);
    cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
    cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
    cout << "p3 -> " << p3.get() << " use count=" << p3.use_count() <<endl;

    cout << endl << "when reset p1..." << endl;
    p1.reset(); // 销毁p1,p1指针失效。但是p1之前指向的资源由于仍被p2引用,所以还未释放
    cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
    cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;

    return 0;
}

输出:

从输出可以看引用计数的变化情况

需要注意的是,shared_ptr对引用计数的增加,限制在发生了拷贝或赋值。并不能完全将指向同一资源的指针都归为一组。例如下列代码,p1,p2虽然指向同一资源,但是创建p2后并没有计数+1,而是分在了不同“控制块”。我们写代码时要注意

int main(){
    A* tmp = new A(0);
    shared_ptr<A> p1(tmp);
    cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
    shared_ptr<A> p2(tmp);
    cout << endl;
    cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
    cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
    shared_ptr<A> p3 = p2;
    cout << endl;
    cout << "p1 -> " << p1.get() << " use count=" << p1.use_count() <<endl;
    cout << "p2 -> " << p2.get() << " use count=" << p2.use_count() <<endl;
    cout << "p3 -> " << p3.get() << " use count=" << p3.use_count() <<endl;

    return 0;
}

输出:

其他shared_ptr的使用方法可以参考unique_ptr,这里不多做介绍。想了解更多的小伙伴可以参考官方文档。

shared_ptr线程安全

shared_ptr的引用计数操作是原子操作,所以可以安全地在多线程环境下使用。

#include <iostream>
#include <algorithm>
#include <thread>
using namespace  std;

void shared_thread_func(shared_ptr<A> p)
{
    cout << "thread id = " << this_thread::get_id() << endl;
    cout << "p->" << p.get() << " use_count=" << p.use_count() <<endl;
    this_thread::sleep_for(chrono::seconds(1));
}
int main(){
    shared_ptr<A> p1 = make_shared<A>(6);
    thread th1(shared_thread_func, p1);
    thread th2(shared_thread_func, p1);

    th1.join();
    th2.join();

    return 0;
}

输出:

从结果看,指针参数传入shared_thread_func函数时,进行了复制,所以引用计数为3,但是形参的销毁并没有导致资源被错误释放。程序运行完成后,正确释放了资源。

shared_ptr循环引用问题

循环引用是指两个或多个对象批次持有对方的引用的现象。这会导致它们的引用计数无法降为0,无法释放资源,引起内存泄漏。

如下列代码:

当B的实例想要释放时,首先要释放C的引用,但是在释放C的资源时,需要首先释放B的对象。这样循环引用,导致谁也无法释放资源、完成析构。

class C;
class B
{
public:
    B(){
        cout << "B Constructor." <<endl;
    }

    ~B(){
        cout << "B Destructor." << endl;
    }

    std::shared_ptr<C> c_ptr; //声明一个指向C对象的指针
};

class C
{
public:
    C(){
        cout << "C Constructor." <<endl;
    }

    ~C(){
        cout << "C Destructor." << endl;
    }

    std::shared_ptr<B> b_ptr; //声明一个指向B对象的指针
};

int main(){

    shared_ptr<B> b_p = make_shared<B>();
    shared_ptr<C> c_p = make_shared<C>();

    cout << "b_p->" << b_p.get() << " count=" << b_p.use_count() << endl;
    cout << "c_p->" << c_p.get() << " count=" << c_p.use_count() << endl;

    b_p->c_ptr = c_p;
    c_p->b_ptr = b_p;

    cout << "b_p->" << b_p.get() << " count=" << b_p.use_count() << endl;
    cout << "c_p->" << c_p.get() << " count=" << c_p.use_count() << endl;

    return 0;
}

输出可见,最后B、C的对象没有被释放

避免循环引用问题,除了更加合理地设计程序外,还可以使用weak_ptr来解决。

3、weak_ptr

weak_ptr主要为了解决shared_ptr的循环引用问题引入。weak_ptr允许持有一个对象的引用,但是不会增加该对象的引用计数。所以weak_ptr不负责控制对象的生命周期。

weak_ptr解决引用计数问题

将上述类C的指针改为weak_ptr,则可以解决循环引用。

class C
{
public:
    C(){
        cout << "C Constructor." <<endl;
    }

    ~C(){
        cout << "C Destructor." << endl;
    }

    std::weak_ptr<B> b_ptr; //声明一个指向B对象的weak_ptr指针
};

输出

weak_ptr的使用

上面提到了,weak_ptr并不负责管理所指对象的生命周期。可想而知,直接使用weak_ptr获取指向的对象是危险的。

1)构造weak_ptr指针

如下所示,weak_ptr主要使用shared_ptr对象构建,指向该shared_ptr的对象

int main(){
    shared_ptr<A> p_shared = make_shared<A>(1);
    // 1)从shared_ptr对象创建
    weak_ptr<A> p_weak1(p_shared);
    weak_ptr<A> p_weak2 = p_shared;
    
    // 2)默认构造
    weak_ptr<A> p_weak3; //weak_ptr具有默认构造方式,不指向任何对象
    return 0;
}
2)获取weak_ptr所指对象

从上面有提到,weak_ptr不进行对象生命周期的管理。所以直接使用weak_ptr是不安全的,所以一般使用weak_ptr的lock()函数获取一个对应的shared_ptr指针,由shared_ptr指针进行对资源的访问。

lock的声明如下

shared_ptr<T> lock() const noexcept;

如下代码

int main(){
    shared_ptr<A> p_shared = make_shared<A>(1);
    cout << p_shared.get() << " " << p_shared.use_count() << endl;

    weak_ptr<A> p_weak1(p_shared);

    cout << "\nweak_ptr lock\n";
    if(auto p_s = p_weak1.lock()) //调用lock()
    {
        p_s->show();
        cout << p_s.get() << " " << p_s.use_count() << endl;
    }
    return 0;
}

输出

从输出看,调用lock()后,返回了一个shared_ptr的指针p_s,并且增加了资源的引用计数。这样,在p_s的生命周期内,其所指向的资源由于一直在被p_s引用,所以不会被释放。这样,p_s对资源的使用就是安全的。

另外,当在调用lock()之前,weak_ptr观察的资源已经被释放,那么lock()将返回空的shared_ptr对象。

int main(){
    shared_ptr<A> p_shared = make_shared<A>(1);
    cout << p_shared.get() << " " << p_shared.use_count() << endl;

    weak_ptr<A> p_weak1(p_shared);

    p_shared.reset(); //释放资源
    cout << "\nweak_ptr lock\n";
    if(auto p_s = p_weak1.lock()) //调用lock()
    {
        p_s->show();
        cout << p_s.get() << " " << p_s.use_count() << endl;
    }
    else
    {
        cout << "p_s is " << p_s << endl;
    }
    return 0;
}

输出

3)判断weak_ptr指向资源是否有效

上一小节,我们知道可以通过lock()的返回值来判断weak_ptr指向资源是否还有效。这一小节,介绍另外一个函数expired。

expired()函数的声明如下,用于判断所属权是否已经过期,如果过期,则返回true,否则返回false.

bool expired() const noexcept;

使用如下

int main(){
    shared_ptr<A> p_shared = make_shared<A>(1);
    cout << p_shared.get() << " " << p_shared.use_count() << endl;

    weak_ptr<A> p_weak1(p_shared);

    cout << "p_weak1.expired = " << p_weak1.expired() << endl;
    p_shared.reset(); //释放资源
    cout << "after reset...\n";
    cout << "p_weak1.expired = " << p_weak1.expired() << endl;

    return 0;
}

输出

4)其他使用

weak_ptr也支持使用use_count获取所指对象的引用计数;

支持使用reset()对资源进行释放,但要注意的是,调用weak_ptr不会使引用计数-1,而是直接归0,也就是说资源将会直接被释放;

但是不支持get()函数直接获取对象地址。

int main(){
    shared_ptr<A> p_shared = make_shared<A>(1);
    shared_ptr<A> p_s2 = p_shared;
    cout << p_shared.get() << " " << p_shared.use_count() << endl;
    weak_ptr<A> p_weak1(p_shared);

    cout << "p_weak1.use_count = " << p_weak1.use_count() << endl;
    p_weak1.reset(); //释放资源
    cout << "after reset...\n";
    cout << "p_weak1.use_count = " << p_weak1.use_count() << endl;

    return 0;
}

输出

【参考资料】

unique_ptr 类 | Microsoft Learn

shared_ptr 类 | Microsoft Learn

weak_ptr 类 | Microsoft Learn

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值