楼主: 羌清
20 0

C++智能指针(RAII思想) [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
0 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-9-3
最后登录
2018-9-3

楼主
羌清 发表于 昨天 17:04 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

1. RAII(资源获取即初始化)是什么?

核心理念:

RAII 将资源的管理与对象的生命周期紧密绑定。通过这种方式,确保资源在不再需要时能够被自动释放。

资源类型包括:堆内存、文件句柄、网络连接、互斥锁等。

对象角色:通常为栈上的局部变量。由于栈对象在离开作用域时会自动调用析构函数,因此非常适合用于管理动态分配的资源。

new

为何使用 RAII?

它利用了 C++ 中栈对象自动析构的机制,来安全地管理堆资源。具体流程如下:

  • 构造函数:负责申请资源(如分配一块内存)。
  • 析构函数:负责释放资源(如释放该内存)。

只要对象超出作用域,无论是否发生异常,析构函数都会被调用,从而避免了内存泄漏和异常路径下资源未释放的问题。

delete

2. C++ 标准库中的智能指针

C++11 引入了 <memory> 头文件,提供了三种主要的智能指针类型。它们本质上是类模板,封装了原始指针,并在析构时自动执行资源清理操作。

<memory>

A. std::unique_ptr(独占式智能指针)

在大多数场景中应优先选用此类型。

特性说明:

  • 所有权模式:独占控制,同一时间仅允许一个 unique_ptr 指向特定资源。
  • 拷贝行为:禁止复制构造,防止所有权分裂。
  • 移动语义:支持移动操作,可通过 std::move 转让资源所有权。
  • 性能表现:零开销设计,运行效率与原始指针相当,且占用空间相同。

C++ 实现示例:

#include <memory>

void usage() {
    // 创建:推荐使用 make_unique (C++14)
    std::unique_ptr<int> ptr1 = std::make_unique<int>(100);

    // std::unique_ptr<int> ptr2 = ptr1; // 编译错误!禁止拷贝

    // 转移所有权:ptr1 变为空,ptr2 接管内存
    std::unique_ptr<int> ptr2 = std::move(ptr1); 

    // 访问数据
    *ptr2 = 200;

} // 函数结束,ptr2 离开作用域,自动 delete 内存。ptr1 是空的,不做操作。

std::unique_ptr

unique_ptr

std::move

B. std::shared_ptr(共享式智能指针)

特性说明:

  • 所有权模式:多个 shared_ptr 可共享同一资源。
  • 实现原理:内部维护引用计数。每当有新指针指向资源时,计数加一;当某个指针销毁时,计数减一。当计数归零时,资源被自动释放。
  • 性能影响:存在额外开销,因需维护原子性引用计数以支持线程安全。

std::shared_ptr

void usage() {
    // 创建:推荐 make_shared,效率更高
    std::shared_ptr<int> p1 = std::make_shared<int>(100); // 计数 = 1

    {
        std::shared_ptr<int> p2 = p1; // 拷贝成功!计数 = 2
        std::cout << *p2 << std::endl;
    } // p2 离开作用域,计数 - 1。当前计数 = 1。内存未释放。

} // p1 离开作用域,计数 - 1。当前计数 = 0。内存释放。

C. std::weak_ptr(弱引用智能指针)

作为 shared_ptr 的辅助工具,weak_ptr 不持有资源的所有权,也不会增加引用计数。

主要用途:解决由 shared_ptr 引发的循环引用问题。

使用方式:weak_ptr 可指向一个由 shared_ptr 管理的对象,但无法确定对象是否仍存活。在访问前必须调用 lock() 方法尝试提升为 shared_ptr,若成功则说明对象仍在。

std::weak_ptr

shared_ptr

shared_ptr

lock()

3. 经典问题:循环引用导致的内存泄漏

这是面试高频考点。当两个对象互相持有对方的 shared_ptr 时,引用计数永远无法降为零,造成资源无法释放。

典型错误代码示例:

class B; // 前置声明

class A {
public:
std::shared_ptr<B> ptrB; // A 持有 B
~A() { std::cout << "A 销毁" << std::endl; }
};

class B {
public:
std::shared_ptr<A> ptrA; // B 持有 A
~B() { std::cout << "B 销毁" << std::endl; }
};

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

    a->ptrB = b; // A -> B (B 计数=2)
    b->ptrA = a; // B -> A (A 计数=2)
} 
// main 结束:
// a 销毁,A 计数变成 1。
// b 销毁,B 计数变成 1。
// 即使程序结束,A 和 B 的析构函数永远不会执行!内存泄漏。

解决方案:将其中一方的 shared_ptr 改为 weak_ptr。常见于“子节点引用父节点”或“观察者模式”中,使用 weak_ptr 避免强引用环。

weak_ptr

class B {
public:
std::weak_ptr<A> ptrA; // 只观测,不拥有。不增加 A 的引用计数。
// ...
};

4. 智能指针的自定义删除器机制

默认情况下,智能指针(如 unique_ptr 和 shared_ptr)在析构时会对内部指针执行 delete 操作。

然而,并非所有资源都由 new 分配,例如:

  • 通过 fopen 打开的文件流 —— 需要 fclose 关闭。
  • 通过 socket 创建的网络连接 —— 需 close 或 closesocket。
  • 使用 malloc 分配的内存 —— 应调用 free。
  • 第三方 C 库创建的对象 —— 通常提供专用的销毁函数。

fopen

fclose

socket

close

malloc

free

destroy_object

若对非 new 分配的资源直接使用 delete,会导致未定义行为甚至程序崩溃。此时必须使用自定义删除器(Custom Deleter)

unique_ptr

delete

FILE*

1. unique_ptr 与 shared_ptr 在删除器处理上的关键差异

这是常被考察的知识点,两者机制截然不同:

特性 std::unique_ptr std::shared_ptr
删除器类型地位 删除器类型是智能指针类型的一部分 删除器不是类型的一部分,仅为构造参数
代码灵活性 较低。不同类型删除器之间不可赋值 极高。只要托管对象类型一致,即使删除器不同也可相互赋值
性能表现 极高(静态绑定)。编译期可内联优化,无额外内存开销 稍低(动态绑定)。删除器存储于控制块中,存在间接调用成本

unique_ptr<int, MyDeleter> p;

shared_ptr<int> p;

2. std::unique_ptr 的删除器用法

为了追求极致性能(零开销抽象),unique_ptr 要求删除器类型在编译期就已确定。

推荐写法一:仿函数(Functor)

高效且紧凑,利用空基类优化(EBO),不会增加 unique_ptr 对象的大小。

#include <iostream>
#include <memory>

// 假设这是一个遗留的 C 风格 API
struct C_Socket { int id; };
void close_socket(C_Socket* s) {
    std::cout << "Closing socket " << s->id << std::endl;
    delete s; // 模拟释放资源
}

// 1. 定义一个仿函数类
struct SocketDeleter {
void operator()(C_Socket* s) const {
    close_socket(s);
}
};

int main() {
    // 2. 将类型作为模板参数传入
    std::unique_ptr<C_Socket, SocketDeleter> ptr(new C_Socket{42});

    // ptr 离开作用域时,会自动调用 SocketDeleter()(ptr.get())
    return 0;
}

写法二:函数指针

更直观易懂,但会使 unique_ptr 占用更多空间(因其内部需保存函数指针)。

// using 定义一个函数指针类型
using DeleteFuncType = void(*)(C_Socket*);

int main() {
    // 传入类型 和 具体函数
    std::unique_ptr<C_Socket, DeleteFuncType> ptr(new C_Socket{88}, close_socket);
    return 0;
}

3. std::shared_ptr 的删除器机制

shared_ptr 更注重灵活性,采用类型擦除(Type Erasure)技术,将删除器隐藏在其内部的控制块中。

这意味着:

  • 无需修改 shared_ptr 的类型声明。
  • 可在运行时传入任意删除逻辑。

常用写法:Lambda 表达式

最便捷的方式,无需额外定义结构体即可实现定制化释放逻辑。

#include <iostream>
#include <memory>
#include <cstdio> // for fopen, fclose

int main() {
    // 打开一个文件
    FILE* file = std::fopen("test.txt", "w");

    // 创建 shared_ptr,并在构造函数中直接传入 Lambda 作为删除器
    // 注意:类型依然是 shared_ptr<FILE>
    
    std::shared_ptr<FILE> filePtr(file, [](FILE* fp) {
        std::cout << "文件被自动关闭了" << std::endl;
        if (fp) std::fclose(fp);
    });

    std::fprintf(filePtr.get(), "Hello RAII\n");

    // filePtr 离开作用域 -> 引用计数归零 -> Lambda 被调用 -> fclose 执行
    return 0;
}

为什么 shared_ptr 如此设计?

因为 shared_ptr 更强调通用性和运行时灵活性,牺牲少量性能换取更高的适配能力,适合复杂场景下的资源管理。

如果使用
unique_ptr
上述情况便无法被放入同一个
vector
中,原因是它们的类型不同,尤其是删除器部分的类型存在差异。 而实际上,系统本身就需要分配一个堆内存块(即控制块)来管理引用计数。若顺带将删除器对象也存储在此块中,额外开销非常有限,并不会带来显著性能负担。 这一设计带来了显著的优势:
std::shared_ptr<int> p1(new int(1), [](int* p){ delete p; }); // 删除器 A
std::shared_ptr<int> p2(new int(2), [](int* p){ free(p); });   // 删除器 B

// 虽然 p1 和 p2 的回收逻辑完全不同,但它们是同一种类型!
// 所以可以放入同一个容器:
std::vector<std::shared_ptr<int>> vec;
vec.push_back(p1);
vec.push_back(p2); // 合法!
二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:include unique Shared delete share

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
jg-xs1
拉您进交流群
GMT+8, 2025-12-9 14:37