题外话:我们总是想做甩手掌柜的,让员工们能聪明的解决问题,而不出错!
引言:我们总是有这样的疑问?为什么有普通的指针还要有智能指针呢?那肯定是普通指针容易出现问题,比如
- 内存泄漏:在传统的手动内存管理中,如果忘记调用
delete
释放内存,会导致内存泄漏。智能指针通过RAII原则自动释放内存,避免了这种情况。
void useResource() {
Resource* res = new Resource();
res->doSomething();
// 如果忘记调用 delete res; 会导致内存泄漏
}
- 悬挂指针:悬挂指针是指向已释放内存的指针,使用它们会导致未定义行为。智能指针确保指针在对象销毁后变为无效,避免悬挂指针。
void useResource() {
Resource* res = new Resource();
delete res; // 释放内存
res->doSomething(); // 悬挂指针,未定义行为
}
- 双重释放:手动管理内存时,如果多次调用
delete
释放同一块内存,会导致程序崩溃。智能指针确保每块内存只被释放一次。
void useResource() {
Resource* res = new Resource();
delete res;
delete res; // 双重释放,程序崩溃
}
就是为了避免上面的错误,我们可以更放心的使用,所有我们来看看如何让指针能更聪明些,更智能些,让它自己管理这些问题。
请看下面简化版 std::unique_ptr
实现
template<typename T>
class unique_ptr {
private:
T* ptr;
public:
// 构造函数
explicit unique_ptr(T* p = nullptr) : ptr(p) {}
// 析构函数
~unique_ptr() {
delete ptr;
}
// 禁用拷贝构造函数和拷贝赋值运算符
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造函数
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 重载 * 和 -> 运算符
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
// 获取原始指针
T* get() const { return ptr; }
// 释放所有权
T* release() {
T* oldPtr = ptr;
ptr = nullptr;
return oldPtr;
}
// 重新设置指针
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
};
让我们来逐行解释代码:
1. 模板定义
template<typename T>
class unique_ptr {
template<typename T>
:定义一个模板类,T
是模板参数,表示指针所指向的对象类型。class unique_ptr
:定义一个名为unique_ptr
的类。
2. 私有成员变量
private:
T* ptr;
private:
:定义私有访问权限,以下成员只有类的内部可以访问。T* ptr
:指向类型T
的指针,用于存储动态分配的对象。
3. 公有成员函数
public:
public:
:定义公有访问权限,以下成员可以被类的外部访问。
4. 构造函数
explicit unique_ptr(T* p = nullptr) : ptr(p) {}
explicit unique_ptr(T* p = nullptr)
:构造函数,接受一个指向类型T
的指针p
,默认为nullptr
。: ptr(p)
:初始化成员变量ptr
为p
。
目的和作用:
- 初始化
unique_ptr
对象,管理传入的动态内存。
避免的错误:
- 避免未初始化指针导致的未定义行为。
5. 析构函数
~unique_ptr() {
delete ptr;
}
~unique_ptr()
:析构函数,当unique_ptr
对象被销毁时调用。delete ptr
:释放ptr
指向的动态内存。
目的和作用:
- 自动释放动态内存,防止内存泄漏。
避免的错误:
- 避免内存泄漏。
6. 禁用拷贝构造函数和拷贝赋值运算符
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(const unique_ptr&) = delete
:禁用拷贝构造函数,防止拷贝unique_ptr
对象。unique_ptr& operator=(const unique_ptr&) = delete
:禁用拷贝赋值运算符,防止赋值unique_ptr
对象。
目的和作用:
- 确保
unique_ptr
对象的独占所有权,防止多个指针管理同一块内存。
避免的错误:
- 避免双重释放和悬挂指针。
7. 移动构造函数
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
unique_ptr(unique_ptr&& other) noexcept
:移动构造函数,接受一个右值引用other
。: ptr(other.ptr)
:初始化成员变量ptr
为other.ptr
。other.ptr = nullptr
:将other.ptr
置为nullptr
,转移所有权。
目的和作用:
- 实现移动语义,转移资源所有权,避免不必要的拷贝。
避免的错误:
- 避免双重释放和悬挂指针。
8. 移动赋值运算符
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
unique_ptr& operator=(unique_ptr&& other) noexcept
:移动赋值运算符,接受一个右值引用other
。if (this != &other)
:检查自赋值。delete ptr
:释放当前对象的内存。ptr = other.ptr
:转移所有权。other.ptr = nullptr
:将other.ptr
置为nullptr
,避免悬挂指针。return *this
:返回当前对象的引用。
目的和作用:
- 实现移动赋值语义,转移资源所有权,避免不必要的拷贝。
避免的错误:
- 避免双重释放和悬挂指针。
9. 重载 *
和 ->
运算符
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T& operator*() const
:重载解引用运算符*
,返回ptr
指向对象的引用。T* operator->() const
:重载成员访问运算符->
,返回ptr
。
目的和作用:
- 提供与原始指针相同的访问方式,使
unique_ptr
使用起来像原始指针。
避免的错误:
- 提供安全的指针操作,避免直接操作原始指针的风险。
10. 获取原始指针
T* get() const { return ptr; }
T* get() const
:返回ptr
,获取原始指针。
目的和作用:
- 提供获取原始指针的接口。
避免的错误:
- 提供安全的指针访问方式。
11. 释放所有权
T* release() {
T* oldPtr = ptr;
ptr = nullptr;
return oldPtr;
}
T* release()
:释放所有权,返回ptr
并将其置为nullptr
。T* oldPtr = ptr
:保存当前指针。ptr = nullptr
:将ptr
置为nullptr
,释放所有权。return oldPtr
:返回原始指针。
目的和作用:
- 提供释放所有权的接口,使得
unique_ptr
不再管理对象。
避免的错误:
- 避免悬挂指针。
12. 重新设置指针
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
void reset(T* p = nullptr)
:重新设置指针,接受一个新的指针p
,默认为nullptr
。delete ptr
:释放当前指针指向的内存。ptr = p
:将ptr
设置为新的指针p
。
目的和作用:
- 提供重新设置指针的接口,管理新的动态内存。
避免的错误:
- 避免内存泄漏和悬挂指针。
总结
通过以上逐行解释,可以看到简化版 std::unique_ptr
是如何实现其功能和作用的。它通过自动内存管理、禁用拷贝构造和拷贝赋值、实现移动语义等机制,避免了常见的内存管理错误,如内存泄漏、悬挂指针和双重释放。智能指针的这些特性显著提高了C++程序的健壮性和可维护性。