C/C++语言—动态内存管理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/z_x_m_m_q/article/details/82503092

C/C++中程序内存区域划分: 

C/C++程序内存区域划分

栈:又叫堆栈,存储非静态局部变量、函数参数、 返回值等,栈是可以向下生长的

内存映射区:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内 存,做进程间通信

堆:用于程序运行时动态内存分配,堆是可以向上增长的

数据段:存储全局数据和静态数据

代码段:存储可执行的代码、只读常量

 C/C++语言内存分配方式(不只是动态内存): 

 这篇博客我主要会写C/C++语言的动态内存管理,这里把其他分配方式也简单的提一下,以便我们都对这块先有个系统的认知

C/C++语言的内存分配方式:

  1. 从栈上分配:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是栈本身的容量有限
  2. 从堆上分配:这就是我们这里重点要说的动态内存分配,程序在运行的时候用malloc或new申请任意多少的内存空间,程序员自己负责在何时用free或delete释放内存.动态内存的生存期由用户决定,使用非常灵活,但问题也最多
  3. 从静态区上分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量

C语言中动态内存管理方式:

C语言中使用 malloc/calloc/relloc/free 进行动态内存管理

简单的使用:

void test()
{
	//用 malloc/calloc/realloc 分别开空间
	int* p1 = (int*)malloc(sizeof(int)*2);
	int* p2 = (int*)calloc(4,sizeof(int));
	int* p3 = (int*)realloc(p2,sizeof(int)*4);
	//释放空间
	//假设 realloc 开空间成功,这里只需释放 p1/p3 即可
	free(p1);
	free(p3);
}

malloc、calloc、realloc的使用:

 这三个函数都在 stdlib.h 函数库内,它们的标准声明分别为:

void* malloc(unsigned size); 

void* calloc(size_t numElements, size_t sizeOfElement); 

void* realloc(void* ptr, unsigned newsize); 

它们的返回值都是请求系统分配的地址,如果请求失败就返回NULL

malloc():参数size为需要内存空间的长度,该函数是在内存的动态存储区中分配一块长度为size字节的连续区域,返回该区域的首地址

calloc():与malloc相似,numElements为元素个数,参数sizeOfElement为申请地址的单位元素长度,即在内存中申请numElements*sizeOfElement字节大小的连续地址空间

relloc():给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度

malloc、calloc、realloc的区别:

  1. 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间已经被重新分配)可能会出现问题。
  2. 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零
  3. realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失。realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址。相反,realloc返回的指针很可能指向一个新的地址
  4. realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块,然后现存的数据就被拷贝至新的位置,而旧块则放回到堆上.

注意:三个函数返回类型都是void*类型。void*表示的是未确定类型的指针,我们无法直接使用开辟的空间。C/C++规定,void* 类型可以强制转换为任何其它类型的指针,转换成一个确定的类型之后才可以使用开辟的空间

用realloc函数扩容后申请的空间地址没变:

void test()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)realloc(p1, sizeof(int)*5);

        //用realloc函数扩容后申请的空间地址没变
	cout << &p1 << endl;  //00AFF914
	cout << &p1 << endl;  //00AFF914

	free(p2);
	p2 = NULL;
}

当然会有realloc函数扩容后申请的空间地址变的情形,这里不演示了。下面做两个题:

判断以下程序能不能执行:

void test()
{
        void* p = (void*)malloc(void);
        ++p;
}

这个程序一定是错的,参数为void代表没有参数,malloc函数不接受0个参数,所以不能执行下去

判断:void *p=(void *)malloc(0);这段代码会被编译过吗?

void test()
{
        void *p = (void *)malloc(0);
         //VS2017 下运行的结果
         cout << p << endl;  //00B60568
}

C99:如果所请求的空间大小为0,其行为由库的实现者定义:可以返回空指针,也可以让效果跟申请某个非0大小的空间一样,所不同的是返回的指针不可以被用来访问一个对象

malloc 和 free 的深度剖析:

malloc函数底层原理:

  1. 操作系统对分配内存的管理:操作系统通过哈希表管理malloc函数分配的内存段,哈希键值key共30个,下标从0到29,每个键值管理一份内存段,大小为(key+3)<<1个字节。不管malloc(size)函数的参数size有多大,实际分配给你的内存只能是这30个内存段中最匹配size的那个内存段。例如malloc(1)的size为1字节,与键值为0管理的那份内存段最为接近,所以实际分配的内存大小为(0+3)<<1 = 6个字节。
  2. 操作系统对堆空间的管理:malloc申请的空间是操作系统从堆里面分配给用户的,即函数返回的指针是指向堆里面一个区域的。操作系统有一个记录空闲内存地址的链表,当操作系统收到用户申请时,就会遍历该链表,找到第一个空间大于用户所申请的空间的结点,然后将该结点的空间分配给用户,并将这个结点从链表删除。

malloc函数使用的注意事项:

  1. 申请了内存后,检查是否申请成功
  2. .当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针赋成NULL,防止再次使用这个指针
  3. malloc、free应该是配对。如果申请后不释放就是内存泄露;释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)

free函数的底层原理:

free()函数原型以及使用非常简单,因为只有一个参数,使用时只要把申请的空间的指针给给它就行了。这个简单也是我们最为疑惑地地方,它凭一个指针是如何完成空间释放的,因为释放空间最起码需要知道从哪里开始释放,释放到哪里。下面我们重点看两个问题:第一个问题是给给free函数的指针是不是释放空间的起始地址,其次是free函数是如何知道释放空间的大小的

在《UNIX环境高级编程》第七章中有这么一段话:

上面这段话在这里对我们提出的问题的确是一个很好的提示, 但还是有些抽象。解决这里的问题,必须的从malloc函数说起,malloc()申请的空间实际上分了两个不同性质的块,一个是用来记录管理信息的,另一个就是用户的可用空间。而用来记录管理信息的实际上是一个结构体,该结构体定义的原型如下:

对于该结构体,毫无疑问的是size记录实际空间的大小(两个块),is_available 应该是一个标记,根据操作系统的定义,它处于不同值时标识空间的归属属性(分配给用户的还是系统的),现在来看free函数的源码:

从上面代码中可以看到的是首先将指针倒回到实际开辟空间的开头,所以给给free函数的指针不是释放空间的起始地址,free函数中将 is_available 置位1,根据操作系统的定义,可能标识了从free指针指向的空间开始将大小为size字节的空间归还给系统。

提示:这里可能有不对的地方,以后弄明白后再修改补充!!!

常见内存泄漏: 

void test()
{
//内存申请了,忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	assert(p1 != NULL);
	DoSomethig();

//程序逻辑不清,以为释放了,实际内存泄漏
	int* p2 = (int*)malloc(sizeof(int));
	int* p3 = (int*)malloc(sizeof(int));
	assert(p2 != NULL);
	assert(p3 != NULL);
	DoSomethig();
	p2 = p3;
	free(p2);
	free(p3);  //p2指向的空间没有释放

//程序误操作,将堆破坏
	char* p4 = (char*)malloc(sizeof(char)*5);
	assert(p4 != NULL);
	strcpy(p4,"Hello Word!");  //拷贝越界
	free(p4);

//释放时传入的地址和申请的地址不一致
	int* p5 = (int*)malloc(sizeof(int)*5);
	assert(p5 != NULL);
	++p5;
	DoSomethig();
	free(p5);
}

 C++中动态内存管理方式:

C语言动态内存管理方式在C++中可以继续使用,同时C++又提供了自己的内存管理方式:通过new和delete运算符进行动态内存管理

  • new/delete 动态管理对象
  • new[]/delete[] 动态管理对象数组

简单使用(注意代码里的注释):

void test()
{
//管理对象
    //动态分配4个字节(1个int)的空间
    int* p1 = new int;
    //动态分配4个字节(1个int)的空间,并初始化为0
    int* p2 = new int(0);

//管理对象数组
    //动态分配8个字节(2个int)的空间
    int* p3 = new int[2];

//一定要匹配使用,否则可能会出现内存泄漏甚至程序崩溃的问题
    delete p1;
    delete p2;
    delete[] p3;
}

malloc/free 和 new/delete比较:

void test()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)malloc(sizeof(int));

	int* p3 = new int;
	int* p4 = new int;

	int* p5 = new int[4];
	int* p6 = new int[4];

	delete p1;
	delete[] p2;

	free(p3);
	delete[] p4;

	free(p5);
	delete p6;
}

上述代码能通过编译吗?能正常运行吗?是否存在内存泄露?(以后详细作答)

new操作符/delete操作符说明:

猜你喜欢

转载自blog.csdn.net/z_x_m_m_q/article/details/82503092
今日推荐