C++智能指针学习笔记

MirrorYuChen
MirrorYuChen
发布于 2025-01-16 / 8 阅读
0
0

C++智能指针学习笔记

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 用法

  • (1) 构造函数:
接口 描述
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
  • (2) 拷贝和移动操作:
接口 描述
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的独占性
  • (3) 指针操作
接口 描述
T* get() const noexcept 返回指向所拥有对象的指针
T& operator*() const 解引用操作符,返回所拥有对象的引用
T* operator->() const noexcept 箭头操作符,返回所拥有对象的指针
  • (4) 释放资源
接口 描述
T* release() noexcept 释放 std::unique_ptr对资源的所有权,并返回指向资源的指针,此时 std::unique_ptr变为空
void reset() noexcept 释放 std::unique_ptr对资源的所有权,并将其重置为空
void reset(pointer p) noexcept 释放 std::unique_ptr对资源的所有权,并接管指定指针的所有权
  • (5) 自定义删除器
接口 描述
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 用法

  • (1) 构造函数
接口 描述
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
  • (2) 赋值操作
接口 描述
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 被置为空。
  • (3) 成员函数
接口 描述
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 用法

  • (1) 构造
接口 描述
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 所指向的对象。
  • (2) 赋值操作
接口 描述
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 所指向的对象。
  • (3) 成员函数
接口 描述
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.参考资料


评论