下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析Func函数中的问题。
下面的代码中会因为异常的抛出而造成内存泄漏。例如如果在p2的new中抛出异常,那么会直接跳到异常的处理catch的代码块中,这样就没有执行Func函数中的delete p1来释放p1的内存了。而如果在div()函数中抛出异常,那么不会执行delete p1和delete p2,这样就造成了p1和p2的内存泄漏。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw "除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 char* str)
{
cout << str << endl;
}
return 0;
}
尽管我们可以通过下面的方法来避免p1和p2的内存泄漏,但是当Func函数中申请的内存越来越多时,这种解决办法就显得很笨重了。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw "除0错误";
}
return a / b;
}
void Func()
{
//1.如果p1这里的new抛出异常会如何?
//2.如果p2这里的new抛出异常会如何?
//3.如果div调用这里抛出异常会如何?
int* p1 = new int;
int* p2 = nullptr;
try
{
p2 = new int;
}
catch (...)
{
delete p1;
throw;
}
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (const char* str)
{
cout << str << endl;
}
return 0;
}
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
C/C++程序中一般我们关心两种方面的内存泄漏。
在linux下内存泄漏检测:linux下几款内存泄漏检测工
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较
总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
在学习智能指针的使用之前我们需要先了解RAII思想。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
如果我们实现一个SmartPtr模板类来充当指针。然后我们在使用指针时就将指针放到SmartPtr对象中,这样就不会出现上面说的内存泄漏现象了。因为当sp1或sp2或div函数抛异常时,程序就会跳到main函数中执行catch中的代码,而这时就出了Func函数的作用域,所以sp1对象和sp2对象就会自动销毁,而且在销毁前会调用自己的析构函数来将new int的内存进行释放。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{
}
~SmartPtr()
{
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw "除0错误";
}
return a / b;
}
void Func()
{
//1.如果p1这里的new抛出异常会如何?
//2.如果p2这里的new抛出异常会如何?
//3.如果div调用这里抛出异常会如何?
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (const char* str)
{
cout << str << endl;
}
return 0;
}
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
总结一下智能指针的原理:
当我们像下面一样调用SmartPtr的拷贝构造函数时会出现错误。这是因为上面实现的SmartPtr模板类中没有写拷贝构造,所以会使用编译器自动生成的浅拷贝构造函数。所以就造成了两次析构函数释放资源,就出现了错误。
那么我们能否自己写SmartPtr的拷贝构造函数呢?让SmartPtr类模板的拷贝构造函数为一个深拷贝的拷贝构造函数。这样也是不行的,因为我们需要的就是浅拷贝,即当sp2拷贝sp1时,就说明sp1和sp2指向了同一个资源,如果我们使用深拷贝的话,那么sp1和sp2就会指向不同的资源,这样就不和指针的作用相同了。
在c++98中,使用了资源管理权限转移的方式来解决智能指针拷贝的问题。下面我们来了解auto_ptr智能指针的原理。
下面的代码中在auto_ptr的拷贝构造函数中将新的auto_ptr对象指向资源,而将原来的auto_ptr对象中保存的指针置为空。即将ap1的资源管理权转移给了ap2,这样的话就会造成ap1为空指针了。
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//管理权转移
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void test_auto()
{
auto_ptr<int> ap1(new int(1));
auto_ptr<int> ap2(ap1);
*ap1 = 1; //管理权转移以后导致ap1悬空,不能访问
*ap2 = 2;
}
int main()
{
test_auto();
return 0;
}
上面是我们自己实现的auto_ptr智能指针。在c++的库中也提供了auto_ptr智能指针,并且库里面的auto_ptr也存在悬空问题,所以在实际中auto_ptr智能指针没有使用价值。
boost库为c++的“准”标准库。c++11中的智能指针unique_ptr和shared_ptr/weak_ptr就是借鉴的boost库中的scoped_ptr和shared_ptr/weak_ptr。
Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。
下面我们就来了解c++11中的unique_ptr智能指针的原理。在c++11中直接使用delete将unique_ptr的拷贝构造函数和辅助运算符重载函数不自动生成。即不允许unique_ptr进行拷贝。
在c++98的语法中,想要将一个类不允许拷贝,那么可以将这个类的拷贝构造函数和赋值运算符重载函数声明为私有。
在c++11中可以直接将拷贝构造函数和赋值运算符重载函数为delete,即表示禁止生成默认拷贝构造函数和赋值运算符重载函数。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//在c++11中可以直接将拷贝构造函数和赋值运算符重载函数为delete
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//防拷贝
//拷贝构造函数和赋值运算符重载函数是默认成员函数,我们不写会自动生成。
//在c++98中:可以通过将拷贝构造函数和赋值运算符重载函数声明为私有来防止拷贝
/*unique_ptr(const unique_ptr& up);
unique_ptr& operator=(const unique_ptr& up);*/
private:
T* _ptr;
};
int main()
{
unique_ptr<int> up1(new int(1));
unique_ptr<int> up2(up1); //因为防止拷贝,所以不能再进行unique_ptr的拷贝了。
return 0;
}
C++11中还提供了更靠谱的并且支持拷贝的shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
下面我们来模拟实现一个shared_ptr类模板。
我们需要先分析shared_ptr中应该使用什么类型的变量来记录引用计数。如果使用静态成员变量的话,那么通过shared_ptr类模板生成的对象都会共同使用这一个计数,这显然是不行的。因为有可能多个对象指向多个资源,我们需要的是让每个资源配对一个引用计数。
所以我们可以将记录引用计数的变量设为一个指针。
这样如果是第一个智能指针对象指向这个资源,那么就申请一个_pcount指针来记录引用计数,当调用析构函数时,先将引用计数减减,判断是否为0,如果为0那么就释放资源和引用计数。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr),_pcount(new int(1))
{
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),_pcount(sp._pcount)
{
++(*_pcount);
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
int main()
{
shared_ptr<int> sp1(new int(1));
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(new int(10));
return 0;
}
我们看到sp1和sp2的引用计数为2,而sp3资源的引用计数为1。
下面我们来实现shared_ptr的赋值运算符重载函数。我们需要注意的是如果被赋值的对象指向的资源只有它自己,那么当对这个对象进行赋值后,需要将这个对象指向的资源和引用计数给释放掉。例如对sp3赋值时,需要将sp3原来指向的资源先释放掉。
智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。
在下面的代码中两个线程都对对象p的引用计数进行++或–操作,并且因为++或–操作不是原子性的,所以就可能会造成线程安全问题。
下面的代码中我们需要注意,在创建线程对象时,直接传引用是会报错的。需要使用ref函数来传引用。如果不加的话会认为是传值传参,而因为mtx锁不允许拷贝,所以传值传参会出错。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr),_pcount(new int(1))
{
}
~shared_ptr()
{
Release();
}
void Release()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
void AddCount()
{
++(*_pcount);
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),_pcount(sp._pcount)
{
AddCount();
}
// sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//如果两个对象管理的是同一个资源,那么就不需要赋值了
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
AddCount();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
};
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(shared_ptr<Date>& sp, size_t n, std::mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
//这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
shared_ptr<Date> copy(sp);
}
}
void test_shared_safe()
{
shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 10000;
std::mutex mtx;
std::thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
std::thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
t1.join();
t2.join();
cout << p.use_count() << endl;
}
int main()
{
test_shared_safe();
return 0;
}
所以我们就需要为每一份资源加一把锁,当多个对象指向同一个资源时,每个对象想要改变这份资源的内容,都需要先申请锁。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr),_pcount(new int(1)),_pmtx(new std::mutex)
{
}
~shared_ptr()
{
Release();
}
void Release()
{
_pmtx->lock();
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
_pmtx->unlock();
}
void AddCount()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx)
{
AddCount();
}
// sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//如果两个对象管理的是同一个资源,那么就不需要赋值了
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddCount();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
std::mutex* _pmtx;
};
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(shared_ptr<Date>& sp, size_t n, std::mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
//这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
shared_ptr<Date> copy(sp);
}
}
void test_shared_safe()
{
shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 10000;
std::mutex mtx;
std::thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
std::thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
t1.join();
t2.join();
cout << p.use_count() << endl;
}
int main()
{
test_shared_safe();
return 0;
}
但是上面的代码中,我们虽然保证了线程的安全,但是我们在释放资源时并没有将锁释放。因为在Release中释放资源时,锁还处于使用状态,所以我们可以使用下面的方案来释放锁。即当判断这个资源的引用计数为0时,在释放资源后进行归还锁,然后再将锁资源释放。
我们还可以通过下面设置一个标志位的方法来进行锁的释放。这样就解决了锁的释放问题。
上面通过给智能指针中的引用计数操作进行加锁,我们保证了智能指针的引用计数是线程安全的。但是我们不能保证智能指针指向的资源是线程安全的。当在多线程情况下,我们看到智能指针指向的资源会出现线程安全问题。
当我们将通过智能指针修改资源的操作进行加锁后,这样就保证了智能指针指向的资源是线程安全的了。
我们使用库里面的shared_ptr智能指针时,如果不将通过智能指针修改资源的操作加锁,那么也会出现线程安全问题。
当我们加上锁之后就没有线程安全问题了。
当我们想要将两个结点连接起来时,我们可以使用下面的代码,但是下面的程序当在delete之前抛异常时,那么就会出现内存泄漏问题。
所以我们可以使用智能指针来实现。但是原生指针不能赋值给智能指针,所以我们需要将ListNode结构体中的_ next和_ prev也使用智能指针。
当我们运行时发现智能指针n1和n2指向的结点并没有调用析构函数来进行释放。
但是当我们屏蔽一句两个结点的连接代码或者两句都屏蔽时,就会调用对应的析构函数。这就是shared_ptr的循环引用问题。
当下面的情况时,n1和n2在析构时因为引用计数为1,减减后为0,所以会执行delete _ ptr,而 _ ptr为ListNode * 类型的指针,所以会调用ListNode的析构函数来释放所指向的结点的资源。
下面的情况时。当n2调用析构函数时,发现引用计数为2,然后将引用计数减减,并不会进行资源释放。当n1调用析构时,将引用计数减减后为0,然后将自己指向的资源释放,在释放资源时会先调用_ next指向的资源,先将_ next指向的资源释放。而_ next指向的资源引用计数为1,减减后为0,所以会调用析构函数释放资源。
下面的情况中,当n2出作用域时会调用自己的析构函数,然后发现引用计数减减后还为1,所以不会delete释放结点资源。当n1出作用域时会调用自己的析构函数,然后发现引用计数减减后也为1,所以不会delete释放结点资源。然后就造成了_ next指向结点2,_ prev指向结点1。然后这两个智能指针的引用计数都为1,并且不再改变,所以就不会释放delete释放结点1和结点2了。
此时我们就可以使用weak_ptr来解决。weak_ptr不是常规的智能指针,不支持RAII思想。但是它支持像指针一样使用。它是专门设计出来辅助解决shared_ptr的循环引用问题的。weak_ptr可以指向资源,但是它不参与资源管理,不增加引用计数。
weak_ptr的拷贝构造函数中可以将一个shared_ptr智能指针赋值给weak_ptr智能指针。
我们看到当使用weak_ptr时就不会出现循环引用问题了。
下面我们来模拟实现一个weak_ptr智能指针。
然后我们看到循环引用问题就解决了。
当我们在智能指针中存放的是一个new出来的数组时,程序会崩溃。因为我们在shared_ptr的析构函数中释放内存时写的都是delete,所以当delete释放 new []时,会出现错误。
所以shared_ptr设计了一个删除器来解决这个问题。定制删除器为一个可调用对象,我们可以传函数指针,仿函数,lambda表达式等。即可以通过定制删除器来让用户自己定义delete时需要进行的操作。如果我们不写默认就是delete。
我们还可以传lambda表达式,并且将delete替换为关闭文件。