【C++】34.智能指针(1)

文章目录


1. 智能指针的使用场景分析

下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。

cpp 复制代码
// 除法函数,可能抛出异常
double Divide(int a, int b)
{
    // 除数为0时抛出字符串异常
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    else
    {
        return (double)a / (double)b;
    }
}

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 (...)  // 捕获所有类型的异常
    {
        // 异常处理中确保释放资源
        cout << "delete []" << array1 << endl;
        cout << "delete []" << array2 << endl;
        delete[] array1;
        delete[] array2;
        throw;  // 重新抛出异常,保持异常的原始类型
    }

    // 正常执行路径下的资源释放
    cout << "delete []" << array1 << endl;
    delete[] array1;
    cout << "delete []" << array2 << endl;
    delete[] array2;
}

int main()
{
    try
    {
        Func();
    }
    // 分层次的异常捕获
    catch (const char* errmsg)  // 捕获字符串异常(除0错误)
    {
        cout << errmsg << endl;
    }
    catch (const exception& e)  // 捕获标准异常
    {
        cout << e.what() << endl;
    }
    catch (...)  // 捕获其他所有类型的异常
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

2. RAII和智能指针的设计思路

  • RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
  • 智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

代码1:

cpp 复制代码
// SmartPtr类:一个简单的智能指针模板类实现
template<class T>
class SmartPtr
{
public:
    // 构造函数:接收一个原始指针
    // RAII原则:Resource Acquisition Is Initialization(资源获取即初始化)
    // 当对象构造时获取资源,在对象析构时释放资源
    // 防止资源泄露,保证异常安全
    SmartPtr(T* ptr)
        :_ptr(ptr)  // 初始化成员指针
    {}

    // 析构函数:当智能指针对象销毁时自动调用
    // 负责释放所管理的内存资源
    ~SmartPtr()
    {
        cout << "delete:" << _ptr << endl;  // 打印被删除的指针地址
        delete _ptr;  // 释放内存
    }

    // 重载操作符*
    // 使智能指针可以像普通指针一样,通过*访问所指向的对象
    T& operator*()
    {
        return *_ptr;
    }

    // 重载操作符->
    // 使智能指针可以像普通指针一样通过->访问成员
    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;  // 存储原始指针的成员变量
};

// 测试函数
void f()
{
    // 创建管理pair<string, string>类型的智能指针对象
    SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
    SmartPtr<pair<string, string>> sp2(new pair<string, string>);
    SmartPtr<pair<string, string>> sp3(new pair<string, string>);
    
    // 创建管理string类型的智能指针对象
    SmartPtr<string> sp4(new string("xxxxx"));

    // 使用重载的操作符访问数据
    cout << *sp4 << endl;           // 通过*操作符访问string内容,打印xxxxx
    cout << sp1->first << endl;     // 通过->操作符访问pair的first成员,打印1111
    cout << sp1->second << endl;    // 通过->操作符访问pair的second成员,打印22222

    div();  // 调用某个分隔函数(代码中未给出具体实现)
}

// 主函数:包含异常处理
int main()
{
    try
    {
        f();  // 调用测试函数
    }
    catch (const exception& e)  // 捕获并处理标准异常
    {
        cout << e.what() << endl;  // 打印异常信息
    }

    return 0;
}

代码2:

cpp 复制代码
// SmartPtr类:一个简单的智能指针模板类实现
template<class T>
class SmartPtr
{
public:
    // 构造函数:接收一个原始指针
    // RAII原则:Resource Acquisition Is Initialization(资源获取即初始化)
    // 当对象构造时获取资源,在对象析构时释放资源
    // 防止资源泄露,保证异常安全
    SmartPtr(T* ptr)
        :_ptr(ptr)  // 初始化成员指针
    {}

    // 析构函数:当智能指针对象销毁时自动调用
    // 负责释放所管理的内存资源
    ~SmartPtr()
    {
        cout << "delete:" << _ptr << endl;  // 打印被删除的指针地址
        delete _ptr;  // 释放内存
    }

    // 重载操作符*
    // 使智能指针可以像普通指针一样,通过*访问所指向的对象
    T& operator*()
    {
        return *_ptr;
    }

    // 重载操作符->
    // 使智能指针可以像普通指针一样通过->访问成员
    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;  // 存储原始指针的成员变量
};

// C++98 auto_ptr
// 管理权转移的智能指针,已被废弃
// 缺陷:管理权转移后,原指针悬空,可能导致访问空指针

// C++11 unique_ptr
// 独占所有权的智能指针
// 特点:不允许拷贝,只允许移动,避免了auto_ptr的问题

// C++11 shared_ptr
// 共享所有权的智能指针
// 特点:使用引用计数,多个指针可以共享同一块资源

// C++11 weak_ptr
// 弱引用智能指针
// 特点:配合shared_ptr使用,不增加引用计数,用于解决循环引用问题

int main()
{
    SmartPtr<string> sp1(new string("xxxxx"));
    SmartPtr<string> sp2(new string("yyyyy"));

    // 问题代码:这里会导致严重问题
    // 1. 浅拷贝:sp2的指针直接赋值给sp1
    // 2. 原有sp1的资源没有释放,导致内存泄漏
    // 3. sp1和sp2指向同一块内存
    // 4. 当sp1和sp2析构时,同一块内存会被删除两次,导致程序崩溃
    sp1 = sp2;  // 这是一个有问题的赋值操作!

    return 0;
}

这就是为什么我们需要使用标准库提供的智能指针:

  • unique_ptr:适用于独占资源的场景
  • shared_ptr:适用于资源需要共享的场景
  • weak_ptr:适用于需要解决循环引用的场景

代码3:

cpp 复制代码
// 智能指针模板类,用于自动管理动态分配的内存
template<class T>
class SmartPtr
{
public:
    // 构造函数,接管原始指针
    // RAII(Resource Acquisition Is Initialization)模式
    // 在构造时获取资源,在析构时释放资源
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}

    // 析构函数,自动释放管理的内存
    // 保证即使发生异常,资源也能被正确释放
    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }

    // 重载解引用运算符
    // 使智能指针能像普通指针一样通过*访问数据
    T& operator*()
    {
        return *_ptr;
    }

    // 重载箭头运算符
    // 使智能指针能像普通指针一样通过->访问成员
    T* operator->()
    {
        return _ptr;
    }

    // 重载下标运算符
    // 支持数组访问语法
    T& operator[](size_t i)
    {
        return _ptr[i];
    }

private:
    T* _ptr;  // 实际管理的原始指针
};

// 除法函数,可能抛出异常
double Divide(int a, int b)
{
    // 除数为0时抛出异常
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    else
    {
        return (double)a / (double)b;
    }
}

void Func()
{
    // 使用智能指针管理动态数组
    // 不需要手动释放内存,智能指针会在函数结束或发生异常时自动释放
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];
    
    // 通过重载的[]运算符访问数组元素
    for (size_t i = 0; i < 10; i++)
    {
        sp1[i] = sp2[i] = i;
    }

    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
    // 函数结束时,sp1和sp2的析构函数会自动调用,释放内存
}

int main()
{
    try
    {
        Func();
    }
    // 异常处理
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

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

  • C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。
  • auto_ptr 是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。
  • unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。
  • shared_ptr 是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。
  • weak_ptr 是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。
  • 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁一点,unique_ptrshared_ptr都特化了一份[]的版本,使用时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
  • template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);
  • shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。
  • shared_ptrunique_ptr 都支持了operator bool的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
  • shared_ptrunique_ptr 都得构造函数都使用explicit 修饰,防止普通指针隐式类型转换成智能指针对象。

代码1:

cpp 复制代码
// 日期结构体,用于演示智能指针的行为
struct Date
{
    int _year;
    int _month;
    int _day;

    // 构造函数,提供默认参数
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}

    // 析构函数,用于观察对象销毁时机
    ~Date()
    {
        cout << "~Date()" << endl;
    }
};

int main()
{
    // 1. auto_ptr (C++17已废弃)
    auto_ptr<Date> ap1(new Date);
    // auto_ptr的拷贝构造会转移所有权
    // ap1在拷贝后变为空指针,这是一个危险的特性
    auto_ptr<Date> ap2(ap1);
    // 危险:ap1已经为空,访问会导致未定义行为
    //ap1->_year++;  // 程序崩溃,但编译器不会提示错误

    // 2. unique_ptr (推荐使用)
    unique_ptr<Date> up1(new Date);
    // unique_ptr禁止拷贝,体现独占所有权的语义
    //unique_ptr<Date> up2(up1);  // 编译错误

    // unique_ptr支持移动语义
    // 使用move显式转移所有权,up1在移动后变为空
    unique_ptr<Date> up3(move(up1));
    // 此时up1为空,up3获得对象的所有权
    up1->_year++;// 编译错误!编译器会提示up1可能为空(编译会报错)

    // 3. shared_ptr (推荐使用)
    shared_ptr<Date> sp1(new Date);
    // shared_ptr支持拷贝,通过引用计数实现共享所有权
    shared_ptr<Date> sp2(sp1);  // 引用计数+1
    shared_ptr<Date> sp3(sp2);  // 引用计数+1

    // 显示当前引用计数
    cout << sp1.use_count() << endl;  // 输出3

    // 所有指向同一对象的shared_ptr都可以正常访问对象
    sp1->_year++;
    cout << sp1->_year << endl;  // 所有指针访问的是同一个对象
    cout << sp2->_year << endl;  // 值相同
    cout << sp3->_year << endl;  // 值相同

    // shared_ptr也支持移动语义
    // 移动后sp1变为空,但不影响sp2和sp3
    shared_ptr<Date> sp4(move(sp1));
    
    return 0;
    // 离开作用域时,最后一个shared_ptr销毁时才会删除Date对象
}

代码2:

cpp 复制代码
// 函数模板作为删除器,用于删除数组
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}

// 仿函数类作为删除器,用于删除数组
template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

// 文件指针的删除器
class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

int main()
{
    // 错误示范:直接使用new[]会导致析构错误
    // unique_ptr<Date> up1(new Date[10]);    // 错误:会用delete释放
    // shared_ptr<Date> sp1(new Date[10]);    // 错误:会用delete释放

    // 方案1:使用数组特化版本
    // C++标准库为数组提供了特化版本,自动使用delete[]
    unique_ptr<Date[]> up1(new Date[5]);
    shared_ptr<Date[]> sp1(new Date[5]);

    // 方案2:使用自定义删除器

    // 2.1 使用仿函数作为删除器
    // unique_ptr要在模板参数中指定删除器类型
    unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
    // shared_ptr在构造函数中指定删除器对象
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

    // 2.2 使用函数指针作为删除器
    // unique_ptr需要在模板参数中指定函数指针类型
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
    // shared_ptr直接传入函数指针
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

    // 2.3 使用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);

    // 管理其他资源:文件指针
    // 使用仿函数关闭文件
    shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
    // 使用lambda关闭文件
    shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    });

    return 0;
}

代码3:

cpp 复制代码
int main()
{
    // 1. 创建shared_ptr的几种方式

    // 方式1:构造函数直接传递new的对象
    shared_ptr<Date> sp1(new Date(2024, 9, 11));

    // 方式2:使用make_shared函数(推荐)
    // make_shared更安全且效率更高,因为只分配一次内存
    shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);

    // 方式3:使用auto进行类型推导
    auto sp3 = make_shared<Date>(2024, 9, 11);

    // 创建空的shared_ptr
    shared_ptr<Date> sp4;

    // 2. 智能指针的布尔转换

    // 检查智能指针是否为空
    // 实际上调用operator bool()
    if (sp1)
        cout << "sp1 is not nullptr" << endl;

    // 检查智能指针是否为空
    if (!sp4)
        cout << "sp1 is nullptr" << endl;

    // 3. 错误用法示例
    
    // 错误:不能直接用=赋值原始指针
    // 必须使用显式构造或make_shared
    //shared_ptr<Date> sp5 = new Date(2024, 9, 11);  // 编译错误
    
    // 错误:同样不能直接赋值原始指针
    //unique_ptr<Date> sp6 = new Date(2024, 9, 11);  // 编译错误

    // 正确的写法:
    shared_ptr<Date> sp5(new Date(2024, 9, 11));
    unique_ptr<Date> sp6(new Date(2024, 9, 11));

    return 0;
}

代码4:

cpp 复制代码
struct Node
{
    A _val;  // 节点存储的数据

    // 方案1:普通指针
    //Node* _next;   // 指向下一个节点
    //Node* _prev;   // 指向前一个节点
    // 缺点:需要手动管理内存,容易造成内存泄漏或重复释放

    // 方案2:shared_ptr方式
    //bit::shared_ptr<Node> _next;  // 指向下一个节点的智能指针
    //bit::shared_ptr<Node> _prev;  // 指向前一个节点的智能指针
    // 缺点:会导致循环引用,引用计数永不为0,无法释放内存

    // 方案3:weak_ptr方式(最佳解决方案)
    bit::weak_ptr<Node> _next;  // 弱引用指向下一节点
    bit::weak_ptr<Node> _prev;  // 弱引用指向前一节点
    // 优点:
    // 1. weak_ptr不增加引用计数
    // 2. 可以访问资源但不参与生命周期管理
    // 3. 成功解决循环引用问题
};

int main()
{
    // 方案1:使用普通指针
    //Node* n1 = new Node;
    //Node* n2 = new Node;
    // 需要手动释放内存
    //delete n1;
    //delete n2;

    // 方案2和3:使用智能指针
    // 创建两个节点,初始引用计数均为1
    bit::shared_ptr<Node> sp1(new Node);
    bit::shared_ptr<Node> sp2(new Node);

    // 打印初始引用计数
    cout << sp1.use_count() << endl;  // 输出1
    cout << sp2.use_count() << endl;  // 输出1

    // 建立双向链接
    sp1->_next = sp2;  // 使用weak_ptr,不会增加sp2的引用计数
    sp2->_prev = sp1;  // 使用weak_ptr,不会增加sp1的引用计数

    // 再次打印引用计数
    cout << sp1.use_count() << endl;  // 仍然是1
    cout << sp2.use_count() << endl;  // 仍然是1

    // main函数结束时:
    // 1. sp1和sp2离开作用域
    // 2. 引用计数变为0
    // 3. Node对象被正确释放
    return 0;
}

这段代码演示了 shared_ptr 循环引用的问题,以及如何使用 weak_ptr 解决:

  1. 首先看结构体定义中三种不同的指针方式:
cpp 复制代码
struct Node
{
    A _val;
    // 方式1:普通指针
    // Node* _next;
    // Node* _prev;

    // 方式2:shared_ptr - 会导致循环引用
    // bit::shared_ptr<Node> _next;
    // bit::shared_ptr<Node> _prev;

    // 方式3:weak_ptr - 解决循环引用
    bit::weak_ptr<Node> _next;
    bit::weak_ptr<Node> _prev;
};
  1. 使用普通指针的问题:
cpp 复制代码
Node* n1 = new Node;
Node* n2 = new Node;
// ... 需要手动管理内存
delete n1;
delete n2;
  • 需要手动管理内存
  • 容易忘记释放导致内存泄漏
  • 不够安全
  1. 使用 shared_ptr 时的循环引用问题:
cpp 复制代码
bit::shared_ptr<Node> sp1(new Node);  // sp1引用计数=1
bit::shared_ptr<Node> sp2(new Node);  // sp2引用计数=1

cout << sp1.use_count() << endl;  // 输出1
cout << sp2.use_count() << endl;  // 输出1

// 创建循环引用
sp1->_next = sp2;  // sp2的引用计数变为2
sp2->_prev = sp1;  // sp1的引用计数变为2

cout << sp1.use_count() << endl;  // 输出2
cout << sp2.use_count() << endl;  // 输出2

问题:

  • sp1引用了sp2,sp2引用了sp1
  • 当main函数结束时,sp1和sp2的引用计数都不会降到0
  • 导致内存泄漏

为什么引用计数会变为2:

cpp 复制代码
// 初始状态:每个shared_ptr引用计数为1
bit::shared_ptr<Node> sp1(new Node);  // count = 1
bit::shared_ptr<Node> sp2(new Node);  // count = 1

/*
sp1 --> [Node1] count=1
sp2 --> [Node2] count=1
*/

// 当执行 sp1->_next = sp2 时:
// Node1的_next成员(它是shared_ptr)获得了一份sp2的拷贝
sp1->_next = sp2;  // sp2的计数+1,变为2

/*
sp1 ----------------> [Node1] count=1
                      |
                      | _next
                      ↓
sp2 ----------------> [Node2] count=2
*/

// 当执行 sp2->_prev = sp1 时:
// Node2的_prev成员(它是shared_ptr)获得了一份sp1的拷贝
sp2->_prev = sp1;  // sp1的计数+1,变为2

/*
sp1 ----------------> [Node1] count=2 <---- _prev
                      |                     |
                      | _next               |
                      ↓                     |
sp2 ----------------> [Node2] count=2 ------
*/

关键点:

  1. 每个Node结构体中的 _next_prevshared_ptr
  2. 当一个 shared_ptr 被赋值给另一个 shared_ptr 时,会调用拷贝构造或赋值运算符
  3. 这会导致引用计数增加

所以:

  • sp1->_next = sp2 相当于又产生了一个指向Node2的 shared_ptr
  • sp2->_prev = sp1 相当于又产生了一个指向Node1的 shared_ptr

这就是为什么:

  1. Node1被sp1和Node2的_prev共同指向,引用计数为2
  2. Node2被sp2和Node1的_next共同指向,引用计数为2

这种情况下,即使main函数结束,sp1和sp2离开作用域:

  • sp1的计数从2减为1(Node2的_prev还在引用它)
  • sp2的计数从2减为1(Node1的_next还在引用它)
  • 由于计数都不为0,两个Node对象都不会被释放
  • 造成内存泄漏

这就是为什么需要使用 weak_ptr - 它可以指向对象但不增加引用计数,从而打破这种循环引用。


  1. 使用 weak_ptr 解决循环引用:
cpp 复制代码
bit::shared_ptr<Node> sp1(new Node);
bit::shared_ptr<Node> sp2(new Node);

sp1->_next = sp2;  // weak_ptr不增加引用计数
sp2->_prev = sp1;  // weak_ptr不增加引用计数

cout << sp1.use_count() << endl;  // 仍然是1
cout << sp2.use_count() << endl;  // 仍然是1

weak_ptr的优势:

  • 不增加引用计数
  • 可以访问资源
  • 不参与资源的生命周期管理
  • 当原始的shared_ptr被销毁时,weak_ptr会自动变为无效

总结:

  • weak_ptr 专门用来解决 shared_ptr 循环引用的问题
  • weak_ptr 不是 RAII 智能指针,它不管理资源的生命周期
  • 在需要打破循环引用的地方使用 weak_ptr 替代 shared_ptr

四个智能指针:

四个智能指针:

  • auto_ptr
  • unique_ptr
  • shared_ptr🌟
  • weak_ptr

auto_ptr

cpp 复制代码
template<class T>
	class auto_ptr
	{
	public:
		// RAII
		// 像指针一样
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		// ap3(ap1)
		// 管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

	private:
		T* _ptr;
	};

unique_ptr

cpp 复制代码
template<class T>
	class unique_ptr
	{
	public:
		// RAII
		// 像指针一样
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		// ap3(ap1)
		// 管理权转移
		// 防拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
	private:
		T* _ptr;
	};

shared_ptr(重要)

cpp 复制代码
template<class T>
class shared_ptr
{
public:
    // 构造函数:创建新的shared_ptr对象
    // ptr: 指向要管理的对象的指针
    // 初始化:设置指针和引用计数(初始为1)
    shared_ptr(T* ptr = nullptr)
        :_ptr(ptr)
        , _pcount(new int(1))  // 新建计数器,初始值为1
    {}

    // 析构函数:当shared_ptr对象销毁时调用
    // 减少引用计数,当计数为0时释放资源
    ~shared_ptr()
    {
        if (--(*_pcount) == 0)  // 引用计数减1,如果变为0
        {
            cout << "delete:" << _ptr << endl;
            delete _ptr;      // 释放管理的对象
            delete _pcount;   // 释放计数器
        }
    }

    // 重载*运算符:访问管理的对象
    T& operator*()
    {
        return *_ptr;
    }

    // 重载->运算符:访问管理的对象的成员
    T* operator->()
    {
        return _ptr;
    }

    // 拷贝构造函数:创建一个新的shared_ptr,共享同一个对象
    // sp: 现有的shared_ptr对象
    shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)      // 共享被管理的对象
        , _pcount(sp._pcount) // 共享引用计数
    {
        ++(*_pcount);       // 增加引用计数
    }

    // 赋值运算符:将一个shared_ptr赋值给另一个
    // 需要处理自赋值和原有资源的释放
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        // 自赋值检查
        if (_ptr == sp._ptr)
            return *this;

        // 释放原有资源(如果引用计数变为0)
        if (--(*_pcount) == 0)
        {
            delete _ptr;
            delete _pcount;
        }

        // 共享新资源
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        ++(*_pcount);       // 增加新资源的引用计数

        return *this;
    }

    // 获取当前引用计数
    int use_count() const
    {
        return *_pcount;
    }

    // 获取原始指针
    T* get() const
    {
        return _ptr;
    }

private:
    T* _ptr;      // 指向管理的对象的指针
    int* _pcount; // 指向引用计数的指针
    // 不能用static int _pcount的原因:
    // 1. static成员是类共享的,所有shared_ptr实例会共享同一个计数
    // 2. 无法区分不同对象组的引用计数
    // 3. 不能正确管理多个不同对象的生命周期
};

这里不能用static int _pcount;而是用int* _pcount;这涉及到 shared_ptr 的核心设计:

cpp 复制代码
// 错误方式:使用静态计数器
template<class T>
class SharedPtr {
private:
    T* _ptr;
    static int _pcount;  // 静态成员,所有SharedPtr对象共享同一个计数器
};

int main() {
    // 问题演示
    SharedPtr<Date> sp1(new Date(2024, 2, 14));  // _pcount = 1
    SharedPtr<Date> sp2(new Date(2025, 1, 1));   // _pcount = 2
    
    // 问题:sp1和sp2共享同一个静态计数器!
    // 无法区分不同对象的引用计数
}

正确方式:每组共享指针有自己的计数器:

cpp 复制代码
template<class T>
class SharedPtr {
private:
    T* _ptr;     // 指向实际对象的指针
    int* _pcount; // 指向引用计数的指针
public:
    SharedPtr(T* ptr = nullptr)
        : _ptr(ptr)
        , _pcount(new int(1))  // 每个新对象组都有自己的计数器
    {}
};

int main() {
    // 正确示例
    SharedPtr<Date> sp1(new Date(2024, 2, 14));  // _pcount指向值为1的计数器
    SharedPtr<Date> sp2(sp1);                    // 共享sp1的计数器,值增加到2
    
    SharedPtr<Date> sp3(new Date(2025, 1, 1));   // 新的计数器,值为1
    SharedPtr<Date> sp4(sp3);                    // 共享sp3的计数器,值为2
    
    // sp1和sp2共享一个计数器
    // sp3和sp4共享另一个计数器
}

使用静态计数器的问题:

  1. 所有 SharedPtr 对象会共享同一个计数器
  2. 无法区分不同对象组的引用计数
  3. 不能正确管理多个不同对象的生命周期

使用成员指针计数器的优势:

  1. 每组共享同一对象的指针有自己的计数器
  2. 可以准确跟踪每个对象被引用的次数
  3. 当计数器归零时,才释放对应的对象

这就是为什么要将计数器设计为指针成员而不是静态成员的原因。


weak_ptr

cpp 复制代码
template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

4. 智能指针的原理

  • 下面我们模拟实现了auto_ptr和unique_ptr的核心功能,这两个智能指针的实现比较简单,大家了解一下原理即可。auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr的思路是不支持拷贝。
  • 大家重点要看看shared_ptr是如何设计的,尤其是引用计数的设计,主要这里一份资源就需要一个引用计数,所以引用计数才用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。
cpp 复制代码
namespace bit
{
    // auto_ptr实现 - C++17已废弃的设计
    template<class T>
    class auto_ptr
    {
    public:
        // 构造函数,接管原始指针
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 拷贝构造函数 - 转移所有权
        auto_ptr(auto_ptr<T>& sp)
            :_ptr(sp._ptr)
        {
            sp._ptr = nullptr;  // 源对象置空
        }

        // 赋值运算符 - 转移所有权
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                delete _ptr;         // 释放当前资源
                _ptr = ap._ptr;      // 获取ap的资源
                ap._ptr = nullptr;   // ap置空
            }
            return *this;
        }

        ~auto_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        // 指针操作符重载
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }

    private:
        T* _ptr;
    };

    // unique_ptr实现 - 独占所有权
    template<class T>
    class unique_ptr
    {
    public:
        // explicit防止隐式转换
        explicit unique_ptr(T* ptr)
            :_ptr(ptr)
        {}

        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }

        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }

        // 禁止拷贝和赋值
        unique_ptr(const unique_ptr<T>& sp) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

        // 支持移动语义
        unique_ptr(unique_ptr<T>&& sp)
            :_ptr(sp._ptr)
        {
            sp._ptr = nullptr;
        }

        unique_ptr<T>& operator=(unique_ptr<T>&& sp)
        {
            delete _ptr;
            _ptr = sp._ptr;
            sp._ptr = nullptr;
            return *this;
        }

    private:
        T* _ptr;
    };

    // shared_ptr实现 - 共享所有权
    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数
        explicit shared_ptr(T* ptr = nullptr)
            : _ptr(ptr)
            , _pcount(new int(1))
        {}

        // 支持自定义删除器的构造
        template<class D>
        shared_ptr(T* ptr, D del)
            : _ptr(ptr)
            , _pcount(new int(1))
            , _del(del)
        {}

        // 拷贝构造 - 增加引用计数
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pcount(sp._pcount)
            , _del(sp._del)
        {
            ++(*_pcount);
        }

        // 释放资源
        void release()
        {
            if (--(*_pcount) == 0)
            {
                _del(_ptr);        // 使用删除器释放资源
                delete _pcount;
                _ptr = nullptr;
                _pcount = nullptr;
            }
        }

        // 赋值运算符
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            if (_ptr != sp._ptr)
            {
                release();
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                ++(*_pcount);
                _del = sp._del;
            }
            return *this;
        }

        ~shared_ptr()
        {
            release();
        }

        // 访问接口
        T* get() const { return _ptr; }
        int use_count() const { return *_pcount; }
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }

    private:
        T* _ptr;                     // 管理的资源指针
        int* _pcount;                // 引用计数
        // 在实际实现中应使用原子计数
        //atomic<int>* _pcount;
        
        // 默认删除器
        function<void(T*)> _del = [](T* ptr) {delete ptr; };
    };

    // weak_ptr实现 - 弱引用
    template<class T>
    class weak_ptr
    {
    public:
        weak_ptr() {}

        // 通过shared_ptr构造
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get())
        {}

        // 赋值运算符
        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get();
            return *this;
        }

    private:
        T* _ptr = nullptr;
    };
}

int main()
{
    // auto_ptr测试 - 展示所有权转移的问题
    bit::auto_ptr<Date> ap1(new Date);
    bit::auto_ptr<Date> ap2(ap1);    // ap1变为空
    //ap1->_year++;                   // 错误:ap1已经为空

    // unique_ptr测试 - 展示独占所有权
    bit::unique_ptr<Date> up1(new Date);
    //bit::unique_ptr<Date> up2(up1); // 错误:不能拷贝
    bit::unique_ptr<Date> up3(move(up1)); // 正确:可以移动

    // shared_ptr测试 - 展示共享所有权
    bit::shared_ptr<Date> sp1(new Date);
    bit::shared_ptr<Date> sp2(sp1);  // 引用计数增加
    bit::shared_ptr<Date> sp3(sp2);  // 引用计数增加
    
    cout << sp1.use_count() << endl; // 输出引用计数
    sp1->_year++;                    // 修改共享对象
    cout << sp1->_year << endl;      // 所有指针看到相同的值
    cout << sp2->_year << endl;
    cout << sp3->_year << endl;

    return 0;
}
相关推荐
令狐掌门2 小时前
C++中间件DDS介绍
c++·中间件·c++ dds
专注VB编程开发20年7 小时前
除了 EasyXLS,加载和显示.xlsx 格式的excel表格,并支持单元格背景色、边框线颜色和粗细等格式化特性
c++·windows·excel·mfc·xlsx
夏天的阳光吖8 小时前
C++蓝桥杯基础篇(四)
开发语言·c++·蓝桥杯
oioihoii9 小时前
C++17 中的 std::to_chars 和 std::from_chars:高效且安全的字符串转换工具
开发语言·c++
张胤尘9 小时前
C/C++ | 每日一练 (2)
c语言·c++·面试
強云10 小时前
23种设计模式 - 装饰器模式
c++·设计模式·装饰器模式
yatingliu201910 小时前
代码随想录算法训练营第六天| 242.有效的字母异位词 、349. 两个数组的交集、202. 快乐数 、1. 两数之和
c++·算法
鄃鳕11 小时前
单例模式【C++设计模式】
c++·单例模式·设计模式
只做开心事11 小时前
C++之特殊类设计
开发语言·c++
宋康11 小时前
C/C++ 指针避坑20条
c语言·开发语言·c++