C++11:智能指针
目录
1.为什么需要智能指针
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");//标准库中定义的一个异常类
return a / b;
}
void func()
{
//1、如果p1这里new 抛异常会如何?
//2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
func();
}
catch (const std::exception&e)//异常可以被基类的引用和指针捕获
{
cout << e.what() << endl;//what() 是 std::exception 类的一个公有虚成员函数,用于获取异常的描述信息
}
return 0;
}
问题分析:上面的问题分析出来我们发现有什么问题?
如果p1抛异常无影响,p2抛异常就无法释放p1,div抛异常无法释放p1,p2,因此catch异常时要分很多种情况。如果申请的空间更多,那情况会更复杂。
2.内存泄漏
2.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
2.2 内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak):堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
3.智能指针的使用及原理
3.1 RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
智能指针时 RAII 思想的一种实现,例如使用RAII思想设计的SmartPtr类
template <typename T>
class SmartPtr {
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{
}
~SmartPtr()
{
delete[] _ptr;
cout << "delete[] " << _ptr << endl;
}
private:
T* _ptr;
};
void Func()
{
SmartPtr<int> sp1(new int[10]);//抛异常和不抛这里都可以通过析构释放空间,
//可以防止发生异常后,后续代码不执行
SmartPtr<char> sp2(new char[10]);
cout << div() << endl;
}
通过构造函数和析构函数我们可以实现资源自动的释放,然而我们也需要 operator* 和 operator->来实现指针的功能:
T& operator*()//解引用
{
return *_ptr;
}
T* operator->()//解引用
{
return _ptr; //(* ). .运算符前是对象, ->运算符前是指针,返回对象成员变量
}
3.2 引用计数实现指针拷贝
我们需要多个指针共同管理一块空间,我们需要进行指针拷贝。
C++98版本的库中就提供了 auto_ptr 的智能指针。auto_ptr的实现原理:管理权转移的思想,只能有一个指针指向那片空间,一旦拷贝就转移值,原指针制空NULL,这种方法是不方便的。
3.2.1 unique_ptr
unique_ptr 实现原理:简单粗暴的使用 delete 防拷贝,让编译器生成函数的删除声明,明确禁止对该函数的调用,对于不需要拷贝的情况比较好。下面简化模拟实现了一份来了解它的原理
namespace xlh{
template <typename T>
class unique_ptr {
public:
//c++98
// 1.只声明不实现 但类外可以实现,不能禁止
//unique_ptr(const unique_ptr<T> ptr);
//unique_ptr<T>& operator=(const unique_ptr<T> ptr);
//2.限定为私有
//c++11
//1.delete,不让调用
unique_ptr(const unique_ptr<T>& ptr) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& ptr) = delete;
private:
T* _ptr;
};
}
但是 unique_ptr 所有权可转移(通过移动语义):禁止拷贝,但支持通过 std::move 来转移所有权。转移后,原来的 unique_ptr 就会变为空。
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
// std::unique_ptr<int> ptr2 = ptr1; // 错误!不能拷贝
std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确!所有权转移给 ptr2
// 此时 ptr1 变为空
| 成员函数 | 作用 | 关键注意事项 |
|---|---|---|
get() | 获取原始指针。 | 仅用于“观察”或传给不接受智能指针的旧 C 接口。不要用返回的指针去 delete,否则 unique_ptr 析构时会二次释放导致程序崩溃。 |
release() | 放弃所有权,返回原始指针。 | 调用后,unique_ptr 置空,但不会释放对象。你需要手动接手并负责 delete,否则会泄漏。通常用来将所有权转交给 C 风格接口。 |
reset() | 重置。销毁当前拥有的对象(若有),并可选择接管一个新指针。 | 最安全的释放方式。例如 ptr.reset() 清空并释放;ptr.reset(newObj) 释放旧对象,接管新对象。 |
swap() | 交换两个 unique_ptr 所拥有的对象。 | 用于高效交换所有权。 |
operator* 和 operator-> | 解引用,访问所拥有的对象。 | 和原始指针用法完全一样,如 ptr->method() 或 *ptr = 10;。 |
operator bool | 隐式转换为 bool。 | 用于判断是否为空,如 if (ptr) { ... }。 |
3.2.2 shared_ptr
另外 C++11 中也开始提供更靠谱的并且支持拷贝的 shared_ptr,是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。大致实现方式是引用计数要跟着资源走,所以我们需要下面的方法简单实现一下。
为什么不能直接使用静态成员变量呢,因为当多个同一类型对象管理不同空间时,会出现问题。
namespace xlh{
//可以拷贝的智能指针
template <typename T>
class shared_ptr {
public:
//RAII
shared_ptr(T* ptr = NULL)
:_ptr(ptr),
_pcount(new int(1))
{
}
~shared_ptr()
{
(*_pcount)--;
if (*_pcount == 0)
{
delete _ptr; delete _pcount;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->() //(* ). .运算符前是对象, ->运算符前是指针,返回对象成员变量
{
return _ptr;
}
//c++11
//2.引用计数
shared_ptr(const shared_ptr<T>& sp)//拷贝构造
:_ptr(sp._ptr),
_pcount(sp._pcount)
{
(*_pcount)++;
}
//1.自己给自己赋值 sp1 = sp1
//2.wdz::shared_ptr<int> sp3(sp1); sp3 = sp1 也算自己给自己赋值所以不用 this != &sp 判断
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止自己给自己赋值
if (_pcount != sp._pcount)
{
if (--(*_pcount) == 0)//引用计数--
{//释放原内存
delete _ptr; delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
//if ((*_pcount)--)//引用计数--
//{
// if (_pcount == 0)//释放原内存
// {
// delete _ptr;delete _pcount;
// }
//}
//_ptr = sp._ptr;
//_pcount = sp._pcount;
//(*_pcount)++;
return *this;
}
T* _ptr;
int* _pcount;
};
}
shared_ptr中比较需要注意的是 opeator= 赋值运算符重载,会面临两种情况
- 自己给自己赋值 sp1 = sp1
- 使用wdz::shared_ptr<int> sp3(sp1)构造,然后 sp3 = sp1 ,虽然是不同对象,但也算自己给自己赋值,所以不用 this != &sp 判断,我们可以用 _ptr 判断,或者 _pcount 判断。我这里是使用的_ptr。

3.2.3 循环引用使用weak_ptr
当然 shared_ptr 也有线程安全的问题,我们后面文章讲解。
循环引用会导致内存泄漏,那么什么是循环引用呢?我们执行下面代码会出现问题:
struct ListNode
{
int val;
wdz::shared_ptr<ListNode> prev;
wdz::shared_ptr<ListNode> next;
};
void Test_shared_ptr3()
{
wdz::shared_ptr<ListNode> n1(new ListNode);
wdz::shared_ptr<ListNode> n2(new ListNode);
//循环引用
n1->next = n2;
n2->prev = n1;
}

这是所使用的资源分布图那么什么时候会出问题呢,当进行资源释放时会出现问题:
- 执行完 Test_shared_ptr3() 后会释放 n2,因为还有 ListNode1 指向 ListNode2,所以可以释放n2。
- 释放n2后,指向 ListNode2 的指针 ListNode1 的 next,ListNode1.next._pcount = 1。然后释放n1。
- 释放n1后,指向 ListNode1 的指针只有ListNode2 的 prev,ListNode2.prev._pcount = 1。
- 节点 A 和节点 B 的引用计数都为 1,但它们互相指着对方,导致两个节点都没有析构,只是释放了 n1 n2 ,最终内存泄漏。
注意:
循环引用既不会“停下来互相等待”(像死锁那样卡住不动),也不会“不停地循环运行”(像死循环那样占用CPU)。实际情况是:释放流程直接结束(栈展开完毕),程序继续运行,但两个对象的内存永远留在堆上没有被释放——即静默的内存泄漏。
c++11给出了weak_ptr:
struct ListNode
{
int val;
std::weak_ptr<ListNode> prev1;//weak_ptr 使用时 不增加 引用计数
std::weak_ptr<ListNode> next1;
};
void Test_shared_ptr3()
{
std::shared_ptr<ListNode> n3(new ListNode);
std::shared_ptr<ListNode> n4(new ListNode);
cout << n3.use_count() << endl;
cout<< n4.use_count() << endl;
n4->prev1 = n3;
n3->next1 = n4;
}
这样使用就不会出现问题,原因是 weak_ptr 指向 shared_ptr 指向的空间时不会增加shared_ptr 的引用计数,节点的引用计数就是 shared_ptr 的 _pcount。
下面时weak_ptr 的简单实现:
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{ }
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{ }
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get(); //不参与资源管理
return *this;
}
//不需要释放任何自定义类型资源
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->() //(* ). .运算符前是对象, ->运算符前是指针,返回对象成员变量
{
return _ptr;
}
private:
T* _ptr;
};
weak_ptr 不支持RALL,可以向指针一样引用,不参与shared_ptr的引用计数,但其实weak_ptr本身也有引用计数,如下图所示,当shard_ptr 提前释放资源,weak_ptr 不会成为野指针,可以检测 expired 状态,直到 weak_ptr 也变为 0。

3.3 实现 new[]
c++11 参考了 boost 库,库中提供了scope_ptr ,scope_array,shared_array,shared_ptr,scope_ptr:
| 智能指针 | 来源库 | 所有权 | 拷贝/移动 | 主要用途 | 现代C++替代方案 |
|---|---|---|---|---|---|
boost::scoped_ptr | Boost | 独占 | 不可拷贝,不可移动 | 管理单个动态对象 | std::unique_ptr<T> |
boost::scoped_array | Boost | 独占 | 不可拷贝,不可移动 | 管理动态数组 (new[]) | std::unique_ptr<T[]> |
std::unique_ptr | C++11 | 独占 | 不可拷贝,支持移动 | 管理单个对象或数组 | (自身就是标准方案) |
boost::shared_ptr | Boost | 共享 (引用计数) | 可拷贝 | 管理单个动态对象 | std::shared_ptr<T> |
boost::shared_array | Boost | 共享 (引用计数) | 可拷贝 | 管理动态数组 (new[]) | std::shared_ptr<T[]> (C++17起) |
std::shared_ptr | C++11 | 共享 (引用计数) | 可拷贝 | 管理单个对象 (C++11) 管理数组 (C++17起) | (自身就是标准方案) |
如果我们想一次申请多个空间,boost 使用的是shared_array,而之前我们自己简单实现的 shared_ptr 都是管理一个指针的,可以使用 delete 正确释放,但如果使用 new[ ] 是会崩溃的,std标准库中没有提供 shared_array,它是定制删除器解决的,传入一个指针帮我们管理,D 就是删除器,即传入一个可以调用的对象。
![]()
这是标准库的使用方法:可以释放指针,关闭文件
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
void Test_shared_ptr5()
{
//仿函数
std::shared_ptr<ListNode1> sp1(new ListNode1[10],DelArray<ListNode1>());
//lambda表达式
std::shared_ptr<ListNode1> sp2(new ListNode1[10], [](ListNode1* ptr) {delete[] ptr; });
//这里防止文件打开时,中间抛异常,没有被正常关闭
std::shared_ptr<FILE> sp3(fopen("Test.cpp","r"), [](FILE* ptr) {fclose(ptr); });
}
我们来模拟实现一下,首先这个类是不能变的,只有一个模板参数。但在构造函数增加了一个模板参数。

库中构造函数内部直接用了两个模板参数:
tempalte<class U, class D>
shared_ptr(U* ptr, D del);
我们这里简单内部用一个:

上面这种方式是不可以的,因为 release 中没有办法访问到 D , 但可以通过包装器解决:
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };//传入指针调用,参数是T*
我们还需要增加一个构造函数
template<class D>
shared_ptr(T* ptr,D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)//包装器,传入一个函数对象,调用时会调用这个函数对象
{
}
以下是一种 shared_ptr 的简单实现,通过对成员变量 _del 的初始化,可以同时实现带 [ ] 与 不带 [ ] 的释放:
namespace xlh
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{ }
template<class D>
shared_ptr(T* ptr,D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)//包装器,传入一个函数对象,调用时会调用这个函数对象
{
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount) {
(*_pcount)++;
}
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()//返回的是成员变量
{
return _ptr;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (sp._pcount != _pcount)
{
release();
/*if (--(*_pcount) == 0)
{
delete _ptr; delete _pcount;
}*/
_ptr = sp._ptr;
_pcount = sp._pcount;
*_pcount++;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };//传入指针调用,参数是T*
};
下面代码可以执行,为了实现最后一行代码,所以封装器成员需要一个默认值。
void Test2()
{
xlh::shared_ptr<ListNode> p1(new ListNode[10], DeleteFile<ListNode>());
xlh::shared_ptr<ListNode> p2(new ListNode[10], [](ListNode* ptr) {delete[] ptr; });
xlh::shared_ptr<FILE> p3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
xlh::shared_ptr<ListNode> p4(new ListNode);//这里没有传入删除器,
//所以会调用默认的删除器,默认的删除器会调用delete来释放内存,而不是delete[]
本篇结束!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)