前言:
目录
(一)智能指针的引入
(二)内存泄漏
1、什么是内存泄漏,内存泄漏的危害
2、内存泄漏分类(了解)
3、如何检测内存泄漏(了解)
4、如何避免内存泄漏
(三)智能指针的使用及原理
1、RAII
2、智能指针的原理
3、std::auto_ptr
4、std::unique_ptr
6、weak_ptr
7、删除器
(四)C++11和boost中智能指针的关系
总结
申请的空间(即 new 出来的空间),在使用结束时,需要 delete 掉,否则会形成内存碎片。在程序 运行期间,new 出来的对象,在析构函数中 delete 掉 ,但是这种方法不能解决所有问题,因为有时 候new 发生在某个全局函数里面,该方法会给程序员造成精神负担。 此时,智能指针就派上了用 场。 使用 智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域 时,类会自动调用析构函数,析构函数会自动释放资源。所以,智能指针的作用原理就是在函数结 束时自动释放内存空间,避免了手动释放内存空间。
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 (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
【解释说明】
p1
指针会保持为空指针,因为在异常抛出之前,指针 p1
尚未被分配一个有效的内存地址。
由于 p1
指针没有被分配有效内存,因此在后续代码中删除 p1
是不安全的。
p2
指针同样也不会被分配有效内存,因为在 p1
抛出异常后,程序流会直接跳转到异常处理块,而不会继续执行 p2
的分配。
div()
函数中的异常会被抛出,并且在 main()
函数的异常处理块中被捕获。
new int
分配内存的过程中抛出异常,指针 p1
和 p2
将保持为未初始化状态,分配的内存将无法释放,会导致内存泄漏。
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;
}
【解释说明】
在上述代码中,涉及到了几种内存泄漏和异常安全问题。我简单的分析一哈:
内存申请但忘记释放:
malloc
来分配内存,但是没有相应的 free
来释放内存:int* p1 = (int*)malloc(sizeof(int));
new
来分配内存,但没有相应的 delete
来释放内存:int* p2 = new int;
异常安全问题:
new
来分配一个整数数组,但没有相应的 delete[]
来释放内存:int* p3 = new int[10];
Func()
函数内部发生异常时,delete[] p3
不会被执行,导致在函数返回时出现内存泄漏。异常引发的内存泄漏:
Func()
函数中,如果发生异常,delete p1
和 delete p2
不会被执行,导致 p1
和 p2
分配的内存没有被释放。为了解决这些问题,大家在平时写代码时需要:
free
或 delete
来释放内存,以避免内存泄漏。try
块,以便在异常发生时可以释放已分配的内存。总之,要编写健壮的代码,需要注意资源管理和异常处理,以确保内存泄漏和其他异常问题最小化。
C/C++ 程序中一般我们关心两种方面的内存泄漏:
此时,针对上面我们给出的代码,给出 使用RAII思想设计的SmartPtr类 的解决方法:
// 使用RAII思想设计的SmartPtr类
template
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
cout << "delete:" << _ptr << 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()
{
SmartPtr sp1(new int);
SmartPtr sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
输出展示:
【解释说明】
上诉这样的做法,此时申请的资源不是自己去进行管理而是交给智能指针去管,即用这个智能指针去构造一个智能指针对象;
针对在前面提到的 p1、p2以及div系统调用这三种情况,我们此时在分析一下,看在这样的场景下是怎么样的?
如果sp1抛异常了 :
如果sp2抛异常了 :
如果div()抛异常了 :
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
auto_ptr 文档介绍
auto_ptr 是 C++98 标准中引入的一种智能指针,用于管理动态分配的内存资源。它提供了一种简单的所有权转移机制,允许将资源所有权从一个 auto_ptr 实例转移到另一个实例。
以下是一些关于 auto_ptr 的重要事项:
所有权转移: auto_ptr 允许通过赋值操作将资源所有权从一个实例转移到另一个实例。这意味着,一旦将资源赋值给另一个 auto_ptr ,原始的 auto_ptr 将不再拥有该资源:
int main()
{
auto_ptr ptr1(new int(5));
auto_ptr ptr2;
ptr2 = ptr1; // 所有权转移
//cout << *ptr2 << endl; // 输出 5
cout << *ptr1 << endl; // 错误!ptr1 不再拥有指针,已经转移给了 ptr2
return 0;
}
输出展示:
【解释说明】
ptr1
拥有一个动态分配的 int
类型指针,并将其赋值给了 ptr2;
ptr1
不再拥有指针,而是将所有权转移到了 ptr2
。因此,尝试使用 ptr1
解引用会导致未定义行为。空悬指针问题: auto_ptr 存在空悬指针(dangling pointer)问题,即在资源所有权转移后,原始 auto_ptr 会变为空指针,但资源可能仍然被另一个 auto_ptr 使用,可能导致不可预测的行为。
int main()
{
auto_ptr ptr(new int(5));
if (true)
{
auto_ptr otherPtr = ptr;
//...
}
cout << *ptr << endl; // 输出不确定的值,可能导致程序崩溃
return 0;
}
输出展示:
【解释说明】
ptr
拥有一个动态分配的 int
类型指针。然后,这个指针被转移给了 otherPtr
,在 if
语句块结束后,otherPtr
超出了作用域,释放了指针,并将其设置为 nullptr;
ptr
成为了悬空指针,访问它将导致未定义行为。【小结】
unique_ptr文档
由于 auto_ptr 存在这些问题,它在 C++11 标准中被弃用。在现代 C++ 中,推荐使用 unique_ptr来代替 auto_ptr 。unique_ptr提供了更好的语义和安全性,同时支持移动语义和自定义删除器,使得资源管理更加灵活可靠。
template
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//c++11 思路:语法直接支持
unique_ptr(const unique_ptr& up) = delete;
unique_ptr& operator=(const unique_ptr& up) = delete;
//c++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以可以声明为私有
/*private:
unique_ptr(const unique_ptr& up);*/
private:
T* _ptr;
};
void Test_unique()
{
unique_ptr up1(new int);
unique_ptr up2(up1);
}
输出展示:
当我们使用编译器自带的,看效果咋样:
【解释说明】
代码修改:
【解释说明】
up1
中的资源所有权转移到了 up2
,避免了编译错误。shared_ptr 文档
引入 shared_ptr 是为了解决资源管理中的共享所有权问题。在许多情况下,多个指针需要共同拥有同一资源,而且需要确保资源在最后一个使用它的指针释放后才被销毁。shared_ptr 提供了一个智能指针实现,允许多个指针共享对资源的所有权,同时确保资源的安全释放。
template
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr& operator = (const shared_ptr sp)
{
if (_ptr != sp._ptr) //防止自己给自己赋值
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count()
{
return *_pcount;
}
~shared_ptr()
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};
输出展示:
【解释说明】
shared_ptr
的实现。这是一个模板类,用于管理动态分配的内存,并实现了共享所有权的功能;通过下面的程序我们来测试 shared_ptr 的线程安全问题。需要注意的是 shared_ptr 的线程安全分为两方面:
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
~Date()
{}
};
void SharePtrFunc(zp::shared_ptr& sp, size_t n, mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
zp::shared_ptr copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n
//次,但是最终看到的结果,并一定是加了2n
{
unique_lock lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
}
void test_shared_safe()
{
zp::shared_ptr p(new Date);
cout << p.get() << endl;
const size_t n = 50000;
mutex mtx;
thread t1(SharePtrFunc, ref(p), n, ref(mtx));
thread t2(SharePtrFunc, ref(p), n, ref(mtx));
t1.join();
t2.join();
cout << p.use_count() << endl;
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
}
输出展示:
shared_ptr的循环引用:
struct ListNode
{
int _data;
zp::shared_ptr _prev;
zp::shared_ptr _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test_cycle()
{
zp::shared_ptr node1(new ListNode);
zp::shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
}
输出展示:
【解释说明】
由于存在循环引用的问题,当程序退出 Test_cycle() 时,两个节点的引用计数不会降为0,因此它们的析构函数不会被调用。这意味着析构函数中的输出语句不会被执行,可能导致内存泄漏的风险。
为了避免循环引用问题,可以将 _prev和 _next成员变量为 weak_ptr,它是shared_ptr的一种弱引用,不会增加引用计数。这样在循环链表中,使用 weak_ptr 来打破强引用关系,防止循环引用导致的内存泄漏。
首先,我们先手动实现一个 weak_ptr或者使用库里面提供的weak_ptr。在这里,我手动的实现了一个:
template
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr& sp)
:_ptr(sp.get())
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
改正后的代码如下:
struct ListNode
{
int _data;
zp::weak_ptr _prev;
zp::weak_ptr _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test_cycle()
{
zp::shared_ptr node1(new ListNode);
zp::shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
}
输出展示:
【解释说明】
~ListNode()
会被调用,相关的内存资源会被正确释放,避免了内存泄漏的问题。在上述的shared_ptr 讲述中,我们使用到了有关 weak_ptr。接下来,我们正式的对其进行相关介绍。
基本介绍:
接下来,我们通过代码简单的了解一下:
class B;
class A
{
public:
shared_ptr pb_;
~A()
{
cout << "A delete\n";
}
};
class B
{
public:
shared_ptr pa_;
~B()
{
cout << "B delete\n";
}
};
void fun()
{
shared_ptr pb(new B());
shared_ptr pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl;
cout << pa.use_count() << endl;
}
int main()
{
fun();
return 0;
}
输出展示:
【解释说明】
注意: 我们不能通过weak_ptr直接访问对象的方法,应该先把它转化为shared_ptr!!!
如果不是 new 出来的对象如何通过智能指针管理呢?其实 shared_ptr 设计了一个删除器来解决这 个问题!!
1.定义
int main()
{
int* p = new int(10);
std::shared_ptr sp(
p,
[](int* ptr)
{
std::cout << "deleting pointer " << ptr << std::endl; delete ptr;
});
// 输出共享指针的引用计数
std::cout << "sp use_count: " << sp.use_count() << std::endl;
// 手动将共享指针的引用计数减 1
sp.reset();
return 0;
}
输出展示:
【解释说明】
p
的共享指针,并使用删除器 Lambda 表达式来输出删除对象的地址和释放堆上分配的内存;sp.reset()
调用后,引用计数变为零,并且删除器被调用以释放内存。首先,C++11引入了标准库中的智能指针,包括std::shared_ptr
和std::unique_ptr
。这些智能指针提供了管理资源所有权的机制,能够自动进行内存管理,避免了手动释放资源的麻烦。C++11的智能指针是通过引入新的语言特性和库支持来实现的。
Boost是一个流行的C++扩展库,它提供了大量的高质量、经过广泛测试和使用的 C++代码。在C++11标准引入智能指针之前,Boost已经提供了自己的智能指针库,包括boost::shared_ptr
和boost::scoped_ptr
等。这些智能指针在C++社区中被广泛使用,并受到了高度赞誉。
实际上,C++11标准库中的智能指针是受到Boost智能指针的影响和启发。C++11标准中的std::shared_ptr
和std::unique_ptr
的设计和功能基本上与Boost中的boost::shared_ptr
和boost::scoped_ptr
相当相似。C++11智能指针还引入了一些新的特性和改进,例如移动语义和自定义删除器等,以提供更好的性能和灵活性。
到此,关于智能指针的讲解便全部结束了。接下来,简单的回顾并总结一下本文!!!
智能指针是一种用于自动管理动态分配资源的指针。它提供了自动化的内存管理,可以减少内存泄漏和悬挂指针等常见的资源管理问题。
常见的智能指针类型:
std::shared_ptr
:允许多个指针共享同一块内存资源,采用引用计数的方式进行内存管理。std::unique_ptr
:独占指针,保证只有一个指针可以访问资源,具有移动语义,可用于实现所有权的传递。std::weak_ptr
:弱引用指针,用于解决std::shared_ptr
循环引用导致的资源泄漏问题。智能指针的优点:
以上便是本文的全部内容,感谢大家的观看与支持!!!