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(地址) 类型相应的构造”:在这块地址上面执行构造函数,把空间变成一个类对象。

猜你喜欢

转载自blog.csdn.net/xuruhua/article/details/80744654