C++11新特性 13.共享智能指针shared_ptr
目录
一.基础介绍
1.基本概念
用途
2.语法
二.使用示例
示例1:基本使用
示例2:循环引用与解决方案
示例3:多线程安全示例
三.使用场景
1.对象需要在多个地方共享时
2. 在容器中存储指针时
3.解决异步编程中的生命周期问题
4.在事件驱动系统中
四.注意事项
一.基础介绍
1.基本概念
内存泄漏(Memory Leak):是指程序在运行过程中,由于某些原因导致动态分配的内存空间在不再使用时没有被正确释放,从而造成这部分内存无法被操作系统或其他程序再次使用的现象。以下从内存泄漏的产生原因、常见场景、危害以及检测和解决方法几个方面详细介绍。
为了避免内存泄露的情况发生,C++11引入了智能指针。
shared_ptr
是 C++ 标准库(C++11 及更高版本)中的一种智能指针,用于管理动态分配的内存资源。它基于 引用计数(Reference Counting) 机制,允许多个 shared_ptr
实例共享同一对象的所有权。当最后一个持有该对象的 shared_ptr
被销毁或重置时,对象会被自动释放,从而避免内存泄漏。
用途
-
共享资源所有权:当多个组件需要共同使用同一对象,且无法确定哪个组件最后释放资源时。
-
避免内存泄漏:自动管理动态分配的内存,减少手动
delete
的遗漏。 -
简化资源生命周期管理:尤其适用于复杂的数据结构(如链表、树)或容器中的动态对象。
-
线程安全:引用计数的增减操作是原子的(但对象本身的线程安全需额外处理)
2.语法
头文件:
#include <memory>
声明与初始化:
// 方式1:使用 make_shared(推荐,高效)
std::shared_ptr<int> p1 = std::make_shared<int>(42);
// 方式2:直接构造(不推荐,可能产生额外开销)
std::shared_ptr<int> p2(new int(100));
//方式3: 拷贝和移动构造
//拷贝
shared_ptr<T> ptr2 = ptr1; // ptr1是已经被创造出来的智能指针
//移动
shared<T> ptr3 = move(ptr1);// ptr1是已经被创造出来的智能指针
// 方式4:自定义删除器(例如释放文件句柄)
std::shared_ptr<FILE> p3(fopen("test.txt", "r"), [](FILE* f) { fclose(f); });
常用成员函数:
-
use_count()
:返回当前引用计数。 -
reset()
:释放当前对象的所有权,减少引用计数。 -
get()
:获取原始指针(谨慎使用,不增加引用计数)。
二.使用示例
示例1:基本使用
#include <iostream>
#include <memory>
int main() {
auto p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1; // 共享所有权,引用计数变为2
std::cout << *p1 << std::endl; // 输出 10
std::cout << "引用计数: " << p1.use_count() << std::endl; // 输出 2
p2.reset(); // p2 释放所有权,引用计数减为1
std::cout << "引用计数: " << p1.use_count() << std::endl; // 输出 1
return 0; // p1 离开作用域,引用计数归零,内存自动释放
}
示例2:循环引用与解决方案
#include <memory>
#include <iostream>
class NodeB; // 前向声明
class NodeA {
public:
std::shared_ptr<NodeB> b_ptr;
~NodeA() { std::cout << "NodeA 销毁\n"; }
};
class NodeB {
public:
// std::shared_ptr<NodeA> a_ptr; // 错误:循环引用,引用计数永不归零
std::weak_ptr<NodeA> a_ptr; // 正确:用 weak_ptr 打破循环
~NodeB() { std::cout << "NodeB 销毁\n"; }
};
int main() {
auto a = std::make_shared<NodeA>();
auto b = std::make_shared<NodeB>();
a->b_ptr = b;
b->a_ptr = a; // 使用 weak_ptr 避免循环引用
return 0; // a 和 b 的引用计数归零,正确释放
}
示例3:多线程安全示例
#include <memory>
#include <thread>
#include <vector>
class Logger {
public:
void log(const std::string& msg) {
// 假设此方法线程安全
std::cout << msg << std::endl;
}
};
void worker(std::shared_ptr<Logger> logger) {
logger->log("线程工作日志");
}
int main() {
auto logger = std::make_shared<Logger>();
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, logger); // 传递 shared_ptr 副本
}
for (auto& t : threads) {
t.join();
}
return 0; // logger 引用计数归零,自动释放
}
三.使用场景
1.对象需要在多个地方共享时
当一个对象需要在多个不同的函数、模块或类之间被使用,且希望自动管理其生命周期,防止内存泄漏时,可使用std::shared_ptr
。
例如,在一个游戏开发项目中,有一个游戏角色的信息类CharacterInfo
,游戏中的多个系统,如渲染系统、战斗系统、任务系统等都需要访问这个角色信息。
#include <iostream>
#include <memory>
class CharacterInfo {
public:
void printInfo() {
std::cout << "Character information" << std::endl;
}
};
// 渲染系统函数
void render(std::shared_ptr<CharacterInfo> info) {
info->printInfo();
}
// 战斗系统函数
void battle(std::shared_ptr<CharacterInfo> info) {
info->printInfo();
}
int main() {
std::shared_ptr<CharacterInfo> character = std::make_shared<CharacterInfo>();
render(character);
battle(character);
return 0;
}
这样多个系统都可以通过std::shared_ptr
安全地访问角色信息,并且当不再有系统需要该信息时,对象会自动被销毁。
2. 在容器中存储指针时
当使用std::vector
、std::list
等标准容器存储对象指针,且希望容器能自动管理这些对象的生命周期时,适合使用std::shared_ptr
。
比如在一个图形绘制程序中,需要管理多个图形对象(如圆形、矩形等),这些图形对象继承自一个基类Shape
。
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Draw a circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Draw a rectangle" << std::endl;
}
};
int main() {
std::vector<std::shared_ptr<Shape>> shapes;
shapes.push_back(std::make_shared<Circle>());
shapes.push_back(std::make_shared<Rectangle>());
for (const auto& shape : shapes) {
shape->draw();
}
return 0;
}
当shapes
容器销毁时,其中存储的std::shared_ptr
所管理的图形对象也会自动被释放。
3.解决异步编程中的生命周期问题
在异步编程中,例如使用线程池进行异步任务处理时,任务可能会访问某些共享资源。为了确保在任务执行期间资源不会被提前释放,可使用std::shared_ptr
。
比如一个网络服务器程序,有一个共享的数据库连接对象DatabaseConnection
,多个异步的网络请求处理任务都可能需要使用这个连接。
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
class DatabaseConnection {
public:
void query(const std::string& sql) {
std::cout << "Executing query: " << sql << std::endl;
}
};
// 模拟异步任务
void asyncTask(std::shared_ptr<DatabaseConnection> connection, const std::string& sql) {
connection->query(sql);
}
int main() {
std::shared_ptr<DatabaseConnection> connection = std::make_shared<DatabaseConnection>();
std::vector<std::thread> threads;
threads.push_back(std::thread(asyncTask, connection, "SELECT * FROM users"));
threads.push_back(std::thread(asyncTask, connection, "INSERT INTO logs VALUES (...)"));
for (auto& th : threads) {
th.join();
}
return 0;
}
通过std::shared_ptr
管理数据库连接对象,能保证在所有异步任务完成前,连接对象不会被销毁。
4.在事件驱动系统中
在事件驱动的编程模型中,对象可能会注册多个事件回调,而这些回调可能会在不同的时间点被触发。为了确保回调函数中使用的对象在回调执行时仍然有效,可使用std::shared_ptr
。
比如一个图形界面应用程序,按钮对象Button
可以注册点击事件回调,回调函数可能会访问应用程序中的其他数据对象。
#include <iostream>
#include <memory>
#include <functional>
class DataObject {
public:
void update() {
std::cout << "Data object updated" << std::endl;
}
};
class Button {
public:
//using Callback = std::function<void()> 是类型别名声明
//将 std::function<void()> 定义为 Callback,这样在后续代码中可以更方便地使用这个类型。
using Callback = std::function<void()>;
//Callback可以为所有void函数,即void函数可以通过Callback类型被调用、传参
void setCallback(const Callback& callback) {
clickCallback = callback;
}
void simulateClick() {
if (clickCallback) {
clickCallback();
}
}
private:
Callback clickCallback;
};
int main() {
std::shared_ptr<DataObject> data = std::make_shared<DataObject>();
Button button;
//lambda
button.setCallback([data]() {
data->update();
});
button.simulateClick();
return 0;
}
这样即使在按钮点击事件触发较晚的情况下,DataObject
对象也不会被提前释放 。
在这个例子中,使用 std::shared_ptr
管理 DataObject
对象可以确保对象的生命周期得到正确管理。由于 Lambda 表达式按值捕获了 data
,即使 data
在 main
函数中的原始对象超出作用域,只要 Lambda 表达式还存在,DataObject
对象就不会被销毁,避免了悬空指针的问题。
四.注意事项
1.循环引用
-
问题:两个对象互相持有
shared_ptr
,引用计数永不归零,导致内存泄漏。 -
解决:将其中一个指针改为
weak_ptr
(见示例 4.2)。
2.原始指针混用
错误示例:
int* raw = new int(10);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 错误!两个独立控制块,会重复释放
正确做法:始终通过 shared_ptr
的拷贝或 make_shared
创建。
3.性能开销
控制块开销:引用计数的原子操作(+1
/-1
)可能影响性能。
优化建议:在性能关键路径避免过度使用 shared_ptr
,改用 unique_ptr
或原始指针。
4.线程安全
引用计数安全:use_count()
的增减是原子的,但返回值可能已过时。
对象访问安全:shared_ptr
不保证对象本身的线程安全,需额外同步(如互斥锁)。
5.enable_shared_from_this
用途:在类的成员函数中安全地获取指向自身的 shared_ptr
。
示例:
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> get_ptr() {
return shared_from_this(); // 必须对象已被 shared_ptr 管理
}
};
auto obj = std::make_shared<MyClass>();
auto ptr = obj->get_ptr(); // 正确