智能指针
定义
为什么需要智能指针
在C++中,动态分配内存是一项常见的任务,但手动管理分配和释放内存可能会导致很多问题,如内存泄漏、悬垂指针以及多次释放同一块内存等。为了避免这些问题,引入了智能指针的概念,它们提供了自动化的内存管理。
以下是智能指针可以解决的一些问题:
智能指针的使用 及 原理
智能指针的原理
即 RAII
↓
RAII
RAII(Resource Acquisition Is Initialization)是一种编程技术和设计原则,它通过将资源的获取与对象的初始化绑定在一起来管理资源。在使用 RAII 时,资源的获取和释放操作被封装在对象的构造函数和析构函数中,利用了对象的生命周期管理资源的自动分配和释放。
该技术的 基本思想:
- 对象的构造函数负责获取资源(如内存、文件句柄、数据库连接等)
- 析构函数负责释放这些资源。
- 通过使用 RAII,可以确保在对象离开作用域时,无论是正常退出还是异常退出,资源都会被正确释放,从而避免了资源泄漏的问题。
下面利用 RAII 思想
实现 一个SmartPtr
的代码:
// 利用RAII思想实现的SmartPtr类
template<class T>
class SmartPtr
{
public:
// 构造函数 - 获取资源
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
// 析构函数 - 释放资源
~SmartPtr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
}
}
// 实现自定义指针类需要的函数
// 重载 operator* 函数用于实现指针解引用操作,允许通过对象的指针访问该指针所指向的对象。返回类型为 T&,表示对指向 T 类型对象的引用。
// 重载 operator-> 函数用于实现指针的箭头操作,允许通过对象的指针直接调用该指针所指向对象的成员函数或成员变量。返回类型为 T* ,表示指向 T 类型对象的指针。
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
上述代码实现了简要的智能指针的功能,智能指针的原理即:
- RAII特性
- 重载
operator*
和opertaor->
, 具有和指针一样的行为
智能指针的种类
auto_ptr
auto_ptr
是C++98标准中提供的智能指针,它具有独占性质,意味着同一时间只能有一个auto_ptr
拥有对特定对象的所有权。
当一个auto_ptr被赋值给另一个auto_ptr时,所有权会被转移,原来的auto_ptr将不再拥有该对象的所有权 (所有权转移) 。这种特性可以用于简单的资源管理,但也容易导致潜在的问题。
由于 auto_ptr
的所有权转移特性,在某些情况下可能会导致意外的行为。
例如:
- 如果将auto_ptr存储在标准容器中,容器的复制或赋值操作会导致对象所有权的转移,从而使得容器内的指针失效。
此外:
- auto_ptr在异常处理机制方面也存在问题,如果在析构过程中抛出异常,可能会导致资源泄漏。
代码分析
下面对 auto_ptr
的模拟实现,展示了 auto_ptr
的性质
namespace aiyimu
{
// C++98: auto_ptr 有一定缺陷
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr=nullptr) // 构造
:_ptr(ptr)
{}
~auto_ptr() // 析构
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
// 指针特性
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 拷贝构造
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
// 所有权 / 管理权 转移
ap._ptr = nullptr;
}
// 赋值重载
auto_ptr operator=(auto_ptr<T>& ap)
{
// 给自己赋值不执行操作
if (this != &ap)
{
if (_ptr) //如果_ptr指向了对象,则删除其指向
{
cout << "Delete:" << _ptr <<endl;
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
}
通过下面对 std::auto_ptr
的调用可以看出其缺点
void Test_auto_ptr()
{
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1); //此时所有权转移
// 此时的 sp1 悬空
*sp2 = 20;
cout << *sp2 << endl;
cout << *sp1 << endl;
}
该函数在调用执行后会报错,原因如下:
sp2
通过拷贝构造函数从sp1
获取了所有权。这导致原来的sp1变为悬空指针,指向的内存区域不再有效。- 在对悬空指针
sp1
进行解引用操作时,会导致未定义行为。因此,打印*sp1的语句会产生不可预测的结果。 - 由于
std::auto_ptr
的问题,该代码没有正确处理资源所有权的转移和管理。
综上所述,建议使用C++11标准中提供的智能指针类型,如std::unique_ptr、std::shared_ptr或std::weak_ptr,以避免这些问题,并更好地管理资源所有权和避免悬空指针的情况。
unique_ptr
unique_ptr
的性质:
- 拥有性:
unique_ptr
是一个独占所有权的智能指针,它禁止两个unique_ptr
对象指向同一个对象。 - 所有权转移:与
auto_ptr
一样,unique_ptr
支持所有权的转移。通过移动语义,可以将一个 unique_ptr 的所有权从一个对象转移到另一个对象,从而避免了资源的复制和多次删除。 - 自动释放:当
unique_ptr
被销毁或者重新赋值时,它会自动删除所拥有的资源,避免了内存泄漏。 - 零开销:
unique_ptr
本身非常轻量,不引入额外的开销,且常被优化为和裸指针一样的大小和性能。
代码分析原理
通过下面的模拟实现 理解其 原理:
namespace
{
// unique_ptr 是一个独占所有权的智能指针,它禁止两个 unique_ptr 对象指向同一个对象。
// 当尝试使用拷贝构造函数或赋值重载运算符来创建或赋值 unique_ptr 对象时,编译器会报错。
template<class T>
class unique_ptr
{
public:
// 使用 delete 关键字 禁用其拷贝和赋值
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
// 构造
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 析构
~unique_ptr()
{
if (_ptr)
{
cout << "Delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
};
下面是一段 使用代码:
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
void test_unique_ptr()
{
aiyimu::unique_ptr<A> up1(new A);
// 这样的拷贝行为违反了 unique_ptr 的意图,unique_ptr 应该是独占资源的智能指针
// aiyimu::unique_ptr<A> up2(up1);
up1->_a1++;
up1->_a2++;
cout << "up1->_a1: " << up1->_a1 << endl;
cout << "up1->_a2: " << up1->_a2 << endl;
// 输出结果: 1 1
}
总结:unique_ptr 是一种独占所有权的智能指针,不允许直接进行拷贝行为。如果需要共享资源,可以使用 shared_ptr
来实现。
shared_ptr
性质
- 共享性:shared_ptr 可以与其他 shared_ptr 共享所管理的资源。通过内部的引用计数机制,shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 拷贝语义:shared_ptr 支持拷贝构造函数和拷贝赋值运算符。当一个 shared_ptr 被拷贝给另一个 shared_ptr 时,它们将共享同一个资源,引用计数会增加。
- 自动释放:当最后一个引用计数为零时,即没有任何 shared_ptr 实例指向某个资源时,shared_ptr 自动释放资源。这可以避免了资源泄漏。
- 循环引用处理:shared_ptr 使用弱引用计数(weak_ptr)来解决循环引用问题。循环引用是指两个或多个对象相互持有对方的 shared_ptr 实例,导致引用计数无法归零。通过将其中一个 shared_ptr 转换为 weak_ptr,可以打破循环引用,使资源正确释放。
- 定制删除器:与 unique_ptr 类似,shared_ptr 也 支持定制的删除器 ,以实现对不同类型资源的特殊释放操作。
其中的重点在于引用计数:
- 对象被销毁时(析构函数调用),资源不再使用,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果引用计数不是0,就说明仍有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
代码分析
namespace aiyimu
{
// shared_ptr 是 C++ 中一种共享所有权的智能指针。
// 与 unique_ptr 只能由一个对象拥有所有权不同,shared_ptr 允许多个 shared_ptr 对象同时管理同一个对象
template<class T>
class shared_ptr
{
public:
// 构造
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pCount(new int(1))
{}
// 析构
~shared_ptr()
{
Release();
}
// 拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount)
{
++(*_pCount); // 计数++
}
// 赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 自己赋值自己,不做操作
if (_ptr == sp._ptr)
{
return *this;
}
Release();
// 共享新资源,计数++
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
//返回*this
return *this;
}
T& operator*()
{
assert(_ptr != nullptr); // 断言指针非空
return *_ptr;
}
T* operator->()
{
assert(_ptr != nullptr); // 断言指针非空
return _ptr;
}
// 返回计数个数
int use_count()
{
return *_pCount;
}
// 获取指针
T* get() const
{
return _ptr;
}
// Release 函数,用于释放资源并销毁 shared_ptr 对象
void Release() {
if (--(*_pCount) == 0)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
private:
T* _ptr;
int* _pCount; // 引用计数
};
};
下面的代码将展示 shared_ptr 的使用 和 性质验证:
void test_shared_ptr1()
{
aiyimu::shared_ptr<A> sp1(new A);
aiyimu::shared_ptr<A> sp2(sp1);
aiyimu::shared_ptr<A> sp3(sp1);
sp1->_a1++;
sp1->_a2++;
cout << "sp2->_a1 : _a2 ->" << sp2->_a1 << ":" << sp2->_a2 << endl; // 1 1
// shared_ptr 使sp1 sp2 共享一块内存,即两者的_a1,_a2的值是同步的
sp2->_a1++;
sp2->_a2++;
cout << "sp1->_a1 : _a2 ->" << sp1->_a1 << ":" << sp1->_a2 << endl; // 2 2
}
下面将介绍 与 shared_ptr 配合使用的 weak_ptr
weak_ptr
weak_ptr 是 C++ 标准库(C++11 及以后版本)中与 shared_ptr 配合使用的智能指针类, 用于解决 shared_ptr
的循环引用问题 。
特性
- 弱引用:
weak_ptr
是一种弱引用,它可以观测(但不拥有)一个由 shared_ptr 管理的对象。通过shared_ptr
创建weak_ptr
,可以同时存在多个 weak_ptr 实例观测同一个资源。 - 不会增加引用计数:
weak_ptr
不会增加所管理资源的引用计数。即使存在 weak_ptr 对象观测某个资源,资源的引用计数也不会增加,因此不会影响资源的生命周期。 - 检查资源是否有效:可以使用 expired() 函数检查
weak_ptr
所观测的资源是否还存在。如果资源已经被释放(即引用计数为零),expired() 返回 true,否则返回 false。 - 获取共享指针:可以使用
lock()
函数将 weak_ptr 转换为 shared_ptr,得到与之关联的共享指针。如果资源仍然存在,则返回一个有效的 shared_ptr;如果资源已被释放,则返回一个空的 shared_ptr。
代码分析
下面是一个简化版本的 weak_ptr 的模拟实现:
// weak_ptr 是 C++ 中一种弱引用智能指针,用于解决 shared_ptr 的循环引用问题。
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
// 该构造函数接受一个shared_ptr<T>类型的参数sp,将其内部指针通过get()函数获取后赋值给_ptr。
// 可以创建一个weak_ptr对象来观测所传入的shared_ptr所管理的资源。
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
// 该构造函数接受一个weak_ptr<T>类型的参数wp,将其内部指针直接赋值给_ptr。
// 可以创建一个新的weak_ptr对象,其观测的资源与原wp对象相同。
weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 赋值
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr;
};