在C++的早期开发中,开发者需要手动调用new和delete管理动态分配的内存,这种方式容易因为忘记释放内存或者重复释放导致内存泄漏、程序崩溃等问题。现代C++引入了智能指针机制,其中shared_ptr是最常用的一种,它通过引用计数自动管理动态对象的生命周期,大幅降低了手动内存管理的出错概率。
shared_ptr的基本使用
创建shared_ptr对象
shared_ptr位于<memory>头文件中,创建shared_ptr最安全的方式是使用make_shared函数,它会一次性分配对象内存和引用计数控制块的内存,效率比先new再构造shared_ptr更高。
#include <memory>
#include <iostream>
int main() {
// 使用make_shared创建shared_ptr,指向一个int对象,值为10
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// 输出指向的值和引用计数
std::cout << "ptr1指向的值: " << *ptr1 << std::endl;
std::cout << "当前引用计数: " << ptr1.use_count() << std::endl;
return 0;
}
shared_ptr的拷贝与赋值
当shared_ptr被拷贝或者赋值时,引用计数会自动增加,当shared_ptr对象销毁时,引用计数会自动减少,当引用计数降为0时,会自动释放所指向的对象内存。
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::cout << "初始引用计数: " << ptr1.use_count() << std::endl; // 输出1
// 拷贝ptr1,引用计数加1
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "拷贝后引用计数: " << ptr1.use_count() << std::endl; // 输出2
// ptr2离开作用域会自动销毁,引用计数减1
{
std::shared_ptr<int> ptr3 = ptr1;
std::cout << "新增拷贝后引用计数: " << ptr1.use_count() << std::endl; // 输出3
}
// ptr3销毁后,引用计数回到2
std::cout << "ptr3销毁后引用计数: " << ptr1.use_count() << std::endl; // 输出2
// 赋值为nullptr,引用计数减1
ptr2 = nullptr;
std::cout << "ptr2置空后引用计数: " << ptr1.use_count() << std::endl; // 输出1
return 0;
}
shared_ptr的底层原理
引用计数控制块
shared_ptr的底层实现依赖一个引用计数控制块,控制块中存储了指向对象的指针、引用计数、弱引用计数等信息。每个被shared_ptr管理的对象都对应一个唯一的控制块,所有指向该对象的shared_ptr都共享这个控制块。
控制块的内存分配时机有两种:使用make_shared时,控制块和对象内存一次性分配;使用new构造shared_ptr时,会单独分配控制块内存。这也是推荐优先使用make_shared的原因之一,能减少内存分配次数。
引用计数的更新逻辑
- 当新的shared_ptr拷贝已存在的shared_ptr时,引用计数加1
- 当shared_ptr被销毁(比如离开作用域、被赋值为其他值)时,引用计数减1
- 当引用计数减为0时,shared_ptr会自动调用删除器释放所指向的对象内存,同时如果弱引用计数也为0,会释放控制块本身的内存
简单的模拟实现
为了更直观理解shared_ptr的原理,我们可以实现一个简化版的MySharedPtr,核心逻辑就是维护引用计数,在构造、拷贝、析构时更新引用计数。
#include <iostream>
// 简化版shared_ptr实现
template <typename T>
class MySharedPtr {
private:
T* ptr; // 指向管理的对象
int* ref_count; // 指向引用计数,所有拷贝共享同一个计数
public:
// 构造函数,接收原始指针
explicit MySharedPtr(T* p = nullptr) : ptr(p), ref_count(nullptr) {
if (ptr != nullptr) {
ref_count = new int(1); // 初始引用计数为1
}
}
// 拷贝构造函数
MySharedPtr(const MySharedPtr<T>& other) : ptr(other.ptr), ref_count(other.ref_count) {
if (ref_count != nullptr) {
(*ref_count)++; // 引用计数加1
}
}
// 析构函数
~MySharedPtr() {
if (ref_count != nullptr) {
(*ref_count)--; // 引用计数减1
if (*ref_count == 0) {
// 引用计数为0,释放对象和控制块
delete ptr;
delete ref_count;
ptr = nullptr;
ref_count = nullptr;
}
}
}
// 重载->运算符,方便访问对象成员
T* operator->() const {
return ptr;
}
// 重载*运算符,方便解引用
T& operator*() const {
return *ptr;
}
// 获取当前引用计数
int use_count() const {
return ref_count == nullptr ? 0 : *ref_count;
}
};
// 测试代码
int main() {
MySharedPtr<int> myPtr(new int(30));
std::cout << "初始引用计数: " << myPtr.use_count() << std::endl; // 输出1
MySharedPtr<int> myPtr2 = myPtr;
std::cout << "拷贝后引用计数: " << myPtr.use_count() << std::endl; // 输出2
std::cout << "指向的值: " << *myPtr << std::endl; // 输出30
return 0;
}
shared_ptr的使用注意事项
避免循环引用
如果两个对象互相持有对方的shared_ptr,会形成循环引用,导致引用计数永远无法降为0,从而产生内存泄漏。这种情况需要使用weak_ptr来打破循环。
#include <memory>
#include <iostream>
class B; // 向前声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() {
std::cout << "A对象销毁" << std::endl;
}
};
class B {
public:
std::shared_ptr<A> a_ptr; // 这里如果换成std::weak_ptr<A>就能打破循环
~B() {
std::cout << "B对象销毁" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // 循环引用,离开作用域后A和B都不会被销毁
std::cout << "a引用计数: " << a.use_count() << std::endl; // 输出2
std::cout << "b引用计数: " << b.use_count() << std::endl; // 输出2
return 0;
}
不要将原始指针和多个shared_ptr混用
同一个原始指针不要用来构造多个独立的shared_ptr,否则会导致每个shared_ptr都有自己独立的引用计数,最终会重复释放同一块内存,引发程序崩溃。
#include <memory>
int main() {
int* raw_ptr = new int(40);
// 错误用法:两个shared_ptr独立管理同一个原始指针
std::shared_ptr<int> ptr1(raw_ptr);
std::shared_ptr<int> ptr2(raw_ptr); // 会导致重复释放,程序崩溃
return 0;
}
优先使用make_shared
如前文所述,make_shared能一次性分配对象和控制块的内存,减少内存分配次数,同时能避免在构造shared_ptr之前发生异常导致的内存泄漏,是创建shared_ptr的首选方式。
shared_ptr作为现代C++内存管理的重要工具,合理运用能大幅减少内存相关的bug,开发者需要理解其原理和注意事项,才能在实际开发中正确发挥它的作用。
shared_ptr智能指针现代内存管理引用计数修改时间:2026-06-22 15:13:10