1. 程式人生 > >C++----動態記憶體管理

C++----動態記憶體管理

一、動態記憶體開闢

1、C語言中c語言中malloc,calloc,realloc

(1)共同點:a、都是從堆上開闢記憶體空間的,必須釋放,否則會造成記憶體洩漏。

                   b、返回值:返回值都是"void *"

(2)不同點:a、malloc只有一個引數,引數為所需要申請空間位元組數的大小:malloc使用前一定要進行檢測,防止拿到沒用的空間。申請成功:返回申請的地址;失敗:返回空。calloc有兩個引數:第一個是元素個數,第二個是單個元素的大小;且需要初始化為0。申請成功,返回申請的首地址,失敗,返回空。c、realloc有兩個引數:第一個表示起始地址(void *p),第二個引數為size, 位元組數。既可以把空間擴大、也可以把空間減少。p為NULL,malloc;非空,size(變小:返回空間原地址;擴容:看size的大小,看是否需要開闢新空間。malloc申請空間,保證一塊空間只能被一個人使用,如何保證:把空間管理起來,在空間的前面加一個結構體,申請的空間的大小,遠大於輸入的數字。管理空間的大小 。有一個結構體,結尾還有四個位元組。可能會造成空間的浪費。拿到結構體的大小即知道這個空間有多大,縮小也是通過這個結構體。

模擬實現一個malloc:

2、在棧上申請一個空間:使用_alloca在棧上動態開闢記憶體,棧上開闢的記憶體由編譯器自動維護,不需要使用者顯式釋放.出了作用域系統自動釋放。

3、常見記憶體洩漏:

(1)記憶體申請之後忘記釋放

(2)程式邏輯不清,以為釋放了,實際記憶體洩漏【兩個指標指向同一塊空間,釋放兩次,會導致同一塊空間被釋放兩次,造成記憶體洩漏。】

(3)程式誤操作,將堆破壞

 char *pTest3 = (char *)malloc(5);       
strcpy(pTest3, "Memory Leaks!");       
free(pTest3); 
拷貝時會發生越界,越界後進行釋放,覆蓋了4個FD點,可以少用,但是不能多用

(4)釋放時傳入的地址和申請時的地方不同

那麼什麼是記憶體洩漏,如何進行記憶體洩漏檢測呢?

 二、c++記憶體管理

1、如何申請:

int main()
{
	//申請單個型別的空間
	int *p1 = new int;
	int *p2 = new int(10);//申請一個整型空間,大小為10
	//申請10個整型空間
	int *p3 = new int[10];
	return 0;
}

2、釋放

int main()
{
	//申請單個型別的空間
	int *p1 = new int;
	int *p2 = new int(10);//申請一個整型空間,大小為10
	//申請10個整型空間
	int *p3 = new int[10];
	//釋放單個空間
	delete p1;
	delete p2;
	//釋放連續空間
	delete[] p3;
	return 0;
}

new申請的空間不需要進行判空,但是malloc申請空間失敗後會返回一個空。new和delete只是c++中的兩個操作符,不是函式。而且這兩個操作符可以進行過載。

3、自定義型別:

class Test
{
public:
	Test()
	{
		_t = 0;
		cout << "Test():" << endl;
	}
	~Test()
	{
		cout << "~Test():" << endl;
	}
private:
	int _t;
};
int main()
{
	Test *pt1 = (Test*)malloc(sizeof(Test));
	//在c++中用malloc申請的空間不是物件,不會呼叫建構函式。malloc繼承自C語言,C語言中沒有建構函式
	Test *pt2 = new Test;
	//用new申請的空間,不僅申請了空間,還執行了建構函式。
	//free(pt1);
	delete pt2;
}

用delete釋放:

用free釋放

int main()
{
	Test *pt1 = (Test*)malloc(sizeof(Test));
	//在c++中用malloc申請的空間不是物件,不會呼叫建構函式。malloc繼承自C語言,C語言中沒有建構函式
	Test *pt2 = new Test;
	//用new申請的空間,不僅申請了空間,還執行了建構函式。
	free(pt1);
	//delete pt2;
	system("pause");
}


說明malloc,free沒有呼叫建構函式;而new,delete呼叫了建構函式

int main()
{
	 
	Test *pt3 = new Test[10];
	delete[] pt3;
 }


delete釋放的是單個物件的空間,delete[]銷燬一段連續的空間。先申請的物件,最後銷燬。

3、需要匹配使用:

(1)

A、沒有呼叫建構函式,就去釋放資源,會導致程式崩潰,

class Test
{
public:
	Test()
	{
		_t = 0;
 		cout << "Test():" << endl;
	}
	~Test()
	{
 		cout << "~Test():" << endl;
	}
private:
	int _t;
 };
int main()
{
	Test *p1 = (Test*)malloc(sizeof(Test));
	delete p1;
	return 0;
} 
執行結果中,只有解構函式的執行結果而沒有建構函式的執行結果。並且程式會發生記憶體洩漏,建構函式和解構函式需要成對使用。


B、

int main()
{
 	Test *p2 = (Test*)malloc(sizeof(Test));
 	delete[] p2;

	return 0;
} 
此時釋放就立刻返回中斷

 

C、

int main()
{
	Test *p3 = new Test;//可能發生記憶體洩漏
	//Test *p4 = new Test;//程式會崩潰
	free(p3);
	//delete[] p4;
	
	//Test *p5 = new Test[10];程式會發生崩潰
	Test *p6 = new Test[10];//程式碼發生崩潰,解構函式不能列印
	//free(p5);
	delete p6;

	system("pause");
	return 0;
} 

總結:mallo申請的空間,不用free去進行釋放,或者new申請的空間,不用delete釋放,有的情況會發生記憶體洩漏,有的情況下會發生程式崩潰;

a、此處會發生錯誤:

Test* pt=new Test[10];
cout<<(*(int *)pt-1)<<endl;

delete pt;

此處會發生錯誤是因為:delete一次釋放一個,不會把指標向前倒四個,所以一定會出現問題。

b、 如下程式碼一定會崩潰:

Test *pt=new Test;
delete[] pt;

因為要到函式的前四個位元組取解構函式的次數,是一個無意義的東西。

如果不寫解構函式,則採用上述程式碼,不會出現崩潰,不寫解構函式,不用進行析構。沒有多申請的4個位元組。沒有解構函式,則並不用呼叫解構函式,就不用多給4個位元組儲存物件的個數。,直接釋放。如果給出了解構函式,則必須成對使用;沒有給出解構函式,則不會因為不匹配而出現記憶體洩漏等問題。

4、因為建構函式是朝物件裡面放東西,所以先開闢空間,空間開闢好了再呼叫建構函式,朝空間裡面放東西。

C++中不支援垃圾回收。在底層有個_callnewh函式指標,是空間不足時的應對措施,但是這部分釋放的工作應該由使用者來完成,使用者自己才能知道空間裡面哪些空間不用使用。丟擲異常,知道new申請失敗。

class Test
{
public:
	Test()
	{
		_t = 0;
 		cout << "Test():" << endl;
	}
	~Test()
	{
 		cout << "~Test():" << endl;
	}
private:
	int _t;
 };
int main()
{
	Test *pt = new Test;
	delete pt;
	system("pause");
	return 0;
} 

new要做的事情:a、申請物件的空間:在底層呼叫operator new函式,用malloc連續申請,直到申請成功。如果用malloc申請失敗了:是否有空間不足的應對措施,,如果有,就繼續申請,直到申請成功,如果沒有,則丟擲異常。b呼叫建構函式。

銷燬時,delete要做的事情:清理資源後釋放空間,。a、先呼叫物件的解構函式,清空物件中的資源;b、呼叫operator delete(p)函式釋放空間:先檢測要釋放的空間是否為空,如果為空,則返回;如果不是空,就呼叫free。(new和delete實際上就是把malloc和free重新封裝了一次。)

new Test[10]要做的事情:(Test *pt=new Test[n])a、先呼叫operator new[](sizeof(Test)*n)函式;再呼叫operator new[](sizeof(Test)*n+4)函式;在operrator new()裡面通過malloc申請空間,同上;pt拿到的地址就是operator new()申請到的地址是向後偏轉4個位元組【pt是operator new[]返回值向後偏轉4個位元組的位置】【10為物件的個數,呼叫解構函式參考】【呼叫建構函式構造n個物件】;b、空間申請成功後,返回空間的首地址;c、在operator new()函式體裡面返回函式的值;d、執行建構函式。在底層申請的空間比真實空間多4個位元組,這4個位元組裡面若為整形【*((int *)pt-1)可以找到多出的四個位元組的位置。 】

delete[] pt要做的事情:a、清空物件中的資源(必須知道有多少空間(讓pt向前偏移4個位元組,取物件的個數即呼叫解構函式的次數),迴圈呼叫解構函式呼叫物件中涉及的資源);b、釋放空間【用operator new[]釋放資源(通過free釋放)】【歸還時,從起始空間的起始地址開始釋放】

(1)如果自己寫的operator new()函式,則先呼叫自己寫的,但是仍然會呼叫建構函式,因為此處的物件是建構函式。

class Test
{
public:
	Test()
	{
		_t = 0;
 		cout << "Test():" << endl;
	}
	~Test()
	{
 		cout << "~Test():" << endl;
	}
private:
	int _t;
 };
void *operator new(size_t size)
{
	return malloc(size);
}
int main()
{
	Test *pt = new Test;
	return 0;
} 

(2)構造一個全域性的operator new()函式和一個區域性的,庫中還有一個,這三個函式是可以共存的。此處先呼叫類裡面的函式

class Test
{
public:
	Test()
	{
		_t = 0;
 		cout << "Test():" << endl;
	}
	void *operator new(size_t size)
	{
		return malloc(size);
	}
	~Test()
	{
 		cout << "~Test():" << endl;
	}
private:
	int _t;
 };
void *operator new(size_t size)
{
	return malloc(size);
}
int main()
{
	Test *pt = new Test;
	return 0;
} 

申請空間是在哪一個檔案的哪一個函式體的哪一行申請的:

class Test
{
public:
	Test()
	{
		_t = 0;
 		cout << "Test():" << endl;
	}
	~Test()
	{
 		cout << "~Test():" << endl;
	}
private:
	int _t;
 };
void *operator new(size_t size,const char *strFileName,const char* strFunc,size_t line)
{
	cout << strFileName << ":" << strFunc << ":" << line << ":" << size << endl;
	return malloc(size);
}
#ifdef DEBUG
#define new new(__FILE__, __FUNCDNAME__, __LINE__) Test
#endif
int main()
{
	Test *pt = new Test;
	return 0;
} 

5、new底層由malloc建立,malloc申請的空間比實際需要的空間大,這些節點在用new建立時,還要開闢一個結構體的大小,會造成空間的浪費。

typedef int DataType;
struct Node
{
	Node *_pNext;
	DataType _data;
	Node(const DataType& data)
		:_pNext(NULL)
		, _data(data)
	{}
};
class List
{
public:
	List()
		:_pHead(NULL)
	{}
	void PushBack(const DataType& data)
	{
		if (NULL == _pHead)
			_pHead = new Node(data);
		else
		{
			Node *pTail = _pHead;
			while (pTail->_pNext)
				pTail = pTail->_pNext;
			pTail->_pNext = new Node(data);
		}
	}
private:
	Node *_pHead;
};

那麼,我們可以先把節點的大小通過malloc一次性給出來,此時只需要多餘的一個結構體,節省了空間。通過連結串列把節點連結起來。但是通過malloc開闢的空間,此時開闢的空間並不是節點,只是和節點相同大小的一段空間,需要呼叫建構函式。(在已經存在的空間上面執行建構函式)

int main()
{
	Node *pt = (Node*)malloc(sizeof(Node));
	new(pt + 3) Node(3);//地址加上偏移量,定位到第三個節點,執行其建構函式(定位New表示式)
	
        int array[10];
        array[0] = 0;
        new(array + 3) int(3);
        return 0;
}
一個連結串列每個節點都用new建立,會有很多空間的浪費,有一些額外的記憶體空間,此時可以通過malloc申請一塊大的記憶體空間出來,然後從這塊空間裡面,一個一個的拿空間出來用。malloc申請的空間不是一個物件,需要通過定位new表示式,生成一個物件。定位new表示式:“new(地址) 型別相應的構造”:在這塊地址上面執行建構函式,把空間變成一個類物件。