C++ 杂记03 指针(二) 智能指针

C++中,智能指针与普通指针不同,是包含指针的一种类模板,用于管理动态分配的内存。智能指针的行为类似于常规指针,但是能够自动地释放所指向的对象,避免内存的泄露。智能指针通过对被引用对象进行计数的方式,或者其他机制,限制被引用的次数,避免形成循环引用。

相较于常规指针,在使用完以后,通常需要使用free或者delete释放指针内容,智能指针会自动释放其所管理的内存,防止程序员忘记释放动态分配的内存而导致内存泄漏。在发生异常的情况下,智能指针可以确保资源被正确地释放,提高程序的健壮性。例如,在函数执行过程中抛出异常,智能指针可以在栈展开时释放资源。

std::unique_ptr

std::unique_ptr是一种独占式智能指针,它所管理的对象只能有一个unique_ptr指向它。当unique_ptr被销毁时,它所指向的对象也会被销毁。unique_ptr不能被复制,但可以被移动(例如,通过返回函数或传递给函数)。

  • 独占所有权unique_ptr所指向的对象在同一时刻只能有一个unique_ptr拥有其所有权,当该unique_ptr被销毁时,它所管理的对象也会被自动销毁,从而确保资源不会泄露。
  • 轻量级unique_ptr对象本身通常只占用一个指针的空间,不会带来过多的额外开销,其大小通常与一个原生指针相当,这使得它在性能敏感的场景中非常高效。
  • 不可复制unique_ptr不支持普通的复制语义,因为复制会导致多个unique_ptr指向同一个对象,从而破坏独占所有权的语义。但它支持移动语义,可以通过移动操作将所有权从一个unique_ptr转移到另一个unique_ptr
#include<iostream>
#include<memory>
using namespace std;

class Resource {
public:
    Resource(int val = 0) : value(val) {
        std::cout << "Resource constructed: " << value << std::endl;
    }
    void setValue(int val)
    {
        value = val;
    }
    void showValue()
    {
        cout<<"This object value is "<<value<<endl;
    }
    ~Resource() {
        std::cout << "Resource destroyed: " << value << std::endl;
    }
    int value;
};


void example01()
{
    //unique_prt的基本使用
    // 1. 创建unique_ptr的几种方式
    std::unique_ptr<int> p1(new int(10));                    // 直接构造
    auto p2 = std::make_unique<int>(20);                     // 推荐方式(C++14)
    std::unique_ptr<int[]> arr = std::make_unique<int[]>(5); // 数组形式
    std::unique_ptr<Resource[]> arr_class(new Resource[3]); //在有默认构造函数情况下,且不需要参数的情况下,可以由编译器给初值
    std::unique_ptr<Resource[]> arr_Resource(new Resource[3]{Resource(1), Resource(2), Resource(3)}); //在给定参数的情况下,相当于是使用数组初始化的方式构造类的对象
    std::unique_ptr<Resource[]> arr_Resource2(new Resource[3]{{4},{5},{6}}); //在给定参数的情况下,给出初值,再调用类的构造函数,构造出类数组元素的对象
    //解引用操作与普通一致
    cout <<"p1指向的内容是: "<<*p1<<endl;
    //数组操作与普通数组一致,但是无法使用加减操作来寻址
    arr[2] = 100;
    //cout << "arr数组的3号元素值为 : "<<*(arr + 2)<<endl;  报错
    cout <<"arr数组3号元素指向的内容是: "<<arr[2]<<endl;

    //调用智能指针访问类数组的对象修改值
    //(arr_Resource + 2)->showValue(); 使用->调用方式会失败
    cout<<"修改前后的对象值: "<<endl;
    arr_Resource[2].showValue();
    arr_Resource[2].setValue(100);
    arr_Resource[2].showValue();

    //获取智能指针的原始指针
    cout<<"通过原始指针访问值: "<<endl;
    Resource* p_resource = arr_Resource.get();
    (p_resource+2)->showValue();

    //unique_ptr只能转移,不能复制
    std::unique_ptr<Resource[]> p2_resource = std::move(arr_Resource);
    cout<<"获取控制权后的智能指针访问数组: "<<endl;
    p2_resource[2].showValue();
    if(arr_Resource != nullptr)
    {
        arr_Resource[2].showValue();
    }
    else
    {
        cout<<"arr_Resource指针已经被置空"<<endl;
    }


}
int main()
{
    example01();
    return 0;
}

程序的输出为:

Resource constructed: 0
Resource constructed: 0
Resource constructed: 0
Resource constructed: 1
Resource constructed: 2
Resource constructed: 3
Resource constructed: 4
Resource constructed: 5
Resource constructed: 6
p1指向的内容是: 10
arr数组3号元素指向的内容是: 100
修改前后的对象值: 
This object value is 3
This object value is 100
通过原始指针访问值: 
This object value is 100
获取控制权后的智能指针访问数组: 
This object value is 100
arr_Resource指针已经被置空
Resource destroyed: 100
Resource destroyed: 2
Resource destroyed: 1
Resource destroyed: 6
Resource destroyed: 5
Resource destroyed: 4
Resource destroyed: 0
Resource destroyed: 0
Resource destroyed: 0

不建议使用unique_ptr指向一个已经存在的变量,这是因为一旦超出了变量的作用域,变量会被释放,造成指针空置而报错或者重复释放。

void example02()
{
    int data = 100; 
    std::unique_ptr<int> p1(&data); //使用unique_ptr指向一个已经存在的变量
    std::unique_ptr<int> p2(&data); //另一个unique_ptr指针
    cout<<"使用p1访问内存变量: data is "<<*p1<<endl;
    cout<<"使用p2访问内存变量: data is "<<*p2<<endl;
}
使用p1访问内存变量: data is 100
使用p2访问内存变量: data is 100
munmap_chunk(): invalid pointer
Aborted (core dumped)

因为智能指针中,已经包含了对象自动释放机制,避免内存泄漏的情况,所以重复释放也会造成程序报错,类似于普通指针的重复释放:

void example03()
{
    // int data = 100; 
    int data2 = 101;
    // std::unique_ptr<int> p1(&data); //使用unique_ptr指向一个已经存在的变量
    // std::unique_ptr<int> p2(&data); //另一个unique_ptr指针
    // cout<<"使用p1访问内存变量: data is "<<*p1<<endl;
    // cout<<"使用p2访问内存变量: data is "<<*p2<<endl;

    int* p3 = &data2;
    cout<<"使用普通指针p3访问内存变量: data2 is "<<*p3<<endl;
    int* p4 = &data2;
    cout<<"使用普通指针p4访问内存变量: data2 is "<<*p4<<endl;
    delete p3;
}
使用普通指针p3访问内存变量: data2 is 101
使用普通指针p4访问内存变量: data2 is 101
munmap_chunk(): invalid pointer
Aborted (core dumped)

unique_str的独占性

前面说到过,unique_str管理的对象只能保证被一个智能指针所管理,但是这个独占性还需要进一步理解。unique_ptr的"独占性"主要体现在所有权(ownership)而不是访问权限(accessibility)上。通过几个例子来说明:

#include <memory>
#include <iostream>

int main() {
    // 1. 错误示范:两个unique_ptr试图拥有同一块内存
    int* raw = new int(42);
    std::unique_ptr<int> ptr1(raw);
    std::unique_ptr<int> ptr2(raw);  // 严重错误!会导致双重释放
    
    // 2. 错误示范:复制unique_ptr
    std::unique_ptr<int> ptr3 = std::make_unique<int>(42);
    // std::unique_ptr<int> ptr4 = ptr3;  // 编译错误!unique_ptr不能被复制
    
    // 3. 合法但危险:普通指针访问unique_ptr管理的内存
    std::unique_ptr<int> ptr5 = std::make_unique<int>(42);
    int* dangerous_ptr = ptr5.get();  // 获取原始指针
    *dangerous_ptr = 100;             // 可以访问和修改数据
    // 当ptr5被销毁时,dangerous_ptr变成悬空指针!
    
    return 0;
} // ptr1和ptr2都会尝试删除同一块内存,导致未定义行为

unique_ptr具有匹配的内存释放机制,不需要类似普通指针借助delete关键字:

void ownership_example() {
    auto ptr = std::make_unique<int>(42);
    int* raw_ptr = ptr.get();  // raw_ptr只是借用访问权
    
    // ptr负责资源的释放,而raw_ptr不应该尝试删除
    // delete raw_ptr;  // 严重错误!
} // ptr自动释放资源
  1. unique_ptr的"独占性"是指资源所有权的独占,而不是访问权限的独占
  2. 只有拥有所有权的unique_ptr才能(也必须)负责资源的释放
  3. 其他指针可以访问同一资源,但:
    • 不能删除资源
    • 必须确保访问时资源仍然有效
    • 需要注意生命周期管理
  4. 使用get()获取原始指针时要特别小心,确保不会在unique_ptr释放后继续使用

建议:

  • 优先使用unique_ptr来管理资源
  • 谨慎使用get()
  • 明确资源的所有权
  • 如果需要共享所有权,考虑使用shared_ptr

std::shared_ptr

  1. 基本用法

在unique_ptr的基础上,其他两种指针相对比较好理解。std::shared_ptr是一种共享所有权的智能指针,允许多个shared_ptr实例共同拥有同一个对象。对象会在最后一个拥有它的shared_ptr被销毁时自动释放。

shared_ptr的声明和使用方式与unique_ptr类似:

void example03()
{
    //shared_ptr初始化方式与unique_prt还是比较一致的
    std::shared_ptr<Resource> p1(new Resource(100));
    p1->showValue();
    p1->setValue(99);
    p1->showValue();
    cout<<"当前p1管理的对应引用为: "<<p1.use_count()<<endl;
    std::shared_ptr<Resource> p2(p1); //使用p1初始化p2
    cout<<"当前p1管理的对应引用为: "<<p2.use_count()<<endl;
    std::shared_ptr<Resource> p3 = std::move(p1); //通过移动构造,初始化p3,p1成为空指针
    cout<<"当前p2管理的对应引用为: "<<p2.use_count()<<endl; 
}

被shared_ptr管理的内存数据,每多一次引用,就会增加一次引用次数。但是每释放一次智能指针,引用次数会减少。

Resource constructed: 100
This object value is 100
This object value is 99
当前p1管理的对应引用为: 1
当前p1管理的对应引用为: 2
当前p2管理的对应引用为: 2
Resource destroyed: 99
  1. 循环引用情况

使用shared_ptr如果不是很注意,会造成循环引用的情况,内存无法被真正释放,造成内存泄漏。下面对几种循环引用的情况进行说明,典型的循环引用案例包括菱形继承、相互引用这些。

  1. 直接循环引用
// A.h
class B; // 前向声明
class A {
    B* b_ptr; // 使用指针
};

// B.h
class A; // 前向声明
class B {
    A* a_ptr; // 使用指针
};

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->ptr_b = b; // A holds a shared pointer to B
b->ptr_a = a; // B holds a shared pointer to A

每当一个新的 std::shared_ptr 被创建或复制时,引用计数器会增加。当一个 std::shared_ptr 被销毁或重新赋值时,引用计数器会减少。如果引用计数器变为零,则删除所管理的对象及其控制块。

而在直接循环引用中,上面代码的计数过程如下:

(1) 创建智能指针a时,A的引用次数加1,从0到1;

(2) 创建智能指针b时,B的引用次数加1,从0到1;

(3) a->ptr_b = b; 使得,b又被a的ptr复制了一次,B的引用次数加1,从1到2;

(4) b->ptr_a = a; 使得,a又被b的ptr复制了一次,A的引用次数加1,从1到2;

当我们离开 main 函数时,会发生以下事情:

局部变量 a 被销毁,这将导致 a 的引用计数减1。但是,由于 b 中的 ptr_a 仍然持有对 a 的引用,所以 a 的引用计数从2变为1,而不是0。同样地,局部变量 b 被销毁,这将导致 b 的引用计数减1。但是,由于 a 中的 ptr_b 仍然持有对 b 的引用,所以 b 的引用计数从2变为1,而不是0。因此,a 和 b 的引用计数都保持为1,而不是降为0。结果是,这两个对象永远不会被销毁,因为它们的引用计数始终大于0,从而导致内存泄漏。

  1. 父子关系中的循环引用

类的继承和包含关系也是经常被混淆的类设计关系,在使用shared_ptr不注意的话,也容易造成循环引用。

class Child;
class Parent {
public:
    std::shared_ptr<Child> child;
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

class Child {
public:
    std::shared_ptr<Parent> parent;
    ~Child() { std::cout << "Child destroyed\n"; }
};

void parent_child_cycle() {
    auto parent = std::make_shared<Parent>();
    auto child = std::make_shared<Child>();
    
    // 建立双向引用
    parent->child = child;
    child->parent = parent;
} // 内存泄漏!
  1. 复杂对象关系中的循环引用
class Component;
class Object {
public:
    std::string name;
    std::shared_ptr<Component> component;
    
    Object(const std::string& n) : name(n) {
        std::cout << "Object " << name << " created\n";
    }
    ~Object() {
        std::cout << "Object " << name << " destroyed\n";
    }
};

class Component {
public:
    std::string name;
    std::shared_ptr<Object> owner;
    
    Component(const std::string& n) : name(n) {
        std::cout << "Component " << name << " created\n";
    }
    ~Component() {
        std::cout << "Component " << name << " destroyed\n";
    }
};

void complex_cycle() {
    auto obj = std::make_shared<Object>("MainObject");
    auto comp = std::make_shared<Component>("MainComponent");
    
    obj->component = comp;
    comp->owner = obj;
} // 内存泄漏!
  1. 使用weak_ptr解决循环引用
class SafeNode {
public:
    std::shared_ptr<SafeNode> next;
    std::weak_ptr<SafeNode> weak_next;  // 使用weak_ptr
    
    ~SafeNode() {
        std::cout << "SafeNode destroyed\n";
    }
};

void safe_cycle() {
    auto node1 = std::make_shared<SafeNode>();
    auto node2 = std::make_shared<SafeNode>();
    
    // 一个方向使用shared_ptr,另一个方向使用weak_ptr
    node1->next = node2;
    node2->weak_next = node1;  // 不会增加引用计数
    
    // 使用weak_ptr时需要检查
    if (auto shared = node2->weak_next.lock()) {
        std::cout << "Node1 still exists\n";
    }
} // 正确释放内存
posted @   Mestro  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示