人言客头像
关注

智能指针的使用及其原理

目录:

1. 智能指针的使用

只要是堆上 new 出来的资源,不想手动管理 delete,优先考虑智能指针;能用unique_ptr就不用shared_ptr

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const std::exception& e)
    {
        cout << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}
void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10];  // ⚠️重点:这一行有可能抛出异常!

    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl; //除0抛异常
    }
    catch (...)
    {
        //释放两块内存,重新抛出异常
        delete[] array1;
        delete[] array2;
        throw;
    }

    //正常执行路径释放
    delete[] array1;
    delete[] array2;
}

两个场景分析:

场景 1:Divide()函数抛出异常(除 0)

进入catch(...),执行delete[] array1、array2,内存正常释放。 👉 这个场景看起来没问题。

场景 2:int* array2 = new int[10]; 这里直接抛出异常(内存分配失败)

new分配内存失败,会抛出bad_alloc异常!

  1. array1 = new int[10]成功,内存分配出来;

  2. 执行 array2 的 new 时抛异常;

  3. 函数立刻终止,直接跳出 Func;

  4. 还没有进入 try 块!catch 捕获不到!

  5. array1指向的内存没有释放 → 内存泄漏!

使用 unique_ptr 改造:

#include <memory>
void Func()
{
    // 全部用智能指针管理
    std::unique_ptr<int[]> array1 = std::make_unique<int[]>(10);
    std::unique_ptr<int[]> array2 = std::make_unique<int[]>(10);

    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
    // 不需要任何try-catch手动释放!
}

分析两种异常情况

  1. array2 构造时 new 抛异常array1是局部栈对象,已经构造完成。发生异常,函数退出,array1 自动调用析构释放内存。
  2. Divide 函数抛异常 函数提前退出,array1array2都是局部对象,离开作用域自动析构,两块内存全部释放。

2. RAII和智能指针

RAII 全称:Resource Acquisition Is Initialization,资源获取即初始化。 它是 C++ 管理资源最核心的设计思想。

核心思想一句话概括:

把资源交给局部对象托管:对象构造时获取资源,对象析构时释放资源;依靠对象生命周期,保证资源一定会释放。

智能指针就是应用 RAII 思想封装裸指针的类模板。 只靠 RAII 还不够,我们希望它用起来像普通指针,所以还要重载运算符模拟指针行为。

两大关键点:

  1. 遵循 RAII:构造接收裸指针,析构执行delete自动释放内存;
  2. 重载运算符operator*operator->operator[],让智能指针拥有和裸指针一样的访问语法。

简易智能指针示例:

template<class T>
class SmartPtr
{
public:
    // 构造函数:获取资源,接收new出来的裸指针(RAII第一步)
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}

    // 析构函数:释放资源(RAII核心)
    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }

    // 重载解引用 *sp
    T& operator*()
    {
        return *_ptr;
    }

    // 重载箭头 ->sp
    T* operator->()
    {
        return _ptr;
    }

    // 重载下标 [] ,支持数组访问 sp[i]
    T& operator[](size_t i)
    {
        return _ptr[i];
    }

private:
    T* _ptr; // 私有成员:封装裸指针
};
  1. template<class T> 类模板:支持任意类型指针int*自定义类型*
  2. 私有成员T* _ptr封装裸指针,资源保存在对象内部;
  3. 构造函数:接收外部new出来的地址,完成资源获取
  4. 析构函数:对象销毁自动执行delete[]自动释放堆内存
  5. 三个运算符重载:对外提供和原生指针一模一样的使用方式。

3. C++标准库智能指针的使用

智能指针概览:

C++ 标准库提供了多种智能指针,除了 weak_ptr 外,它们都符合 RAII 行为。它们的主要区别在于拷贝时的思路不同

  1. auto_ptr (C++98):拷贝时转移管理权(已废弃)。

  2. unique_ptr (C++11):独占所有权,不支持拷贝,只支持移动。

  3. shared_ptr (C++11):共享所有权,基于引用计数。

  4. weak_ptr (C++11):弱引用,不控制生命周期,用于解决循环引用问题。

3.1 auto_ptr

auto_ptr 是 C++98 设计出来的智能指针。它的特点是:拷贝时会把被拷贝对象的资源管理权转移给拷贝对象

致命缺陷:
这是一个非常糟糕的设计。当发生拷贝时,原对象(被拷贝者)会被置空(悬空)。如果后续继续访问原对象,就会导致程序崩溃。

代码示例:

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

struct Date {
    int _year;
    Date(int year = 1) : _year(year) {}
    ~Date() { cout << "~Date()" << endl; }
};

int main() {
    auto_ptr<Date> ap1(new Date);
    
    // 拷贝时,管理权限转移,被拷贝对象 ap1 悬空
    auto_ptr<Date> ap2(ap1);
    
    // 空指针访问,ap1 对象已经悬空,下面代码会崩溃
    // ap1->_year++; 
    
    return 0;
}

3.2 unique_ptr

unique_ptr 翻译过来是“唯一指针”。它的特点非常鲜明:不支持拷贝,只支持移动

  • 适用场景: 如果不需要拷贝的场景,非常建议使用它。

  • 移动语义: 虽然不支持拷贝,但支持 std::move 移动。注意,移动后原对象也会悬空,所以使用移动要谨慎。

示例:

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

struct Date {
    int _year;
    Date(int year = 1) : _year(year) {}
    ~Date() { cout << "~Date()" << endl; }
};

int main() {
    unique_ptr<Date> up1(new Date);
    
    // 不支持拷贝
    // unique_ptr<Date> up2(up1); // 编译报错
    
    // 支持移动,但是移动后 up1 也悬空
    unique_ptr<Date> up3(move(up1)); 
    
    return 0;
}

3.3 shared_ptr

shared_ptr 是“共享指针”。它的特点是支持拷贝,也支持移动。底层是通过引用计数的方式实现的。

  • 机制: 每当有一个新的 shared_ptr 指向同一块资源,引用计数 +1;每当一个 shared_ptr 析构,引用计数 -1。当引用计数为 0 时,释放资源。

  • 适用场景: 如果需要拷贝的场景,就需要使用它。

示例:

int main() {
    shared_ptr<Date> sp1(new Date);
    
    // 支持拷贝,引用计数增加
    shared_ptr<Date> sp2(sp1);
    shared_ptr<Date> sp3(sp2);
    
    cout << sp1.use_count() << endl; // 输出 3
    
    sp1->_year++;
    cout << sp1->_year << endl; // 输出 2
    cout << sp2->_year << endl; // 输出 2
    cout << sp3->_year << endl; // 输出 2
    
    // 支持移动,但是移动后 sp1 也悬空
    shared_ptr<Date> sp4(move(sp1));
    
    return 0;
}

3.4 weak_ptr

weak_ptr 是“弱指针”。它完全不同于上面的智能指针。

  • 特点: 不支持 RAII,意味着不能用它直接管理资源。它不增加引用计数。

  • 产生本质: 是为了解决 shared_ptr循环引用导致内存泄漏的问题。

weak_ptr 的核心价值:通常发生在两个对象互相持有对方的 shared_ptr 时,导致引用计数永远无法归零。

错误版本:

#include <iostream>
#include <memory>

struct Node {
    int val;
    std::shared_ptr<Node> next; // shared_ptr互相引用

    Node(int v) : val(v) {
        std::cout << "构造 Node " << val << "\n";
    }
    ~Node() {
        std::cout << "析构 Node " << val << "\n";
    }
};

int main() {
    auto a = std::make_shared<Node>(1);
    auto b = std::make_shared<Node>(2);

    a->next = b;
    b->next = a; // 循环引用!

    // main函数结束,a、b离开作用域
    // 但是a、b内部互相持有shared_ptr,引用计数 != 0
    // 析构函数不会调用 → 内存泄漏!
    return 0;
}

weak_ptr 修复版本:

#include <iostream>
#include <memory>

struct Node {
    int val;
    std::weak_ptr<Node> next;  // weak_ptr 弱指针,不增加引用计数

    Node(int v) : val(v) {
        std::cout << "构造 Node " << val << "\n";
    }
    ~Node() {
        std::cout << "析构 Node " << val << "\n";
    }
};

int main() {
    auto a = std::make_shared<Node>(1);
    auto b = std::make_shared<Node>(2);

    a->next = b;
    b->next = a;

    // weak_ptr使用:lock() 尝试提升为shared_ptr
    if (auto sp = a->next.lock()) {
        std::cout << "a 的 next 值:" << sp->val << "\n";
    }

    // main结束,a,b销毁;weak_ptr不影响计数,对象正常释放
    return 0;
}

3.5 make_shared

shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。

template <class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);

基本用法:

#include <memory>

// 直接使用构造函数
std::shared_ptr<Foo> p1(new Foo(1, 2));

// 使用 make_shared(推荐)
auto p2 = std::make_shared<Foo>(1, 2);

make_shared 将参数完美转发给 Foo 的构造函数,返回值是一个 shared_ptr<Foo>

优势:

  1. 内存分配次数少: 直接构造 shared_ptr 需要两次内存分配(对象本身 + 控制块),而 make_shared 只分配一次(对象和控制块在同一块内存)。

  2. 异常安全: 避免因参数求值顺序导致的内存泄漏。

int main() {
    // 直接构造
    shared_ptr<Date> sp1(new Date(2024, 9, 11));
    
    // 使用 make_shared 构造
    shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
    
    // 配合 auto 使用更简洁
    auto sp3 = make_shared<Date>(2024, 9, 11);
    
    return 0;
}

3.6 explicit 修饰构造函数

shared_ptrunique_ptr 的构造函数都使用了 explicit 修饰。这是为了防止普通指针隐式类型转换成智能指针对象。

#include <iostream>
#include <memory>

struct Date {
    int _year;
    Date(int year = 1) : _year(year) {}
    ~Date() { std::cout << "~Date()" << std::endl; }
};

void takeSmartPtr(std::shared_ptr<Date> sp) {
    std::cout << "管理权已移交,当前引用计数: " << sp.use_count() << std::endl;
}

int main() {
    // 错误:禁止隐式转换 (编译报错)
    // shared_ptr<Date> sp1 = new Date(); 
    // takeSmartPtr(new Date()); 

    // 正确:必须显式调用构造函数
    std::shared_ptr<Date> sp2(new Date()); 
    takeSmartPtr(sp2); 

    // 正确:使用 make_shared (更加推荐)
    auto sp3 = std::make_shared<Date>();
    takeSmartPtr(sp3);

    return 0;
}

4. 删除器

智能指针析构时默认是进行 delete 释放资源。这就意味着,如果不是 new 出来的资源,交给智能指针管理,析构时就会崩溃。

为了解决这个问题,智能指针支持在构造时给定一个删除器。删除器本质是一个可调用对象(仿函数、函数指针、Lambda),在智能指针析构时会调用它来释放资源。

4.1 数组的特化版本

因为 new[] 经常使用,为了简洁,unique_ptrshared_ptr 都特化了一份 [] 的版本。这样在析构时就会自动调用 delete[]

// 如果没有特化,直接这样写会崩溃(因为默认 delete 不匹配 new[])
// unique_ptr<Date> up1(new Date[10]); 

// 使用特化版本,管理 new[] 的资源
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);

4.2 自定义删除器

unique_ptrshared_ptr 支持删除器的方式有所不同:

  • unique_ptr:是在类模板参数中支持的。

  • shared_ptr:是在构造函数参数中支持的。

方法1:

template<class T>
class DeleteArray {
public:
    void operator()(T* ptr) {
        delete[] ptr;
    }
};

// unique_ptr 需要将删除器类型作为模板参数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);

// shared_ptr 只需要在构造函数中传入删除器对象
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

方法2:

// 函数指针做删除器
template<class T>
void DeleteArrayFunc(T* ptr) {
    delete[] ptr;
}

// unique_ptr 传入函数指针类型
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
// shared_ptr 直接传函数指针
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);


// Lambda 表达式做删除器
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };

// unique_ptr 使用 decltype 推导 Lambda 类型
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
// shared_ptr 直接传 Lambda
shared_ptr<Date> sp4(new Date[5], delArrOBJ);

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/xiaoxin_________/article/details/164885759

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--