说在前面:本文主要介绍C++中的RAII理念以及智能指针的使用,并举出了相应的例子进行举例说明。
一. 什么是RAII?
RAII(Resource Acquisition Is Initialization),也就是资源获取即初始化,是一种在C++中管理资源(如内存、文件句柄、网络连接等)的编程技术。智能指针是RAII原则的一个应用示例,用于自动管理动态分配的内存,以防止内存泄漏和其他资源管理错误。
RAII的核心理念是在对象的生命周期中获取和释放资源。资源在对象被创建时获取(初始化),并在对象被销毁时释放。这种方法利用了C++的作用域规则,确保即使在出现错误或异常时,资源也能被正确清理。
RAII的主要优点包括:
- 异常安全:资源在对象析构时自动释放,即使发生异常也能保证资源被释放。
- 内存管理:避免内存泄漏,因为每个资源都与一个对象的生命周期绑定。
- 代码清晰:管理资源的逻辑被封装在对象中,使得资源管理更加直观和一致。
二. 什么是智能指针?
智能指针是一种实现RAII的类,用于自动管理动态分配的内存。C++标准库提供了几种智能指针,主要包括std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。
智能指针的使用减少了直接使用裸指针(raw pointers)时可能出现的内存管理错误,如忘记释放内存或尝试释放已经释放的内存。通过智能指针,RAII确保资源在正确的时间自动释放,从而提高代码的可靠性和清晰度。
三. 什么是shared_ptr?
std::shared_ptr
是 C++ 标准库中的一部分,它是一个智能指针,用于自动管理一个引用计数的对象。当最后一个拥有该对象的 shared_ptr
被销毁或被赋予新的对象时,它指向的对象会被删除。
以下是 std::shared_ptr
的一个基本实现示例(不代表标准库的代码),仅仅是为了举例说明。
template <typename T>
class SharedPtr {
private:
T* ptr; // 指向管理的对象
int* count; // 引用计数
public:
// 构造函数
SharedPtr(T* p = nullptr) : ptr(p), count(new int(1)) {
if (ptr == nullptr) {
*count = 0;
}
}
// 拷贝构造函数
SharedPtr(const SharedPtr<T>& sp) : ptr(sp.ptr), count(sp.count) {
(*count)++;
}
// 赋值操作符
SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
if (this != &sp) {
// 减少原对象的引用计数
if (ptr) {
(*count)--;
if (*count == 0) {
delete ptr;
delete count;
}
}
// 赋值新对象
ptr = sp.ptr;
count = sp.count;
(*count)++;
}
return *this;
}
// 析构函数
~SharedPtr() {
(*count)--;
if (*count == 0) {
delete ptr;
delete count;
}
}
// 重载解引用和指针访问操作符
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
// 获取引用计数
int use_count() const { return *count; }
};
需要注意的是这里的引用计数是指针类型,因为引用计数必须在智能指针的所有实例之间共享。当创建智能指针的新副本(如通过拷贝构造函数或赋值操作)时,所有副本都需要访问并修改同一个引用计数。使用指向共享计数的指针是实现这种共享的一种简单且有效的方法。
还有一点需要注意,std::shared_ptr
通过引用计数来管理对象的生命周期。如果两个或多个 shared_ptr
对象相互引用,形成一个闭环,它们的引用计数将永远不会达到零,从而导致内存泄漏。这种情况通常通过使用 std::weak_ptr
来打破循环来解决。接下来我们用代码说明这一点:
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destructed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destructed\n"; }
};
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; // A 和 B 的析构函数永远不会被调用
}
这个例子中,两个类 A
和 B
互相持有对方的 std::shared_ptr
。当在 main
函数中创建 A
和 B
的实例并相互赋值时,形成了一个循环引用。这意味着,即使 main
函数结束,a
和 b
的作用域结束,它们的引用计数也不会降到零,因为它们相互引用。结果是,A
和 B
的析构函数永远不会被调用,导致内存泄漏。如何解决,我们会在weak_ptr中介绍。
四. 什么是unique_ptr?
std::unique_ptr
是 C++ 标准库中的一种智能指针,它提供了对动态分配内存的独占所有权。这意味着在任何时刻,最多只有一个 std::unique_ptr
实例可以指向一个特定的内存资源。当该 std::unique_ptr
实例被销毁或指向另一个资源时,它原先指向的资源也会被自动释放。std::unique_ptr
是一种轻量级的智能指针,相比于 std::shared_ptr
,它具有更少的内存和性能开销。这里我们也给出unique_ptr的代码(仅供参考):
template <typename T>
class UniquePtr {
private:
T* ptr; // 原始指针
public:
// 构造函数
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
// 禁止拷贝构造
UniquePtr(const UniquePtr&) = delete;
// 禁止拷贝赋值
UniquePtr& operator=(const UniquePtr&) = delete;
// 移动构造函数
UniquePtr(UniquePtr&& other) : ptr(nullptr) {
std::swap(ptr, other.ptr);
}
// 移动赋值运算符
UniquePtr& operator=(UniquePtr&& other) {
if (this != &other) {
delete ptr; // 释放当前资源
ptr = nullptr;
std::swap(ptr, other.ptr);
}
return *this;
}
// 解引用操作符
T& operator*() const { return *ptr; }
// 指针访问操作符
T* operator->() const { return ptr; }
// 获取原始指针
T* get() const { return ptr; }
// 释放资源并重置指针
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
// 析构函数
~UniquePtr() {
delete ptr;
}
};
需要注意的是UniquePtr
禁止了拷贝构造和拷贝赋值,以确保对象的独占所有权。通过实现移动语义,它允许所有权的转移。
五. 什么是weak_ptr?
std::weak_ptr
是 C++ 标准库中的一种智能指针,它设计用来观察但不拥有 std::shared_ptr
管理的对象。它与 std::shared_ptr
协同工作,提供了一种访问由 shared_ptr
所拥有的对象的方式,同时不影响该对象的引用计数。这种特性使得 std::weak_ptr
非常适合解决 std::shared_ptr
相互引用(即循环引用)时可能导致的内存泄漏问题。std::weak_ptr
不会增加或减少它所观察的对象的引用计数。这意味着它不会阻止 std::shared_ptr
所管理的对象被销毁。当两个或多个 std::shared_ptr
实例相互引用时,可能导致循环引用,从而引起内存泄漏。std::weak_ptr
可以被用来打破这种循环,因为它不会增加引用计数。
这里给出上面循环引用通过weak_ptr解决后的代码:
#include <iostream>
#include <memory>
class B;
class A {
public:
std::weak_ptr<B> b_ptr; // 改为 weak_ptr
~A() { std::cout << "A destructed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 改为 weak_ptr
~B() { std::cout << "B destructed\n"; }
};
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; // A 和 B 的析构函数将被正确调用
}
在这个修正后的版本中,A
和 B
类使用 std::weak_ptr
相互引用,这样即使它们相互引用,也不会阻止它们的析构函数被调用和资源被释放。当 main
函数结束时,a
和 b
的作用域结束,它们的引用计数降到零,从而允许 A
和 B
被正确销毁。
六. 如何通过weak_ptr访问资源?
要通过 std::weak_ptr
访问资源,你需要先将它转换为 std::shared_ptr
。由于 std::weak_ptr
本身不拥有资源,它仅提供对 std::shared_ptr
所管理对象的非拥有性引用,所以直接访问资源的能力有限。转换为 std::shared_ptr
确保了在访问资源时该资源仍然存在。
这种转换通常使用 std::weak_ptr
的成员函数 lock()
来实现。lock()
方法检查 std::weak_ptr
所关联的 std::shared_ptr
是否仍存在(即是否还有其他 std::shared_ptr
实例指向该资源),如果存在,则返回一个指向该资源的有效 std::shared_ptr
实例;如果不存在(即资源已被释放),则返回一个空的 std::shared_ptr
实例。
下面是一个使用 std::weak_ptr
访问资源的示例:
#include <iostream>
#include <memory>
class MyClass {
public:
void show() { std::cout << "MyClass::show()\n"; }
};
int main() {
std::shared_ptr<MyClass> sp = std::make_shared<MyClass>(); // 创建 shared_ptr
std::weak_ptr<MyClass> wp = sp; // 创建 weak_ptr,观察 sp 指向的对象
// 使用 lock() 方法获取 shared_ptr
std::shared_ptr<MyClass> sp2 = wp.lock();
if (sp2) { // 检查得到的 shared_ptr 是否有效
sp2->show(); // 访问资源
}
sp.reset(); // 释放原始 shared_ptr 指向的对象
sp2 = wp.lock(); // 再次尝试获取 shared_ptr
if (!sp2) {
std::cout << "Resource is no longer available\n";
}
return 0;
}
在这个例子中,我们首先创建了一个指向 MyClass
的 std::shared_ptr
(sp
),然后创建了一个 std::weak_ptr
(wp
),它观察 sp
指向的对象。通过调用 wp.lock()
,我们试图获取一个新的 std::shared_ptr
(sp2
)来访问和操作对象。当原始的 std::shared_ptr
(sp
)被重置或销毁,导致没有 std::shared_ptr
指向对象时,wp.lock()
将返回一个空的 std::shared_ptr
,这表示资源不再可用。这样的机制确保了即使原始的 std::shared_ptr
不存在时,也能安全地检查和访问资源。
七. shared_ptr有哪些创建方式?
1. 使用 std::make_shared
: std::make_shared
是创建 std::shared_ptr
的推荐方式。它在一个单独的动态内存分配中同时构造对象和管理它的控制块(包含引用计数等信息)。
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(args...);
2. 直接构造: 可以直接构造 std::shared_ptr
,但这种方式会导致两次动态内存分配——一次用于对象,另一次用于控制块。
std::shared_ptr<MyClass> ptr(new MyClass(args...));
3. 从 std::unique_ptr
转换: 你可以将一个 std::unique_ptr
转换为 std::shared_ptr
。这会将所有权从 std::unique_ptr
转移到 std::shared_ptr
。
std::unique_ptr<MyClass> uniquePtr(new MyClass(args...));
std::shared_ptr<MyClass> ptr = std::move(uniquePtr);
4. 从另一个 std::shared_ptr
或 std::weak_ptr
复制/赋值: 通过复制或赋值另一个 std::shared_ptr
或 std::weak_ptr
也可以创建 std::shared_ptr
。这会增加与源智能指针关联的对象的引用计数。
std::shared_ptr<MyClass> original = std::make_shared<MyClass>(args...);
std::shared_ptr<MyClass> ptr = original; // 复制构造
std::weak_ptr<MyClass> weakPtr = original;
std::shared_ptr<MyClass> ptrFromWeak = weakPtr.lock(); // 从 weak_ptr 构造
八. 总结
本篇博客我们主要介绍了RAII与智能指针的一些基本概念,并通过理论和代码结合的方式说明了它们的基本用法。