公司裏小組組織c++知識的分享會,正好我手上碰到過幾個purify的內存泄露問題,就借這裏總結一下c++的內存問題。
借鑑陳碩總結的分類,c++大致的內存問題有以下幾個方面:
1.緩衝區溢出
在使用自己編寫的緩衝區或者使用不安全的函數時,會遇到類似數組越界的緩衝區溢出問題,Linux內核的解決辦法是棧隨機化,金絲雀的檢測,具體的攻擊手段和例子,可以參考我另一篇的buffer lab實驗。在自己寫程序的時候,最重要的一點是記錄或者限制緩衝區的長度,使用vector<char>
這樣的容器,strncpy
這樣更安全的函數。
在purify檢測的時候,使用的就是類似金絲雀的機制,在緩衝區前後插入特殊的數值,如果其數值被修改了,就是溢出。
2.空懸指針/野指針
指針在所指的內存空間被釋放後,沒有置爲NULL,指針的生存期還沒有結束,這時候指針所指的區域是個隨機值。並且可以繼續使用!!!
下面使用代碼進行驗證,環境爲win7,64位,Dev-c++5.11,gcc(c11)。
void testNullPtr(){
int *p=new int(10);
delete p;
cout<<*p<<endl;
*p=20;
cout<<*p<<endl;
}
上面代碼的運行結果:
可以看出第一次是隨機值,第二次竟然正常更新了。
在正常工程使用中,指針delete完之後置NULL,以及不要返回局部指針變量!!!如果兩個指針指向同一內存,其中一個置NULL,依然解決不了空懸指針的問題。如下:
重置p對q沒有任何作用!
int *p(new int(42));
auto q=p;
delete p;
p=nullptr;
關於局部變量的示例代碼:
char *itoa(int n){
char buf[43];
sprintf(buf,"%d",n);
return buf;
}
將buf聲明爲static
即可。
3.重複釋放
非常經典的double free問題,運行如下程序:
void testDoubleFree(){
int *p=new int(10);
delete p;
delete p;
}
運行結果會產生運行時錯誤,調試狀態下會有SIGTRAP信號。
解決辦法也是指針置NULL。拓展一下,指針置NULL之後還可以釋放嗎?答案是可以。這裏需要研究一下new和delete。
new和delete其實就是對malloc和free的封裝。
- 對於簡單數據,直接調用operator new分配內存。但是可以使用new_handler來處理new失敗的情況。
- 另一處不同,malloc失敗返回NULL,而new拋出異常。
- 對於複雜數據結構,例如對象,先調用operator new分配內存後,在調用其構造函數。
- delete對於簡單數據,直接調用free。
對於複雜的數據結構,先析構,後delete。
free的部分代碼如下(glibc):
函數會對指針進行NULL的檢測,爲空直接返回,因此delete完之後置NULL可以避免重複釋放問題。
if (ptr == NULL)
{
catomic_increment (&calls[idx_free]);
return;
}
4.內存泄露
這一問題可以通過智能指針解決,下面再細講。
5.不配對的new[]/delete
new和delete在聲明使用的時候需要配對使用。
int *p=new int(10);
delete p;
int *pa=new int[10];
delete []pa;
當我們釋放一個指向數組的指針時,它指示編譯器指針指向一個對象數組的第一個元素,元素按逆序銷燬。
承接上面對new和delete的研究,在調用new[]和delete[]的時候。
- 對於簡單數據,new[]計算好大小後調用operator new。
- 對於複雜數據類型,先分配內存,寫入數組大小,然後調用N次構造。
- 對於簡單數據類型,delete[]和delete效果一樣。
對於複雜的數據類型,delete[]先析構,後釋放空間。
注意!!!以下代碼正常運行,編譯器沒有警告。
class Obj{
public:
Obj(){}
};
void FitNewDelete(){
Obj *p=new Obj[3];
delete p;
}
在《c++primer》中提到,上面的行爲是未定義的。可能會崩潰,可以是行爲異常。比較一勞永逸的辦法是使用vector代替數組。
6.內存碎片
常用的解決辦法是實現自己的memory pool,這裏不詳細討論了,因爲首先現在的malloc有所優化,第二這個問題有時候影響不大。
上面參照了《linux多線程服務端》的分類,現在講解一下purify的問題,purify的內存問題主要分爲以下幾類。
這裏不詳細解釋了,英文應該很好懂。
講一下在工作過程中碰到的兩個實例。
第一個是UMR,未初始化內存讀的問題,很多時候,這個問題並不算問題,不具有準確性。
例如下面的代碼就會有UMR問題,這裏是因爲結構體的字節填充,其中smth的field2會因爲4字節的地址對齊的需要,被填充三個字節。
struct something {
int field1;
char field2;
};
/* ... */
struct something smth, smth2;
smth.field1 = 1;
smth.field2 = 'A';
smth2 = smth;
而我遇到的UMR代碼如下:
struct test: public std::binary_function<x, y, bool>
{
bool operator() (const x& thisInfo, y otherEntityType) const
{
return (thisInfo.entityType == otherEntityType);
}
};
這個函數乍看沒有任何問題,函數的綁定和函數操作而已,如果做類似調用,就會有UMR問題。
this->ritTypeInfo = std::find_if(RITS_begin, RITS_end, bind2nd(test(), entityType));
問題出在編譯器自己合成的構造函數和拷貝構造上,如果你不希望編譯器合成,或者不清楚合成的代碼效果,請明確的構造出來。這裏的解決辦法就是構造出空的構造函數和拷貝構造函數即可。
接下來的一個例子也與拷貝構造函數和拷貝賦值運算符有關。
首先明確一下兩個函數出現的地方,拷貝賦值是同類對象賦值時出現。拷貝構造是在使用=定義變量時、將一個對象作爲實參傳遞給一個非引用類型的形參、以及從一個返回類型爲非引用類型的函數返回一個對象時使用。
如果自己沒有定義,編譯器會爲我們合成一個。實例代碼如下:
class CheckApp{};
class check{
CheckApp *pCheckApp;
public:
check(){
pCheckApp=new CheckApp();
}
~check(){
if(pCheckApp!=NULL){
delete pCheckApp;
pCheckApp=NULL;
}
}
};
這裏在使用拷貝賦值和拷貝構造函數時,編譯器會爲我們合成拷貝構造和拷貝賦值函數。類似下面的代碼:
check::check(const check& rhs)
:pCheckApp(rhs.pCheckApp){}
check& check::operator=(const check& rhs){
if(this!=&rhs){
pCheckApp=rhs.pCheckApp;
}
return *this;
}
這個代碼有什麼問題?看一下面的圖片就知道了。
對於指針型的成員變量,合成的拷貝賦值和拷貝構造只是複製了指針本身,而不是指向的對象,這叫做淺拷貝,當其中的s1或者s2析構釋放的時候,它所指向的內存空間就被釋放掉了,另一個指針就變成了野指針,會出現double free。同時也存在其中一個指針更改值造成另一個對象值也更改的現象。
應急的解決辦法是自己完成拷貝構造和拷貝賦值。代碼如下:
//先完成拷貝構造,下面賦值要用
check::check(const check& rhs){
pCheckApp=new CheckApp();
if(pCheckApp!=NULL){
*pCheckApp=*(rhs.pCheckApp);
}
}
//進行深拷貝,按值拷貝
check& check::operator=(const check &rhs){
//這個判斷是防止自賦值 a=a
if(this!=&rhs){
//使用構造局部變量,進行成員交換
//局部變量出作用域會自動釋放
//防止new失敗,簡化設計
check tmp(rhs);
CheckApp *tmpCheckApp=pCheckApp;
pCheckApp=tmp.pCheckApp;
tmp.pCheckApp=tmpCheckApp;
}
return *this;
}
上面提到應急兩個字,言外之意,應該有更好的解決辦法,相信各位應該也能想到了,智能指針。
簡單來說,智能指針在上圖的S1和S2與內存空間之間加了一個代理層,一個新的對象,讓s1和s2所指的對象永久有效,先命名爲proxy,同時把兩個指針都變成對象,sp1,sp2。proxy有兩個成員,指針和計數器。sp1析構後,計數器減一,計數爲0時,銷燬proxy指針指向的對象。
空懸指針野指針可以用shared_ptr/weak_ptr解決,對於重複釋放可以選擇unique_ptr與scoped_ptr解決。
其中shared_ptr、weak_ptr、scoped_ptr爲boost庫模板。
c11吸收了shared_ptr、weak_ptr並使用具有移動語意的unique_ptr代替scoped_ptr,它們聲明在memory頭文件中。
這裏簡單介紹一下用法,代碼如下,更多的查看手冊:
shared_ptr運行多個指針指向同一個對象,這就可以解決上面的淺拷貝問題。
注意shared_ptr有一個非常有用的特性,刪除器,可以使析構動作在構造時被捕捉。
template<class T>
struct endPtr{
//主要用於非動態分配的對象
//用於不具有良好的析構函數的對象
//deleter是個泛型類型,需要operator()
void operator()(T* p){
delete [] p;
cout<<"now delete"<<endl;
}
};
void testSharePtr(){
//shared_ptr<int> q(new int(10)); 不建議
shared_ptr<int> q=make_shared<int> (42);
//c11 構造
auto p=make_shared<int> (40);
//使用
cout<<"p:"<<*p<<" use: "<<p.use_count()<<endl;
//引用數
cout<<"q:"<<q.use_count()<<endl;
//判斷
cout<<"is unique?: "<<q.unique()<<endl;
p=q;
cout<<"p:"<<p.use_count()<<endl;
cout<<"q:"<<q.use_count()<<endl;
int *tmp=new int[100];
//定義自己的刪除器
shared_ptr<int> r(tmp,endPtr<int>());
}
結果如下:
一個unique_ptr只能指向一個給定對象,不支持普通的拷貝和賦值,但是可以使用函數release或者reset轉移指針所有權,scoped_ptr則不允許,兩者異同:
A auto_ptr is a pointer with copy and with move semantics and ownership (=auto-delete).
A unique_ptr is a auto_ptr without copy but with move semantics.
A scoped_ptr is a auto_ptr without copy and without move semantics.
auto_ptrs are allways a bad choice – that is obvious.
Whenever you want to explicitely have move semantics, use a unique_ptr.
Whenever you want to explicitely disallow move semantics,use a scoped_ptr.
最後介紹一下weak_ptr,shared_ptr是強引用,拿鐵絲綁着對象,而weak_ptr是棉線掛着(陳碩的比喻),weak_ptr不控制所指對象的生命週期,對象的釋放和weak_ptr無關,這種弱引用可以拿來打破shared_ptr的循環引用問題,兩個shared_ptr互相引用,會造成對象無法釋放。
weak_ptr起到一個檢測的作用!!!
auto p=make_shared<int> (42);
//不改變引用計數
weak_ptr<int> wp(p);
//由於對象可能不存在,使用lock函數,如果有的話,返回shared_ptr
if(shared_ptr<int> np=wp.lock()){
}
weak_ptr還可以用於弱回調,把shared_ptr綁定到function裏,會延長對象的生命週期,如果想實現對象活着就調用,否則忽略的效果,可以使用weak_ptr。
最後對weak_ptr和shared_ptr做一個簡單的分析。
unique_ptr使用元素,指針和刪除器。
// unique_ptr內部片段
template <typename _Tp, typename _Dp = default_delete<_Tp> >
class unique_ptr
{
// use SFINAE to determine whether _Del::pointer exists
class _Pointer
{
template<typename _Up>
static typename _Up::pointer __test(typename _Up::pointer*);
template<typename _Up>
static _Tp* __test(...);
typedef typename remove_reference<_Dp>::type _Del;
public:
typedef decltype(__test<_Del>(0)) type;
};
typedef std::tuple<typename _Pointer::type, _Dp> __tuple_type;
__tuple_type _M_t;
public:
typedef typename _Pointer::type pointer;
typedef _Tp element_type;
typedef _Dp deleter_type;
};
shared_ptr在基類的基礎上加上刪除器參數。下面是示意性的摘錄。
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
public:
//其中一個構造函數
template<typename _Tp1, typename _Deleter>
shared_ptr(_Tp1* __p, _Deleter __d)
: __shared_ptr<_Tp>(__p, __d) { }
};
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
{
public:
typedef _Tp element_type;
protected:
friend class __weak_ptr<_Tp, _Lp>;
private:
void*
_M_get_deleter(const std::type_info& __ti) const noexcept
{ return _M_refcount._M_get_deleter(__ti); }
_Tp* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
};
最後給一個玩具型的參考實現,來自c++primer答案參考。
/***************************************************************************
* @file shared_pointer.hpp
* @author Yue Wang
* @date 04 Feb 2014
* Jul 2015
* Oct 2015
* @remark This code is for the exercises from C++ Primer 5th Edition
* @note
***************************************************************************/
#pragma once
#include <functional>
#include "delete.hpp"
namespace cp5
{
template<typename T>
class SharedPointer;
template<typename T>
auto swap(SharedPointer<T>& lhs, SharedPointer<T>& rhs)
{
using std::swap;
swap(lhs.ptr, rhs.ptr);
swap(lhs.ref_count, rhs.ref_count);
swap(lhs.deleter, rhs.deleter);
}
template<typename T>
class SharedPointer
{
public:
//
// Default Ctor
//
SharedPointer()
: ptr{ nullptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
{ }
//
// Ctor that takes raw pointer
//
explicit SharedPointer(T* raw_ptr)
: ptr{ raw_ptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
{ }
//
// Copy Ctor
//
SharedPointer(SharedPointer const& other)
: ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ other.deleter }
{
++*ref_count;
}
//
// Move Ctor
//
SharedPointer(SharedPointer && other) noexcept
: ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ std::move(other.deleter) }
{
other.ptr = nullptr;
other.ref_count = nullptr;
}
//
// Copy assignment
//
SharedPointer& operator=(SharedPointer const& rhs)
{
//increment first to ensure safty for self-assignment
++*rhs.ref_count;
decrement_and_destroy();
ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter;
return *this;
}
//
// Move assignment
//
SharedPointer& operator=(SharedPointer && rhs) noexcept
{
cp5::swap(*this, rhs);
rhs.decrement_and_destroy();
return *this;
}
//
// Conversion operator
//
operator bool() const
{
return ptr ? true : false;
}
//
// Dereference
//
T& operator* () const
{
return *ptr;
}
//
// Arrow
//
T* operator->() const
{
return &*ptr;
}
//
// Use count
//
auto use_count() const
{
return *ref_count;
}
//
// Get underlying pointer
//
auto get() const
{
return ptr;
}
//
// Check if the unique user
//
auto unique() const
{
return 1 == *refCount;
}
//
// Swap
//
auto swap(SharedPointer& rhs)
{
::swap(*this, rhs);
}
//
// Free the object pointed to, if unique
//
auto reset()
{
decrement_and_destroy();
}
//
// Reset with the new raw pointer
//
auto reset(T* pointer)
{
if (ptr != pointer)
{
decrement_n_destroy();
ptr = pointer;
ref_count = new std::size_t(1);
}
}
//
// Reset with raw pointer and deleter
//
auto reset(T *pointer, const std::function<void(T*)>& d)
{
reset(pointer);
deleter = d;
}
//
// Dtor
//
~SharedPointer()
{
decrement_and_destroy();
}
private:
T* ptr;
std::size_t* ref_count;
std::function<void(T*)> deleter;
auto decrement_and_destroy()
{
if (ptr && 0 == --*ref_count)
delete ref_count,
deleter(ptr);
else if (!ptr)
delete ref_count;
ref_count = nullptr;
ptr = nullptr;
}
};
}//namespace