天天看點

C++對象構造語義學1 對象指派及析構語義學2 new與delete探究3 臨時對象

1 對象指派及析構語義學

1.1 對象複制

當我們沒有寫預設的拷貝構造函數并且不滿足編譯器為我們預設合成構造函數的條件,當我們拷貝構造一個對象時,編譯器也會進行一些特殊的複制處理

EG:

#include<iostream>
using namespace std;

class A
{
public:
	int a;
};

int main()
{
	A a;
	a.a = 1;
	
	A a1 = a;
	cout << a1.a << endl;  //列印1

	return 0;
}
           

1.2 對象析構

在以下兩種情況下,如果我們自己沒有寫析構函數,編譯器會為對象合成析構函數:

  1. 繼承的基類中有析構函數,編譯器會為派生類合成一個析構函數調用基類的析構函數
  2. 存在某一個成員變量類型為類A(類A有析構函數)

2 new與delete探究

2.1 new後面加不加括号的差別

EG:

#include<iostream>
using namespace std;


class A
{
public:
	int a;
};

class B
{
public:
	B()
	{

	}
	int b;
};

int main()
{

	//對于類A,我們沒有自己提供構造函數
	/*
	A a;
	cout << a.a << endl;  //會報錯,使用未初始化變量

	A a1 = A();
	cout << a1.a << endl;  //會報錯,使用未初始化變量
	*/

	A* a = new A();  //對于沒有構造函數的類,在new之後帶有括号,類成員變量會被初始化為0
	cout << a->a << endl;

	A* a1 = new A;
	cout << a1->a << endl; //new後面沒有加括号,在VS2017中雖然沒有報錯,但是列印的是一個随機值(由此推斷:C++在棧上的變量必須初始化才可以使用,但是堆上的變量可以不用初始化直接使用不會報錯)

	//對于類B,由于自己提供了一個無參構造函數,是以以下兩種寫法是一樣的
	B *b = new B();
	B *b1 = new B();
	cout << b->b << endl;  //列印随機值
	cout << b1->b << endl;  //列印随機值

	return 0;
}
           

2.2 new和delete幹了啥

對于一個有構造函數的類與有析構函數的類,new和delete主要做了以下事情

new:首先調用 operater new ,然後在這個new關鍵字中調用malloc函數,然後調用類的構造函數

delete:首先調用類的析構函數,然後調用 operator delete,然後在 delete關鍵字中調用free函數

在我們使用new關鍵字申請一塊記憶體時,其實編譯器真正消耗的記憶體比我們申請的記憶體要大,因為編譯器需要一塊記憶體來管理我們申請的記憶體

2.3 類内重載new和delete操作符

EG:

#include<iostream>
using namespace std;

class A
{
public:

	//構造函數與析構函數
	A()
	{
		cout << "構造函數" << endl;
	}

	~A()
	{
		cout << "析構函數" << endl;
	}

	//重載new與delete操作符
	void* operator new(size_t size)
	{
		cout << "new" << endl;
		return (A *)malloc(size);
	}

	void operator delete(void *p)
	{
		cout << "delete" << endl;
		free(p);
	}

	//重載new[]與delete[]操作符
	void* operator new[](size_t size)
	{
		cout << "new[]" << endl;
		return (A *)malloc(size);
	}

	void operator delete[](void *p)
	{
		cout << "delete[]" << endl;
		free(p);
	}

};

void func1()  //測試 A類中重載的new與delete操作符
{
	A *a = new A();
	delete a;
}

void func2()
{
	A *a = new A[3]();
	delete[]a;
}

int main()
{

	func1();
	func2();

	return 0;
}
           

2.4 重載全局new與delete

EG:

#include<iostream>
using namespace std;

void *operator new(size_t size)
{
	cout << "全局new" << endl;
	return malloc(size);
}
void *operator new[](size_t size)
{
	cout << "全局new[]" << endl;
	return malloc(size);
}
void operator delete(void *phead)
{
	cout << "全局delete" << endl;
	free(phead);
}
void operator delete[](void *phead)
{
	cout << "全局delete[]" << endl;
	free(phead);
}


int main()
{
	int *a = new int();

	return 0;
}
           

2.5 定位new

定位new:在已經配置設定好的記憶體中初始化一個對象,使用定位new不申請新的空間

EG:

#include<iostream>
using namespace std;


int main()
{
	void * vp = malloc(sizeof(int));

	int *p = new(vp) int();  //使用定位new

	free(vp);

	return 0;
}
           

2.6 重載多個版本的new

EG:

#include<iostream>
using namespace std;

class A
{
public:

	//重載new關鍵字第一個參數類型必須是size_t
	void *operator new(size_t size, void *phead)
	{
		cout << "重載定位new" << endl;
		return phead;
	}
	void *operator new(size_t size, int a)
	{
		cout << "重載帶一個int參數的船新版本的new" << endl;
		return malloc(size);
	}
	void *operator new(size_t size, int a,int b)
	{
		cout << "重載帶兩個int參數的船新版本的new" << endl;
		return malloc(size);
	}
};


int main()
{

	void *vp = malloc(sizeof(A));
	A *a1 = new (vp) A();  //使用重載的定位new

	A *a2 = new (3) A();  //使用重載帶一個int參數的new

	A *a3 = new (3,5) A();  //使用重載帶兩個int參數的new

	delete a1;
	delete a2;
	delete a3;

	return 0;
}
           

2.7 記憶體池

當我們頻繁的使用new申請空間時,由于new内部使用malloc,而一次malloc消耗的空間大于我們申請的空間,如果是頻繁的申請小塊空間,更加得不償失。記憶體池一次申請一塊較大的空間,當需要申請記憶體時,就從申請的較大的記憶體空間中取。

EG:

#include<iostream>
#include <ctime>
using namespace std;

class A
{
public:
	static void *operator new(size_t size);
	static void operator delete(void *phead);
	static int m_iCout; //配置設定計數統計,每new一次,就統計一次
	static int m_iMallocCount; //每malloc一次,我統計一次
private:
	A *next;
	static A* m_FreePosi; //總是指向一塊可以配置設定出去的記憶體的首位址
	static int m_sTrunkCout; //一次配置設定多少倍的該類記憶體
};

int A::m_iCout = 0;
int A::m_iMallocCount = 0;
A *A::m_FreePosi = nullptr;
int A::m_sTrunkCout = 5; //一次配置設定5倍的該類記憶體作為記憶體池子的大小

void* A::operator new(size_t size)
{
	A *tmplink;
	if (m_FreePosi == nullptr)
	{
		//為空,我要申請記憶體,要申請一大塊記憶體
		size_t realsize = m_sTrunkCout * size; //申請m_sTrunkCout這麼多倍的記憶體
		m_FreePosi = reinterpret_cast<A*>(new char[realsize]); //傳統new,調用的系統底層的malloc
		tmplink = m_FreePosi;

		//把配置設定出來的這一大塊記憶體(5小塊),彼此要鍊起來,供後續使用
		for (; tmplink != &m_FreePosi[m_sTrunkCout - 1]; ++tmplink)
		{
			tmplink->next = tmplink + 1;
		}
		tmplink->next = nullptr;
		++m_iMallocCount;
	}
	tmplink = m_FreePosi;
	m_FreePosi = m_FreePosi->next;
	++m_iCout;
	return tmplink;
}

void A::operator delete(void *phead)
{
	(static_cast<A*>(phead))->next = m_FreePosi;
	m_FreePosi = static_cast<A*>(phead);
}


int main()
{
	clock_t start, end; //包含頭檔案 #include <ctime>
	start = clock();
	//for (int i = 0; i < 500'0000; i++)
	for (int i = 0; i < 15; i++)
	{
		A *pa = new A();
		printf("%p\n", pa);
	}
	end = clock();
	cout << "申請配置設定記憶體的次數為:" << A::m_iCout << " 實際malloc的次數為:" << A::m_iMallocCount << " 用時(毫秒): " << end - start << endl;
	return 0;
}
           

2.8 嵌入式指針

在記憶體池中,每産生一個新的對象就需要多消耗四個位元組來存放next指針,有點浪費,引入了嵌入式指針後可以解決這個問題。嵌入式指針就是利用對象的前四個位元組來存放這個next指針,是以使用嵌入式指針必須記憶體大于四個位元組(在X86平台下一個指針占用的記憶體是4個位元組)

EG:

#include<iostream>
using namespace std;

class TestEP
{
public:
	int m_i;
	int m_j;

public:
	struct obj //結構體
	{
		//成員,是個指針
		struct obj *next;  //這個next就是個嵌入式指針
	};
};

int main()
{
	TestEP mytest;
	cout << sizeof(mytest) << endl; //8
	TestEP::obj *ptemp;  //定義一個指針
	ptemp = (TestEP::obj *)&mytest; //把對象mytest首位址給了這個指針ptemp,這個指針ptemp指向對象mytest首位址;
	cout << sizeof(ptemp->next) << endl; //4
	cout << sizeof(TestEP::obj) << endl; //4
	ptemp->next = nullptr;

	return 0;
}
           

2.9 使用嵌入式指針版本的記憶體池

EG:

//專門的記憶體池類
	class myallocator //必須保證應用本類的類的sizeof()不少于4位元組;否則會崩潰或者報錯;
	{
	public:
		//配置設定記憶體接口
		void *allocate(size_t size)
		{
			obj *tmplink;
			if (m_FreePosi == nullptr)
			{
				//為空,我要申請記憶體,要申請一大塊記憶體
				size_t realsize = m_sTrunkCout * size; //申請m_sTrunkCout這麼多倍的記憶體
				m_FreePosi = (obj *)malloc(realsize);
				tmplink = m_FreePosi;

				//把配置設定出來的這一大塊記憶體(5小塊),彼此用鍊起來,供後續使用
				for (int i = 0; i < m_sTrunkCout - 1; ++i) //0--3
				{
					tmplink->next = (obj *)((char *)tmplink + size);
					tmplink = tmplink->next;
				} //end for
				tmplink->next = nullptr;
			} //end if
			tmplink = m_FreePosi;
			m_FreePosi = m_FreePosi->next;
			return tmplink;
		}
		//釋放記憶體接口
		void deallocate(void *phead)
		{
			((obj *)phead)->next = m_FreePosi;
			m_FreePosi = (obj *)phead;
		}
	private:
		//寫在類内的結構,這樣隻讓其在類内使用
		struct obj
		{
			struct obj *next; //這個next就是個嵌入式指針
		};
		int m_sTrunkCout = 5;//一次配置設定5倍的該類記憶體作為記憶體池子的大小
		obj* m_FreePosi = nullptr;
	};
           

3 臨時對象

3.1 臨時對象的銷毀帶來的坑

如果産生的臨時對象沒有變量接住的話,那麼産生的臨時對象在本行語句結束後會被銷毀

EG:

#include<iostream>
using namespace std;



int main()
{
	const char *p1 = (string("123") + string("45")).c_str();
	printf("p1 = %s\n", p1);  //由于(string("123") + string("45"))産生的臨時對象在執行上行語句後被銷毀了,是以列印的結果不是 "12345"

	string str = (string("123") + string("45")).c_str();
	const char *p2 = str.c_str();
	printf("p2 = %s\n", p2);  //列印 "12345"

	return 0;
}
           

繼續閱讀