由于异常的存在,反复横跳可能会导致内存泄漏,不同的异常处理逻辑没有妥善管理内存分配和释放,可能会在某些路径中遗漏delete
操作,从而导致内存泄漏。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
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
的 new int
抛出异常:这通常是因为内存分配失败。在这种情况下,程序会终止到这一行,控制权会转移到 main
函数中的 catch
块,因为 Func
中没有处理这个异常。
如果 p2
的 new int
抛出异常:同样的道理,如果 new int
在 p2
的分配中抛出异常,它也会传播到 main
,并在 catch
块中处理。
如果 div
抛出异常(例如除以零):同样,这个异常会被 main
中的 catch
块捕获,输出 “除0错误”。
在所有情况下,如果在 delete p1
或delete p2
之前抛出了异常,程序将不会执行到这两行。因此,内存泄漏的风险在这种情况下是存在的,因为如果 new
语句抛出异常,就不会有对应的 delete
调用。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
使用RAII思想设计一个Smartptr
类:
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete[] _ptr;
cout << "delete: " << _ptr << endl;
}
private:
T* _ptr;
};
double Division(int a, int b)
{
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[20]);
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
智能指针也是一个指针,因此也需要运算符重载
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete[] _ptr;
cout << "delete: " << _ptr << endl;
}
T* get()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return *_ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
上述基本框架解决了指针问题,但是没有解决拷贝的问题。 智能指针有自己的发展历史,拷贝的思想是不一样的。
std::auto_ptr文档
C++98版本的库中就提供了auto_ptr
的智能指针。下面演示的auto_ptr
的使用及问题。
auto_ptr
的实现原理:管理权转移的思想
特点:拷贝构造时转移管理权,sp1
是一个将亡值,进行资源转移,sp1
是一个左值,资源转移导致sp1
对象悬空,因此无法再安全访问 sp1
。任何尝试解引用或访问sp1
都会导致未定义行为。
不推荐使用这一做法
内部核心:
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
std::unique_ptr
使用:
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
unique_ptr<A> sp1(new A);
//获取原生指针
A* p = sp1.get();
cout << p << endl;
//访问元素
sp1->_a1++;
sp1->_a2++;
return 0;
}
特点:std::unique_ptr
不支持拷贝,建议使用。
shared_ptr
的原理:是通过引用计数的方式来实现多个shared_ptr
对象之间共享资源。
shared_ptr
在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
享。0
,就说明自己是最后一个使用该资源的对象,必须释放该资源;0
,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对
象就成野指针了std::shared_ptr
的使用:
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2(sp1);
return 0;
}
在模拟实现shared_ptr
中,引用计数器需要使用指针来实现;
赋值的原理:
在执行赋值的操作时,需要先对sp1
的pcount
进行--
操作,因为此时的pcount==2
,如果直接修改sp1
的pcount
,导致数据不匹配,因此需要先将pcount
的值减为1,再去赋值,这样才能保证引用计数器的值是正确的。这样可以避免内存泄漏。
自己给自己赋值原理:只要资源一样就是自己给自己赋值,直接返回对应指针就行。
完整代码:
#pragma once
namespace gwj
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//sp2(sp1)
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
(*_pcount)++;
}
//sp1==sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
this->release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
void release()
{
if (--(*_pcount) == 0)
{
//最后一个管理的对象释放资源
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
//最后一个管理的对象释放资源
delete _ptr;
delete _pcount;
}
}
int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return *_ptr;
}
private:
T* _ptr;
int* _pcount; //引用计数器,用指针实现
};
}
智能指针对象本身拷贝析构是线程安全的,底层引用计数加减是安全的,指向的资源访问不是线程安全的(该加锁需要加锁)
智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++
或--
,这个操作不是原子的,引用计数原来是1
,++
了两次,可能还是2
.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++
、--
是需要加锁的,也就是说引用计数的操作是线程安全的。
struct Node
{
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
int _val;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<Node> p1(new Node);
shared_ptr<Node> p2(new Node);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
return 0;
}
不支持RAII,不单独管理资源,辅助解决shared_ptr
的循环引用。本质是赋值或拷贝时,只指向资源,但是不增加shared_ptr
的引用计数。
struct Node
{
//shared_ptr<Node> _next;
//shared_ptr<Node> _prev;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
int _val;
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
shared_ptr<Node> p1(new Node);
shared_ptr<Node> p2(new Node);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1->_next = p2;
p2->_prev = p1;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
return 0;
}
定制删除器可以通过在 std::shared_ptr
中使用自定义的删除函数来管理资源。
class A
{
public:
A()
{}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
int _a2 = 1;
};
// 定制删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
int main()
{
//std::shared_ptr<A[]> sp1(new A[10]);
std::shared_ptr<A[]> sp1(new A[2], [](A* ptr) {delete[] ptr; });
std::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });
return 0;
}
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。