文章目录
- 前言:
- 1. 智能指针的使用及原理
- 2. C++ 98 标准库中的 auto_ptr:
- 3. C++ 11 中的智能指针
- 循环引用:
- shared_ptr 定制删除器
- 4. 内存泄漏
- 总结:
前言:
随着C++语言的发展,智能指针作为现代C++编程中管理动态分配内存的一种重要工具,越来越受到开发者的青睐。智能指针不仅简化了内存管理,还有助于避免内存泄漏等常见问题。本文将深入探讨智能指针的使用及其原理,从C++98标准库中的auto_ptr
开始,逐步过渡到C++11中更为强大和灵活的智能指针类型,如unique_ptr
和shared_ptr
。此外,文章还将讨论循环引用问题、内存泄漏的原因及其危害,并提供相应的解决方案。通过本文的学习,读者将能够更好地理解和运用智能指针,编写出更安全、更高效的C++代码。
1. 智能指针的使用及原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// SmartPtr.h
// 使用RAII思想设计的smartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr) {
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
ShardPtr<int> sp1(new int);
ShardPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
return 0;
}
//test.cpp
#include <iostream>
#include "SmartPtr.h"
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
- 需要像指针一样的去使用:
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(new int(0));
*sp1 += 10;
SmartPtr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";
sp3->second = 1; // 等价于 sp3.opertor->()->second = 1;
cout << sp3->first << " " << sp3->second << endl;
- 智能指针的拷贝问题
// 智能指针的拷贝问题
int main()
{
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(sp1);
return 0;
}
vector
/ list.
… 需要深拷贝,它们都是利用资源存储数据,资源是自己的。拷贝时,每个对象各自一份资源,各管各的,所以深拷贝。
智能指针 / 迭代器… 期望的是浅拷贝
资源不是自己的,代为持有,方便访问修改数据。他们拷贝的时候期望的指向同一资源,所以浅拷贝。而且智能指针还要负责释放资源。
itertor it = begin();
2. C++ 98 标准库中的 auto_ptr:
auto_ptr
管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
注意:在使用auto_ptr
过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!
// 智能指针的拷贝问题
// 1. auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
// 注意:在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!
int main()
{
std::auto_ptr<int> sp1(new int(1));
std::auto_ptr<int> sp2(sp1);
*sp2 += 10;
// 悬空
*sp1 += 10;
return 0;
}
auto_ptr
的实现:
namespace hd
{
template<class T>
class auto_ptr {
public:
// RAII
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
// ap2(ap1)
auto_ptr(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
~auto_ptr()
{
if (_ptr) {
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
3. C++ 11 中的智能指针
boost 智能指针
scoped_ptr
/ scoped_array
shared_ptr
/ shared_array
C++ 11
unique_ptr
跟scoped_ptr
类似的
shared_ptr
跟shared_ptr
类似的
unique_ptr
:
禁止拷贝,简单粗暴,适合于不需要拷贝的场景
赋值也禁掉了:
unique_ptr
:实现
namespace hd
{
template<class T>
class unique_ptr {
public:
// RAII
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
// ap2(ap1)
unique_ptr(const unique_ptr<T>& ap) = delete; // 禁掉拷贝构造
// 赋值也要禁掉,赋值会生成默认成员函数,浅拷贝,也会出现问题
unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;
~unique_ptr()
{
if (_ptr) {
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
如果必须要拷贝用shared_ptr
:
shared_ptr
允许自由拷贝,使用引用计数解决多次释放的问题
引用计数: 记录有几个对象参与管理这个资源
shared_ptr
实现:
使用静态成员变量实现。
namespace hd
{
template<class T>
class shared_ptr {
public:
// RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
_count = 1;
}
// sp(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
++_count;
}
~shared_ptr()
{
if (--_count == 0)
{
std::cout << "delete:" << _ptr << std::endl;
delete _ptr;
}
}
int use_count()
{
return _count;
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
static int _count;
};
template<class T>
int shared_ptr<T>::_count = 0;
}
中释放了一个资源!
如果使用静态成员属于这个类,属于这个类的所有对象
需求:每个资源配一个引用计数,而不是全部都是一个引用计数!
所以,一个资源配一个引用计数无论多少个对象管理这个资源,只有这一个计数对象!
怎么找到这个引用呢?每个对象存一个指向计数的指针!
namespace hd
{
template<class T>
class shared_ptr {
public:
// RAII
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
void release()
{
// 说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
std::cout << "delete:" << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
}
// 赋值 sp1 = sp3;
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) // 避免自己给自己赋值
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
shared_ptr
的缺陷:
// shared_ptr 的缺陷
struct ListNode
{
int _val;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
,_next(nullptr)
,_prev(nullptr)
{}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode(10));
std::shared_ptr<ListNode> n2(new ListNode(20));
n1->_next = n2;
n2->_prev = n1;
//delete n1;
//delete n2;
return 0;
}
循环引用:
- 左边的节点,是由右边的节点
_prev
管着的,_prev
析构,引用计数减到 0, 左边的节点就是释放 - 右边节点中
_prev
什么时候析构呢?右边的节点被delete
时,_prev
析构。 - 右边节点什么时候
delete
呢?右边的节点被左边的节点的_next
管着的,_next
析构,右边的节点就释放了。 _next
什么时候析构呢?_next
是左边节点的成员,左边节点delete
,_next
就析构了- 左边节点什么时候释放呢?回调 1 点 又循环上去了
右边节点释放 -> _prev
析构 -> 左边节点的释放 -> _next
析构 -> 右边节点释放
所以这是 shared_ptr
特定场景下的缺陷, 只要有两个shared_ptr
互相管理就会出现这样的情况,所以即使用了智能指针,同样可能导致内存的泄漏。
struct ListNode
{
int _val;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
{}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode(10));
std::shared_ptr<ListNode> n2(new ListNode(20));
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
//delete n1;
//delete n2;
return 0;
}
用weak_ptr
可以通过不增加引用计数的方式,避免这个问题。(存在单独自己的 引用计数)
weak_ptr
不支持RAII, 不参与资源管理,不支持指针初始化,但是还是能起到指向你的作用
weak_ptr
的实现:
namespace hd
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get(); // 用 get方法调原生指针
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
shared_ptr 定制删除器
template<class T>
struct DeleteArry
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
// 定制删除器
int main()
{
std::shared_ptr<ListNode> p1(new ListNode(10));
std::shared_ptr<ListNode[]> p2(new ListNode[10]); // 可以用数组的
std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArry<ListNode>()); // 用仿函数的对象去释放!
std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr); }); // 用lamada表达式也是可以的
return 0;
}
定制删除器实现:
namespace hd
{
template<class T>
class shared_ptr
{
public:
// function<void(T*)> _del = [](T* ptr) {delete ptr; };
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
// RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
// sp1 = sp4
// sp4 = sp4;
// sp1 = sp2;
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
void release()
{
// 说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
std::cout << "delete:" << _ptr << std::endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
~shared_ptr()
{
// 析构时,--计数,计数减到0,
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
std::function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}
4. 内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
总结:
本文详细介绍了智能指针的概念、使用和原理,从C++98的auto_ptr
到C++11的unique_ptr
和shared_ptr
,展示了智能指针在现代C++编程中的应用和发展。我们了解到RAII(资源获取即初始化)的设计模式,它通过将资源管理封装在对象的生命周期中,简化了资源的获取和释放过程。文章还讨论了智能指针的拷贝问题,特别是auto_ptr
的缺陷和shared_ptr
的循环引用问题,以及如何使用weak_ptr
和定制删除器来解决这些问题。
此外,文章还探讨了内存泄漏的概念、原因和危害,以及如何在实际编程中避免这些问题。通过具体的例子和代码,我们学习了如何使用智能指针来管理资源,确保资源在使用完毕后能够被正确释放,从而避免内存泄漏和其他潜在的资源管理问题。
总的来说,智能指针是C++中一个强大的特性,它不仅提高了代码的安全性和效率,还使得资源管理变得更加简单和直观。通过本文的学习,读者应该能够更加自信地在C++项目中使用智能指针,编写出更加健壮和可靠的软件。