C++11中智能指针的使用(shared_ptr、unique_ptr、weak_ptr)
一、shared_ptr原理
shared_ptr 是另一种智能指针,用于实现多个 shared_ptr 实例共享同一个对象的所有权。它通过内部的控制块(通常是一个包含计数器和指向对象的指针的结构)来管理对象的生命周期。每当一个新的 shared_ptr 被创建并指向对象时,控制块中的计数器就会递增;每当一个 shared_ptr 被销毁或重置时,计数器就会递减。当计数器减至零时,对象被删除。
1.std::shared_ptr的原理
std::shared_ptr
的核心原理是引用计数。它通过一个**控制块(Control Block)**来管理对象的生命周期。控制块记录了以下信息:
1.1 引用计数(Strong Count):
表示当前有多少个 std::shared_ptr
正在管理同一个对象。
当一个 std::shared_ptr
被复制时,引用计数加1。
当一个 std::shared_ptr
被销毁时,引用计数减1。
当引用计数变为0时,对象会被自动释放。
1.2 弱引用计数(Weak Count):
用于支持 std::weak_ptr
,表示有多少个 std::weak_ptr
正在观察这个对象。
当弱引用计数变为0时,控制块本身也会被销毁。
1.3 删除器(Deleter):
一个函数对象,用于在对象被销毁时执行清理操作(如释放内存)。
1.4 分配器(Allocator):
用于分配和释放内存。
1.1 控制块的结构
控制块通常是一个独立的结构体,与 std::shared_ptr
和 std::weak_ptr
共享。它的结构大致如下:
struct ControlBlock {int* ptr; // 指向被管理的对象unsigned* strong; // 引用计数(Strong Count)unsigned* weak; // 弱引用计数(Weak Count)Deleter deleter; // 删除器
};
1.2 生命周期管理
-
当创建一个新的
std::shared_ptr
时,控制块会被初始化,引用计数设置为1。 -
当一个
std::shared_ptr
被复制时,引用计数加1。 -
当一个
std::shared_ptr
被销毁时,引用计数减1。如果引用计数变为0,控制块会调用删除器来释放对象。 -
当最后一个
std::weak_ptr
被销毁时,控制块本身也会被销毁。
1.3 引用计数的线程安全性
std::shared_ptr
的引用计数操作是线程安全的。它使用原子操作来保证在多线程环境下引用计数的正确性。
2、std::shared_ptr的用法
2.1 创建 std::shared_ptr
(1)使用 std::make_shared
推荐使用 std::make_shared
来创建 std::shared_ptr
,因为它更高效且安全。
#include <iostream>
#include<memory>
using namespace std;int main()
{// 创建一个shared_ptrshared_ptr<int> ptr = make_shared<int>(20);cout << "Value = "<< *ptr << endl;
}
std::make_shared
会同时分配对象和控制块的内存,减少了内存分配的次数。
(2)直接构造
也可以通过构造函数直接创建,但需要小心避免悬挂指针。
int* ptr2 = new int(10);shared_ptr<int> ptr3(ptr2);
(3)从其他智能指针转换
std::shared_ptr
可以从 std::unique_ptr
或其他 std::shared_ptr
转换而来。
std::shared_ptr<int> ptr3 = std::make_shared<int>(42);
std::shared_ptr<int> ptr4 = ptr3; // 复制构造,引用计数增加
2.2 引用计数机制
std::shared_ptr
的核心是引用计数。当一个 std::shared_ptr
被复制时,引用计数会增加;当一个 std::shared_ptr
被销毁时,引用计数会减少。当引用计数为零时,它所管理的对象会被自动释放。
std::shared_ptr<int> ptr5 = std::make_shared<int>(100);
{std::shared_ptr<int> ptr6 = ptr5; // 引用计数 +1std::cout << "Value: " << *ptr6 << std::endl;std::cout << "ptr5 Use_Count: " << ptr5.use_count() << std::endl;std::cout << "ptr6 Use_Count: " << ptr6.use_count() << std::endl;} // ptr6 超出作用域,引用计数 -1
std::cout << "Value: " << *ptr5 << std::endl; // 仍然可以访问
std::cout << "Use_Count: " << ptr5.use_count() << std::endl;
代码运行结果:
在C++中通过 std::shared_ptr
的成员函数 use_count()
来获取当前 shared_ptr
的引用计数。这个函数返回一个 std::size_t
类型的值,表示当前有多少个 std::shared_ptr
共享同一个控制块。
2.3 使用自定义删除器
如果需要对对象进行特殊处理(如释放资源或调用特定函数),可以为 std::shared_ptr
提供自定义删除器。
std::shared_ptr<int> ptr7(new int(30), [](int* p) {cout << "Custom Deleter Call" << endl;delete p;});
程序结束,调用客户自定义的删除器。
2.4 使用 std::weak_ptr
避免循环引用
std::shared_ptr
可能会导致循环引用问题,从而无法正确释放资源。通过 std::weak_ptr
可以解决这个问题。
class B;class A
{
public:shared_ptr<B> m_ptrB;~A() { cout << "A 析构" << endl; };
};class B
{
public:shared_ptr<A> m_ptrA;~B() { cout << "B 析构" << endl; };
};int main()
{shared_ptr<A> ptrA = make_shared<A>();shared_ptr<B> ptrB = make_shared<B>();ptrA->m_ptrB = ptrB;ptrB->m_ptrA = ptrA;
}
上述代码运行时不会调用析构函数。
解决办法:将B类中的shared_ptr改为weak_ptr后,程序运行结果:
2.5 其他操作
-
reset()
:释放当前管理的对象,并可选地绑定到新的对象。 -
use_count()
:返回当前对象的引用计数。 -
get()
:返回底层裸指针(不推荐直接使用,仅在必要时)。
std::shared_ptr<int> ptr8 = std::make_shared<int>(200);
std::cout << "Use count: " << ptr8.use_count() << std::endl; // 输出引用计数
ptr8.reset(); // 释放对象
3、注意事项
-
循环引用问题
-
如果两个或多个
std::shared_ptr
相互引用,会导致引用计数永远不会变为0,从而无法释放对象。 -
使用
std::weak_ptr
可以解决循环引用问题。
-
-
不要直接操作底层指针
-
尽量避免使用
get()
获取底层指针,因为这可能导致悬挂指针或内存泄漏。 -
如果需要操作底层指针,建议使用
std::weak_ptr
来确保对象仍然有效。
-
-
性能开销
-
std::shared_ptr
的引用计数操作是线程安全的,但会带来一定的性能开销。 -
如果不需要线程安全,可以考虑使用
std::unique_ptr
。
-
-
优先使用
std::make_shared
std::make_shared
会同时分配对象和控制块的内存,减少了内存分配的次数,性能更好。
4、注意事项
std::shared_ptr
是一种非常强大的智能指针工具,适用于需要共享所有权的场景。它通过引用计数自动管理内存,减少了内存泄漏和悬挂指针的风险。但在使用时需要注意以下几点:
-
尽量避免循环引用,必要时使用
std::weak_ptr
。 -
不要直接操作底层指针,除非绝对必要。
-
优先使用
std::make_shared
创建std::shared_ptr
,因为它更高效。
二、unique_ptr原理
unique_ptr是独占式的智能指针,每一次只会有一个指针指向其给定的对象。当unique_ptr离开其作用域时,其所指的对象会被自动删除,并且该对象拥有的任何资源都会被释放。
std::unique_ptr
是 C++11 引入的一种智能指针,用于管理动态分配的资源(如通过 new
分配的内存)。它的核心原理是通过独占所有权语义来自动管理资源的生命周期,确保资源在合适的时机被释放,从而避免内存泄漏和野指针问题。
以下是 std::unique_ptr
的工作原理和关键特性:
1. 独占所有权
std::unique_ptr
采用独占所有权机制,即同一时间只能有一个 unique_ptr
指向某个资源。这意味着:
-
不能复制:
unique_ptr
不能被复制(即没有拷贝构造函数和拷贝赋值运算符),因为复制会导致多个指针指向同一资源,从而破坏所有权的独占性。 -
可以移动:
unique_ptr
支持移动语义(通过移动构造函数和移动赋值运算符),允许将资源的所有权从一个unique_ptr
转移到另一个unique_ptr
。移动操作会将原指针置为nullptr
,确保资源的唯一所有权。
2. 自动释放资源
std::unique_ptr
在以下情况下会自动释放其管理的资源:
-
析构函数:当
unique_ptr
被销毁(如超出作用域、对象析构)时,它会调用资源的析构函数(如delete
)来释放资源。 -
重置:通过调用
reset()
方法,可以手动释放当前资源,并可选择分配新的资源。
3. 定制删除器
std::unique_ptr
允许用户自定义删除器(deleter),这使得它不仅可以管理动态分配的内存,还可以管理其他类型的资源(如文件句柄、网络连接等)。删除器是一个可调用对象,用于定义资源的释放方式。默认情况下,unique_ptr
使用 delete
作为删除器,但用户可以通过模板参数或构造函数传递自定义删除器。
struct FileDeleter
{void operator()(FILE* file){fclose(file);cout << "Custom Deleter called" << endl;}
};unique_ptr<FILE,FileDeleter> file(fopen("1.txt","r"));
file.reset(); // 调用自定义删除器关闭文件
4. 实现原理
std::unique_ptr
的实现基于模板和智能指针的底层机制:
模板参数:std::unique_ptr
是一个模板类,通常有两个模板参数:
T
:指针指向的类型。
Deleter
:删除器类型(默认为 std::default_delete<T>
)。
内部存储:unique_ptr
内部存储一个裸指针(T*
)和一个删除器对象。它通过操作这个裸指针来管理资源,并在需要时调用删除器释放资源。
移动语义:unique_ptr
的移动构造函数和移动赋值运算符通过交换内部指针和删除器来实现资源的转移,确保所有权的唯一性。
5. 使用示例
以下是一个简单的 std::unique_ptr
使用示例:
#include <iostream>
#include <memory>struct Foo // object to manage
{Foo() { std::cout << "Foo...\n"; }~Foo() { std::cout << "~Foo...\n"; }
};struct D // deleter
{void operator() (Foo* p){std::cout << "Calling delete for Foo object... \n";delete p;}
};int main()
{std::cout << "Creating new Foo...\n";std::unique_ptr<Foo, D> up(new Foo(), D()); // up owns the Foo pointer (deleter D)std::cout << "Replace owned Foo with a new Foo...\n";up.reset(new Foo()); // calls deleter for the old onestd::cout << "Release and delete the owned Foo...\n";up.reset(nullptr);
}
代码运行结果:
6. 优点
自动管理资源:避免手动调用 delete
,减少内存泄漏风险。
独占所有权:确保同一时间只有一个指针管理资源,避免野指针问题。
支持自定义删除器:可以管理除动态内存之外的其他资源。
轻量级:std::unique_ptr
通常只有裸指针大小,性能开销极小。
7.缺点
- 不能复制:由于独占所有权,
unique_ptr
不能被复制,这在某些场景下可能需要额外的逻辑来处理。 - 依赖移动语义:需要 C++11 或更高版本支持,因为其依赖于移动构造函数和移动赋值运算符。
总之,std::unique_ptr
是一种非常强大且轻量级的智能指针,适用于大多数需要管理动态资源的场景。
三、weak_ptr原理
在C++中,weak_ptr
是一种智能指针,用于解决shared_ptr
的循环引用问题。它不拥有对象的所有权,而是观察shared_ptr
管理的对象,避免增加引用计数。以下是weak_ptr
的原理和用法详解:
1. weak_ptr
的核心原理
不增加引用计数:weak_ptr
指向由shared_ptr
管理的对象,但不会增加其引用计数。当最后一个shared_ptr
被销毁时,对象仍会被释放,即使有weak_ptr
存在。
观察者模式:weak_ptr
只是"观察"资源,不控制生命周期。若要访问资源,需临时转换为shared_ptr
(通过lock()
方法)。
解决循环引用:在相互持有shared_ptr
的场景中(如双向链表、父-子对象),使用weak_ptr
打破循环,防止内存泄漏.
1.1 与 std::shared_ptr
的关系
-
std::weak_ptr
是基于std::shared_ptr
的。它不会增加对象的引用计数,但会与std::shared_ptr
共享对象的控制块(control block)。控制块中包含了对象的引用计数和弱引用计数。 -
引用计数(
use_count
):记录有多少个std::shared_ptr
指向对象。当引用计数为 0 时,对象会被销毁。 -
弱引用计数(
weak_count
):记录有多少个std::weak_ptr
指向对象。弱引用计数不会影响对象的生命周期,但当对象被销毁时,所有指向该对象的std::weak_ptr
会失效。
1.2 实现原理
-
当创建一个
std::weak_ptr
时,它会从一个std::shared_ptr
或另一个std::weak_ptr
中获取控制块的指针,并增加弱引用计数。 -
当使用
std::weak_ptr
的lock()
方法时,它会检查控制块中的对象是否仍然存在。如果对象存在,它会返回一个指向该对象的std::shared_ptr
,并增加引用计数;如果对象已经被销毁,则返回一个空的std::shared_ptr
。 -
当一个
std::weak_ptr
被销毁时,它会减少弱引用计数。当弱引用计数和引用计数都为 0 时,控制块也会被销毁。
2. 基本用法
2.1** 创建weak_ptr
**
必须从shared_ptr
或另一个weak_ptr
构造.
从 std::shared_ptr
创建:从 std::shared_ptr
创建:
std::shared_ptr<int> ptr = std::make_shared<int>(10);
std::weak_ptr<int> w_ptr(ptr); // 从shared_ptr中构造
从另一个 std::weak_ptr
创建:
std::weak_ptr<int> w_ptr2(w_ptr);
2.2** 检查对象是否仍然存在**
- 使用
expired()
方法检查对象是否已经被销毁:
if (w_ptr2.expired()){std::cout << "Object has been destroyed." << std::endl; }else{std::cout << "Object still exists." << std::endl;}
2.3获取对象的 std::shared_ptr
- 使用
lock()
方法获取对象的std::shared_ptr
:
std::shared_ptr<int> shared_from_weak = w_ptr2.lock();if (shared_from_weak) {std::cout << "Accessing object: " << *shared_from_weak << std::endl;}else {std::cout << "Object has been destroyed." << std::endl;}
如果对象仍然存在,lock()
方法会返回一个指向该对象的 std::shared_ptr
;如果对象已经被销毁,则返回一个空的 std::shared_ptr
。
3、std::weak_ptr
的使用场景
3.1解决循环引用问题
-
在使用
std::shared_ptr
时,可能会出现循环引用的情况,导致对象无法被正确销毁。例如,两个对象互相持有对方的std::shared_ptr
,它们的引用计数永远不会为 0。 -
通过将其中一个引用改为
std::weak_ptr
,可以打破循环引用。例如:
class B;class A {
public:std::shared_ptr<B> b_ptr;
};class B {
public:std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;return 0;
}
在这个例子中,B
持有 A
的 std::weak_ptr
,避免了循环引用。
3.2观察但不拥有对象
当一个对象需要被多个组件观察,但这些组件不需要拥有对象时,可以使用 std::weak_ptr
。例如,一个观察者模式中,观察者可以持有被观察对象的 std::weak_ptr
,这样即使观察者仍然存在,也不会阻止被观察对象的销毁。
3.3缓存场景
在某些缓存场景中,可以使用 std::weak_ptr
来存储对对象的弱引用。当对象被销毁时,缓存中的 std::weak_ptr
会自动失效,避免了对已销毁对象的访问。
4、注意事项
-
线程安全
std::weak_ptr
的操作(如lock()
、expired()
等)是线程安全的,因为它们都是通过控制块中的原子操作来实现的。
-
生命周期管理
- 使用
std::weak_ptr
时,需要注意对象的生命周期。虽然std::weak_ptr
不会阻止对象的销毁,但在访问对象时,必须确保对象仍然存在。
- 使用
-
性能开销
std::weak_ptr
的使用会带来一定的性能开销,因为它需要维护弱引用计数。在性能敏感的场景中,需要权衡使用std::weak_ptr
的利弊。
总之,std::weak_ptr
是一种非常有用的智能指针,它通过与 std::shared_ptr
共享控制块,提供了对对象的弱引用。它可以用于解决循环引用问题、观察对象以及缓存场景等。在使用时,需要注意对象的生命周期和性能开销。