C++智能指针学习笔记
1.裸指针
裸指针(raw pointer)在内存管理上存在问题,如,内存泄漏和野指针等。
1.1 内存泄漏
如果代码中忘记释放内存,或复杂程序流程中(如,异常抛出等情况),无法保证 delete/free
正常执行,就会造成内存泄漏。如:
int *ptr = new int(10);
delete ptr; // 忘记delete,就会导致内存泄漏
1.2 野指针问题
野指针是指一个指向一个已经释放或未初始化内存区域的指针。如:
int *ptr = new int(10);
delete ptr;
ptr = nullptr; // ptr未置为nullptr或其它安全状态,调用
// ptr,如*ptr = 20,就会出现野指针问题
2.智能指针
C++中,智能指针是一种特殊对象,它存储着指向动态分配对象的指针,并自动管理对象生命周期。智能指针在释放内存后,会将指针置为 nullptr
或其它安全状态,以防止野指针出现。
2.1 std::unique_ptr
2.1.1 特点
(1) 所有权独享 :std::unique_ptr
是独占所指向对象所有权的智能指针,这意味着在任意时刻,一个对象有且只有一个 unique_ptr
能够指向它:
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::make_unique<int>(20);
ptr1 = ptr2; // 错误,unique_ptr不支持赋值操作
std::unique_ptr<int> ptr3 = ptr2; // 错误,unique_ptr不支持拷贝构造
(2) 支持移动语义 :std::unique_ptr
支持移动构造和移动赋值,将一个对象的所有权转移到另一个对象:
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::make_unique<int>(20);
ptr2 = std::move(ptr1); // 移动赋值
具体过程如下:首先,会析构掉 ptr2
所持有指针,然后,将 ptr2
所持有指针的所有权移交给 ptr1
,最后,ptr2
将不再持有资源,并被置为安全状态。
(3) 自动资源管理 : std::unique_ptr
是C++对裸指针的一个 RAII
封装,在构造时候分配资源,在析构时候释放资源,它能够自动调用 delete
释放掉所指向对象内存:
{
std::unique_ptr<int> ptr = std::make_unique<int>(20);
} // 离开作用域,ptr会自动释放内存
2.1.2 用法
接口
描述
unique_ptr<T>() noexcept
默认构造函数,创建一个空的 std::unique_ptr
unique_ptr<T>(nullptr_t) noexcept
接受 nullptr
参数的构造函数,创建一个空的 std::unique_ptr
unique_ptr<T>(pointer)
接受指针参数的构造函数,创建一个拥有指定指针的 std::unique_ptr
unique_ptr<T, Deleter>(pointer, deleter)
接受指针和自定义删除器参数的构造函数,创建一个拥有指定指针和删除器的 std::unique_ptr
接口
描述
unique_ptr<T>& operator=(nullptr_t) noexcept
将 std::unique_ptr
赋值为 nullptr
unique_ptr<T>& operator=(unique_ptr<T>&& r) noexcept
移动赋值运算符,将右值的资源所有权转移给左值
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete
禁用拷贝赋值运算符,确保 std::unique_ptr
的独占性
unique_ptr(const unique_ptr&) = delete;
禁用拷贝构造函数,确保 std::unique_ptr
的独占性
接口
描述
T* get() const noexcept
返回指向所拥有对象的指针
T& operator*() const
解引用操作符,返回所拥有对象的引用
T* operator->() const noexcept
箭头操作符,返回所拥有对象的指针
接口
描述
T* release() noexcept
释放 std::unique_ptr
对资源的所有权,并返回指向资源的指针,此时 std::unique_ptr
变为空
void reset() noexcept
释放 std::unique_ptr
对资源的所有权,并将其重置为空
void reset(pointer p) noexcept
释放 std::unique_ptr
对资源的所有权,并接管指定指针的所有权
接口
描述
void reset(pointer p, Deleter d)
重置 std::unique_ptr
的指针和删除器
Deleter get_deleter() const noexcept
获取与 std::unique_ptr
关联的删除器
2.2 std::shared_ptr
2.2.1 特点
(1) 资源共享 :多个 std::shared_ptr
指针可以共享同一个对象的所有权,它通过引用计数来管理对象生命周期,当最后一个 shared_ptr
销毁时,它会自动释放掉所指向对象的资源。
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1;
(2) 线程安全的引用计数 :std::shared_ptr
指针的引用计数操作是线程安全的,这使得它可以在多线程环境中安全共享对象:
#include <memory>
#include <thread>
std::shared_ptr<int> shared_data = std::make_shared<int>(10);
void func() {
{
std::shared_ptr<int> local_ptr = shared_data; // 引用计数增加
// 使用 localPtr
} // localPtr 被销毁,引用计数减少
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
}
注意,虽然 std::shared_ptr
的引用计数是线程安全的,但它并不保证对所管理对象的访问也是线程安全的。也就是说,如果多个线程通过 std::shared_ptr
访问和修改同一个对象,仍然需要额外的同步机制(如互斥锁)来保证对象的线程安全。
std::shared_ptr<int> shared_data = std::make_shared<int>(20);
std::mutex mtx;
void func() {
std::lock_guard<std::mutex> lock(mtx);
*shared_data += 10; // 加锁后访问和修改对象
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
}
虽然 std::shared_ptr
的引用计数是线程安全的,但原子操作可能会带来一定的性能开销。在单线程环境中,这种开销通常是可以忽略的,但在高并发的多线程环境中,如果对 std::shared_ptr
的操作非常频繁,可能会对性能产生影响。在这种情况下,可以考虑使用 std::unique_ptr
(如果对象只需要一个所有者)或者其他优化措施来减少性能开销。
2.2.2 用法
接口
描述
std::shared_ptr<T>()
创建一个空的 std::shared_ptr
,不指向任何对象,引用计数为 0。
explicit std::shared_ptr<T>(T* ptr);
从裸指针 ptr
构造一个 std::shared_ptr
,接管 ptr
所指向对象的所有权,引用计数初始化为 1。注意,这个构造函数是 explicit
的,防止隐式类型转换。
std::shared_ptr<T>(const std::shared_ptr<U>& r);
从另一个 std::shared_ptr
对象 r
构造,共享 r
所指向的对象的所有权,引用计数增加 1。这里 U
可以是 T
的派生类。
template <class Y> std::shared_ptr<T>(const std::weak_ptr<Y>& r);
从一个 std::weak_ptr
对象 r
构造,如果 r
所指向的对象仍然存在,则共享所有权,引用计数增加 1;否则,构造一个空的 std::shared_ptr
。
接口
描述
std::shared_ptr<T>& operator=(const std::shared_ptr<U>& r);
将当前 std::shared_ptr
对象赋值为另一个 std::shared_ptr
对象 r
。这会使得当前对象共享 r
所指向对象的所有权,引用计数相应地增加。
std::shared_ptr<T>& operator=(std::shared_ptr<U>&& r);
将当前 std::shared_ptr
对象赋值为另一个 std::shared_ptr
对象 r
的右值引用。这会使得当前对象接管 r
所指向对象的所有权,r
被置为空。
接口
描述
T* get() const;
返回 std::shared_ptr
所指向的对象的裸指针。注意,虽然可以获取裸指针,但通常不建议直接使用它进行 delete
操作,因为 std::shared_ptr
会自动管理内存。
void reset(T* ptr = nullptr);
重置 std::shared_ptr
,使其指向新的对象 ptr
(默认为 nullptr
)。如果 ptr
不为 nullptr
,则 std::shared_ptr
会接管 ptr
所指向对象的所有权。同时,原来所指向的对象的引用计数会减少,如果减到 0,则释放原对象。
void swap(std::shared_ptr<T>& other);
交换当前 std::shared_ptr
对象和另一个 std::shared_ptr
对象 other
所指向的对象。
long use_count() const;
返回当前 std::shared_ptr
所指向对象的引用计数。这个函数可以用来检查对象是否仍然被其他 std::shared_ptr
共享。
bool unique() const;
返回一个布尔值,表示当前 std::shared_ptr
是否是唯一拥有其指向对象的 std::shared_ptr
。如果 use_count()
返回 1,则 unique()
返回 true
。
2.3 std::weak_ptr
2.3.1 特点
(1) 观察但不拥有所有权 :std::weak_ptr
用于观察一个由 std::shared_ptr
管理的对象,但不拥有该对象所有权。也就是说,它允许你访问对象,但不会影响对象的引用计数:
std::shared_ptr<int> ptr = std::make_shared<int>(20);
std::weak_ptr<int> weak = ptr;
(2) 解决循环引用问题 :在使用 syd::shared_ptr
时,可能会出现循环引用的情况,导致对象无法被正确释放。std::weak_ptr
可以用来打破这种循环引用,如:
#include <memory>
class A;
class B {
public:
std::shared_ptr<B> ptr;
};
class A {
public:
std::weak_ptr<B> ptr;
};
int main(int argc, char *argv[]) {
auto a = std::make_ptr<A>();
auto b = std::make_ptr<B>();
a->ptr = b;
b->ptr = a;
return 0;
}
如果没有 std::weak_ptr
,a和b之间会形成循环引用,导致内存泄漏。
2.3.2 用法
接口
描述
std::weak_ptr<T>();
创建一个空的 std::weak_ptr
,不指向任何对象。
template <class Y> std::weak_ptr<T>(const std::shared_ptr<Y>& r);
从一个 std::shared_ptr
对象 r
构造一个 std::weak_ptr
,观察 r
所指向的对象,但不增加引用计数。
template <class Y> std::weak_ptr<T>(const std::weak_ptr<Y>& r);
从另一个 std::weak_ptr
对象 r
构造,观察 r
所指向的对象。
接口
描述
template <class Y> std::weak_ptr<T>& operator=(const std::shared_ptr<Y>& r);
将当前 std::weak_ptr
对象赋值为一个 std::shared_ptr
对象 r
,观察 r
所指向的对象。
template <class Y> std::weak_ptr<T>& operator=(const std::weak_ptr<Y>& r);
将当前 std::weak_ptr
对象赋值为另一个 std::weak_ptr
对象 r
,观察 r
所指向的对象。
接口
描述
std::shared_ptr<T> lock() const;
如果 std::weak_ptr
所观察的对象仍然存在,则返回一个 std::shared_ptr
,共享该对象的所有权;否则,返回一个空的 std::shared_ptr
。这个函数用于在需要时将 std::weak_ptr
转换为 std::shared_ptr
,以便安全地访问对象。
bool expired() const;
返回一个布尔值,表示 std::weak_ptr
所观察的对象是否已经被销毁。如果 expired()
返回 true
,则表示对象已经不存在。
T* get() const;
返回 std::weak_ptr
所观察的对象的裸指针。注意,这个指针可能指向一个已经销毁的对象,因此在使用之前应该先调用 lock()
或 expired()
进行检查。
void swap(std::weak_ptr<T>& other);
交换当前 std::weak_ptr
对象和另一个 std::weak_ptr
对象 other
所观察的对象。
3.参考资料