智能指针到底该怎么用:现代C++内存管理实践指南

C++
972 words

为什么需要智能指针?

在C++开发中,手动管理内存资源(如new/delete)容易导致:

  • 内存泄漏:忘记释放资源
  • 悬垂指针:访问已释放的内存
  • 双重释放:多次删除同一对象

智能指针通过RAII(Resource Acquisition Is Initialization)技术自动管理资源生命周期:

  1. 在构造函数中获取资源
  2. 在析构函数中释放资源
  3. 确保异常安全(即使发生异常也能正确释放资源)
1
2
3
4
5
6
7
8
9
10
11
12
// 传统方式 - 有内存泄漏风险
void unsafe_example() {
int* raw_ptr = new int(42);
// 如果这里抛出异常...
delete raw_ptr; // 可能不会执行
}

// 智能指针方式 - 异常安全
void safe_example() {
std::unique_ptr<int> smart_ptr = std::make_unique<int>(42);
// 即使抛出异常,资源也会自动释放
}

三大智能指针使用场景

1. unique_ptr:独占所有权指针

  • 适用场景
    • 工厂函数返回对象
    • 作为类成员(明确所有权关系)
    • 局部临时对象管理
1
2
3
4
5
6
7
8
9
10
11
12
13
// 工厂函数示例
std::unique_ptr<Connection> create_connection() {
return std::make_unique<Connection>("db://localhost");
}

// 类成员示例
class DeviceController {
private:
std::unique_ptr<Device> device_; // 明确设备所有权
public:
DeviceController()
: device_(std::make_unique<Device>()) {}
};

2. shared_ptr:共享所有权指针

  • 适用场景
    • 多对象共享同一资源
    • 缓存系统
    • 观察者模式(需配合weak_ptr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 共享配置数据
class AppConfig {
public:
static std::shared_ptr<AppConfig> load() {
static auto config = std::make_shared<AppConfig>();
return config;
}
};

// 多模块共享配置
void module1() {
auto config = AppConfig::load();
// 使用配置...
}

void module2() {
auto config = AppConfig::load();
// 使用相同配置...
}

3. weak_ptr:解决循环引用问题

  • 适用场景
    • 打破shared_ptr循环引用
    • 实现缓存和观察者模式
    • 临时访问共享资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Observer {
std::weak_ptr<Subject> subject_; // 避免循环引用

public:
void observe(std::shared_ptr<Subject> subject) {
subject_ = subject;
}

void notify() {
if (auto subject = subject_.lock()) {
// 安全访问
subject->update();
}
}
};

智能指针最佳实践

1. 优先使用make_unique/make_shared

1
2
3
4
5
// 好:单次内存分配(控制块+对象)
auto ptr1 = std::make_shared<Widget>();

// 不好:两次内存分配
std::shared_ptr<Widget> ptr2(new Widget);

2. 避免裸指针转换

1
2
3
4
5
6
7
8
// 危险:可能造成双重释放
Widget* raw = new Widget;
std::shared_ptr<Widget> p1(raw);
std::shared_ptr<Widget> p2(raw); // 错误!

// 正确方式
auto p1 = std::make_shared<Widget>();
auto p2 = p1; // 共享所有权

3. 自定义删除器处理特殊资源

1
2
3
4
5
6
7
// 文件句柄管理
std::unique_ptr<FILE, decltype(&fclose)>
file_ptr(fopen("data.txt", "r"), &fclose);

// 数组类型
std::shared_ptr<int[]> arr(new int[10],
[](int* p) { delete[] p; });

4. 多线程注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::shared_ptr<Counter> global_counter;

void thread_work() {
// 线程安全:引用计数原子操作
auto local_counter = global_counter;

// 非线程安全:需要额外同步
if (!local_counter) {
std::lock_guard<std::mutex> lock(mtx);
if (!global_counter) {
global_counter = std::make_shared<Counter>();
}
local_counter = global_counter;
}
}

常见陷阱与调试技巧

陷阱1:循环引用

1
2
3
4
5
6
7
8
9
10
struct Node {
std::shared_ptr<Node> next;
// std::weak_ptr<Node> prev; // 正确方式
std::shared_ptr<Node> prev; // 导致循环引用
};

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // 引用计数永远不会归零

解决方案:使用weak_ptr打破循环

陷阱2:返回unique_ptr的原始指针

1
2
3
4
5
6
7
class ResourceHolder {
std::unique_ptr<Resource> resource_;
public:
Resource* get() const { return resource_.get(); } // 危险!
};

// 外部可能delete指针导致双重释放

解决方案:返回weak_ptr或const引用

调试技巧:使用enable_shared_from_this

1
2
3
4
5
6
7
8
class Session : public std::enable_shared_from_this<Session> {
public:
void process() {
// 安全获取自身的shared_ptr
auto self = shared_from_this();
queue_.push(self); // 放入任务队列
}
};

C++17/20智能指针增强

C++17特性

1
2
3
4
5
6
7
8
// 数组支持
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);

// 结构化绑定
auto [ptr1, ptr2] = std::make_tuple(
std::make_unique<int>(1),
std::make_unique<int>(2)
);

C++20特性

1
2
3
4
5
// 原子shared_ptr
std::atomic<std::shared_ptr<Config>> atomic_config;

// make_shared支持对齐内存
auto aligned_ptr = std::make_shared<AlignedStruct>();

总结:何时使用哪种智能指针?

场景 推荐指针 说明
独占资源 unique_ptr 明确所有权,零开销
共享资源 shared_ptr 需要共享访问权
缓存/观察 weak_ptr 避免循环引用
特殊资源 自定义删除器 文件句柄、数组等
多线程共享 atomic C++20起可用
Comments