C++对象构造语义学

|▌冷眼眸甩不掉的悲伤 提交于 2020-01-27 05:41:59

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;
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!