动态内存管理详解&柔性数组

        本篇将详细的介绍在C语言中的动态内存管理,其中包括为什么要有动态内存分配,已经对应的动态内存函数:malloc、realloc、calloc以及free,这些函数的作用以及这些函数的用法都会详细给出。还会提出在常见的动态内存错误。给出一些和动态内存相关的试题。然后还会介绍柔性数组的概念及用法,以及使用柔性数组的优势。

        可提高旁边的目录快速找到自己想看的部分。

1.为什么会有动态内存分配

        除了动态内存的分配,我们还有直接分配内存,有直接申请一个字节(char),4个字节(int)……,还有分配一块内存——数组,如下:

#include 

int main() {
	//变量类型分配空间
	int n = 0;
	char c = 'q'; 
	//数组分配空间
	int arr[10] = { 0 };
	return 0;
}

        但是对于这样的内存分配,存在一个缺陷,那就是分配的内存已经固定死了,当我想要给这个数组(变量)在多分配一些内存时,我们只能通过在源代码中修改,在编译时并不能修改 。

即: 空间开辟的大小是固定的。

        数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小就不能在调整。

        所以在C语言中引入了动态内存开辟,让我们在编译时也可以申请更多的空间。

2.动态内存分配函数 

2.1malloc

        首先分析的是这个malloc函数,给出cplusplus网站对于该函数的解释:

动态内存管理详解&柔性数组_第1张图片

        由上图可知,使用malloc函数需要包含头文件stdlib.h,其中引入的参数为size_t类型的参数,表明需要申请空间的大小,单位为字节,所以malloc函数的作用就是申请分配内存。malloc函数的用法和注意事项有以下几个点:

        1.如果内存开辟成功,那么会返回一共指向开辟好空间的一个指针;

        2.如果开辟失败,则返回一个NULL指针,所以对于malloc函数的返回值,我们需要做检查;

        3.返回值的类型为void*,表明malloc函数并不知道开辟空间的类型,在具体使用时,由程序员自己来决定(强制类型转换);

        4.如果size为0,malloc的行为是标准未定义的,取决于编译器。

以上的几点,将在下面详细解释:

1.开辟成功,返回一个指向开辟好空间的一个指针(地址):

动态内存管理详解&柔性数组_第2张图片

        由上图所示,我们先将指针赋值NULL,然后在用malloc函数申请内存空间之后,将分配好的内存空间的首地址赋值给指针变量。

2.开辟失败,返回NULL

动态内存管理详解&柔性数组_第3张图片

        当我们在内存中一下申请很多字节的内存时,内存分配不了这么多,返回一个空指针。所以我们在动态内存的分配中,很有可能分配的内存失败,所以我们一般还是需要分配的动态内存进行检查,检查分配的空间是否成功,防止我们对NULL指针的使用。

3.void*指针变量,强制转化。

        对于上述对于malloc函数的说明可知,malloc返回的指针类型为void*类型,对于void*类型的指针变量,我们既不可以引用也不可以解引用,所以需要将这个指针类型进行强制转换,才可以进行使用,如下:

动态内存管理详解&柔性数组_第4张图片

2.2free 

        对于上述已经从动态内存中分配的空间,我们申请了,当然也需要将内存还回去,这时候我们就需要用free函数将分配好的内存空间进行释放。

        在对free函数的讲解前,我们先来看看动态分配的内存空间是从哪里分配的空间,如下图:  

动态内存管理详解&柔性数组_第5张图片

        如上图所示,内存空间一共分为栈区、堆区以及静态区,其中对于栈区,存储局部变量和函数参数,堆区则存储malloc、calloc、realloc申请的内存,静态区存储全局变量和静态变量。

        当程序结束时,操作系统会回收整个进程所占用的内存,包括申请在堆区的内存(也就是malloc、realloc、calloc函数的申请的空间),因此申请的内存会被自动释放回收。

        但是尽管在程序结束时会自动释放堆区内存,但是也存在内存泄漏的情况,申请的内存可能一直存在于栈区中,导致内存资源的浪费。

        所以为了防止内存泄漏的问题,我们最好使用free函数来手动释放申请的内存,并且将被释放的指针变量置为NULL。

        以下为free函数的详细解释:

动态内存管理详解&柔性数组_第6张图片

        free函数的返回值为void,函数参数为void*,即可以为任意类型的指针变量。但是要注意,free释放的为动态开辟的内存,如果释放的指针变量指向的内存不是动态开辟的,那么对于free函数的行为是未定义的。当free释放的指针为NULL指针,那么free函数什么都不用做。 

动态内存管理详解&柔性数组_第7张图片

        如上所示,当我们释放了ptr的内存,但是该指针还是指向原来的地址,说明该指针还能找到那个源地址,但是源地址的信息被释放了, 那么目前这个ptr指针就是一个野指针,所以我们最好将其置为NULL

3.calloc

        接下来我们讲解这个calloc函数,给出cplusplus官网的解释:

动态内存管理详解&柔性数组_第8张图片

        该函数返回值同样为void*,函数参数size为每个元素的大小,num为元素的个数,所以分配的内存空间为num*size个字节,分配完之后,将所有字节都初始化为0。这个函数的作用和malloc函数其实非常相似,不同的是,malloc函数并不会将分配的内存初始化。以下总结calloc函数的要点:

        1.函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节初始化为0。

        2.与函数malloc的区别只在于calloc会返回地址前把申请空间的每个字初始化为全0。

动态内存管理详解&柔性数组_第9张图片

        由上图所示,打印出来的值都是0。 

2.4realloc

        以下给出realloc函数在cplusplus网站的定义:

动态内存管理详解&柔性数组_第10张图片

        由上图所示,所得realloc函数的介绍及用法:

        realloc函数的作用是将原动态分配的内存再次重新分配内存,可以将内存变小也可以变大。realloc函数返回的类型仍为void*,没有具体要求返回的类型,说明和以上的内存分配函数基本一致,返回类型利用强制转换由自己决定。以下将详细讲解realloc函数的要点与细节。

        1.传入参数之一ptr:是要调整内存的地址;

        2.size为调整之后的新大小;

        3.返回值为调整后的内存起始位置;

        4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新分配的内存空间;

        5.realloc在调整内存空间时存在两种情况:情况一:原空间之后存在足够大的空间、情况二:原空间之后没有足够大的空间。

给出以下realloc函数的实例应用:

动态内存管理详解&柔性数组_第11张图片

        如上图的两种的使用realloc函数的实例,第一种是使用一个新的指针接收realloc函数返回的参数,但是对于第二种realloc函数用法,是使用原ptr接收来自realloc函数返回的参数。注意:只有第一种用法是对的,对于第二种用法存在安全隐患,用原指针接收新的地址存在的安全隐患就是:当realloc函数申请内存失败之后返回的指针为NULL,但是这时修改了原指针,所以原指针指向的内存地址也没有了,也就是即可能会申请失败,还会导致原地址内容丢失。 

        对于以上的解释,当realloc返回的函数为失败时,返回的内容为NULL。但是当分配成功时,一共存在两种情况,如下:

动态内存管理详解&柔性数组_第12张图片

        如上所示,一共有两种分配内存的方式,第一种:当原地址后面分配的地址不够分配的新地址长度时,会重新分配一块地址内存,赋值给指针,同时会把原地址的内容拷贝过来。第二种: 当原地址后面分配的地址够分配的新地址长度时,会直接在原地址内存的后面分配新的内存

        其实对于realloc函数,也可以使用成malloc函数,也就是,realloc函数的功能包含malloc函数的功能,如下:

int main() {
	int* p = (int*)realloc(NULL, 40);
	if (p == NULL) {
		printf("%s\n", strerror(errno);
		return 1;
	}
    free(p);
    p=NULL;
	return 0;
}

        将原地址置为NULL,那么在使用realloc函数时,会直接在内存中随机分配内存,然后分配给p指针。 

3.常见的动态内存错误

        以下举出的例子只是举出一些较为常见的例子,但仍然存在许多的错误情况,在此不做出一一列举。

3.1对NULL指针的解引用操作

void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
}

        如上代码,申请的空间为整型最大值的四分之一,分配的内存空间过大,很可能分配失败,返回NULL指针,此时p指向的地址为NULL,*p——对NULL指针的解引用,错误的写法。

3.2对动态内存空间的越界访问

void test() {
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL) {
		exit(1);
	}
	for (i = 0; i <= 10; i++) {
		*(p + i) = i;
	}
	free(p);
}

动态内存管理详解&柔性数组_第13张图片

        对以上函数的调用,存在越界访问的问题,访问到了第十一个整型的位置,而初始时,只分配了10个整型的空间。

 4.3对非动态开辟的内存使用free释放

void test() {
	int a = 10;
	int* p = &a;
	free(p);
}

        直接释放局部变量,会导致程序崩溃。 

4.4使用free释放一块动态开辟内存的一部分

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

        对于此情况,我们将动态开辟内存所指向的指针+1,导致在p为起始位置的往后100个字节,有4个字节不是动态开辟的内存,会导致程序崩溃。

4.5对同一块动态内存多次释放

void test() {
	int* p = (int*)malloc(100);
	free(p);
	//...一系列操作
	free(p);
}

        在写程序时,很可能因为忘记进行了什么样的操作之后,继续进行了该操作,当free多次同一块地址时,第二次free时,free的就不在是动态开辟的内存了,所以会导致程序崩溃。

4.6动态开辟内存忘记释放(内存泄露)

void test() {
	int* p = (int*)malloc(100);
	if (p != NULL) {
		*p = 20;
		//...一系列操作
	}
}

        如上,对分配的内存进行一系列操作之后,并未进行释放空间,虽然程序并不会报错,但这很可能导致内存泄露。 注意:对于所有动态内存分配的内存,在结束程序前都要释放内存,防止内存泄漏

4.动态内存经典试题

        以下将详细解析有关动态内存的相关题目,加深对以上知识的理解。

4.1 problem-1

        检查以下代码存在什么问题?

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

        该函数表面上是一个给str指针分配内存,然后将hello world拷贝到已经分配好的内存之中,然后打印出hello world。但是对于该函数真的可以运行吗?以下是运行结果:

动态内存管理详解&柔性数组_第14张图片

        对于该test函数,运行出的结果并没有打印出hello world,存在以下几个问题:

        1.当我们将指针str传入GerMemory函数时,GerMemory只是将str的值拷贝了一份,并没有拿到str指针的地址,对于拷贝的值分配内存后,当离开这个函数,这个分配的内存和拷贝的值也已经找不到了。(简单来说就是GerMemory函数的参数是一级指针,应该用二级指针来接收一级指针的地址,所以GerMemory函数的参数采用的实际是传值,而不是传址)。

        2.既然str没有拿到内存空间,那么strcpy就是对NULL指针的解引用,程序崩溃。

        3.对于准备接收的动态内存在结束时并没有释放,以及将该指针置为NULL。

        4.没有对动态分配的内存进行判断是否内存分配成功。

将代码修改为以下代码即可成功:

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
    if(*p==NULL){
        assert(*p); //断言,若为NULL,直接结束整个代码
    }
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	
	printf(str);
	free(str);
	str = NULL;
}

int main() {
	Test();
	return 0;
}

4.2 problem-2 

        检查以下代码存在什么问题? 

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

        该函数表面上的作用是用GetMemory函数生成一个字符数组,然后将字符数组的地址传给str,最后将str打印出来,以下是运行结果:

动态内存管理详解&柔性数组_第15张图片

        最后打出来的形式却是以乱码的形式打印出来。

        该函数存在的问题就是:GetMemory函数是在栈区开辟的空间(free部分有讲),当我们离开该函数的时候,栈区里的内容也已经被计算机所处理了,不在是原来的hello world值,而GetMemory还将p的地址返回,这是的p指针也已经是一个野指针了,所以对于一个野指针的打印,肯定会乱码。

4.3 problem-3

        以下代码存在什么问题?

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

        以上函数表面上看着已经不存在什么错误了,GetMemory的参数也是二级指针,对于strcpy中也没有对NULL指针的解引用,当我们运行时也确实可以打印出hello,但是真正的问题在于,没有对分配的内存进行判断是否分配成功,并且忽略了动态内存最后的处理:释放内存。

4.4 problem-4 

        以下代码存在什么问题? 

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

        起始对于该函数存在的问题是显而易见的:

        1.没有判断内存是否分配成功就用strcpy函数。

        2.free掉str的内存之后,又对str指针用strcpy函数拷贝world。

对于该函数的运行结果为: 

动态内存管理详解&柔性数组_第16张图片

        为什么会是这样的结果:首先对str指针分配的拷贝了一个hello字符串,然后又将str的内存free掉,但是此时str仍然指向的是原来分配的地址,因为free之后没有将str置为NULL,所以str不为NULL,进入判断语句,strcpy可以找到该地址,也可以拷贝,但是这是一种危险的行为,存在越界访问。 

5.柔性数组        

        对于柔性数组,顾名思义就是有弹性(柔性)的数组,这个数组的成员个数我们可以改变,但是对于柔性数组的格式也存在要求,如下:

struct soft_arr {
	int i;
	int a[];//柔性数组成员
	//也可以将a[ ]写成a[0]
};

        柔性数组的特点:

        1.结构体的柔性数组成员前面必须至少存在一个其他类型的成员。

        2.如果用sizeof返回这种结构体的大小,计算出的结果不包括柔性数组的成员。

        3. 包含柔性数组成员的结构用动态内存malloc(calloc、realloc)进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期⼤⼩。

动态内存管理详解&柔性数组_第17张图片

        如上图,计算出来的结果只有4个字节,相当于只计算了int类型的内存大小。

5.1柔性数组的使用

        对于柔性数组的初始化以及使用如下:

struct soft_arr {
	int i;
	int a[];
	//也可以将a[ ]写成a[0]
};

int main() {
	struct soft_arr* p = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 10 * sizeof(int));
	if (p == NULL) {
		perror("malloc:");
		return 1;
	}
	p->i = 0;
	int i = 0;
	for (i = 0; i < 10; i++) {
		*(p->a + i) = i;
	}
    //...other oprations
	//需要更多内存时
	struct soft_arr* tmp = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 15 * sizeof(int));
	if (tmp == NULL) {
		perror("realloc");
	}
	p = tmp;
	//...other oprations
	free(p);
	p = NULL;
	return 0;
}

        对于柔性数组的内存分配我们使用内存分配函数进行内存分配,首先我们需要将柔性数组原有的内存大小分配给指针p,然后多出来的内存分配其实就是对柔性数组的成员内存进行分配。当我们需要更多的内存时,我们也可以使用realloc函数调节内存。

5.2柔性数组的优势

        先给出两组代码,实现的功能一致,但是写法和原理存在区别:

        代码一:

struct soft_arr {
	int i;
	int a[];
	//也可以将a[ ]写成a[0]
};

int main() {
	struct soft_arr* p = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 10 * sizeof(int));
	if (p == NULL) {
		perror("malloc:");
		return 1;
	}
	p->i = 0;
	int i = 0;
	for (i = 0; i < 10; i++) {
		*(p->a + i) = i;
	}
    //...other oprations
	//需要更多内存时
	struct soft_arr* tmp = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 15 * sizeof(int));
	if (tmp == NULL) {
		perror("realloc");
	}
	p = tmp;
	//...other oprations
	free(p);
	p = NULL;
	return 0;
}

        代码二:

struct hard_arr {
	int i;
	int* a;
};

int main() {
	struct hard_arr* p = (struct hard_arr*)malloc(sizeof(struct hard_arr));
	if (p == NULL) {
		perror("malloc:");
		return 1;
	}
	p->i = 0;
	p->a = (int*)malloc(10 * sizeof(int));
	if (p->a == NULL) {
		perror("malloc:");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++) {
		*(p->a + i) = i;
	}
	//...other oprations
	//需要更多内存时
	int* tmp = (int*)malloc(sizeof(15 * sizeof(int)));
	if (tmp == NULL) {
		perror("realloc");
	}
	p = tmp;
	//...other oprations
	free(p->a);
	p->a = NULL;
	free(p);
	p = NULL;
	return 0;
}

        对于以上两个代码实现的功能一模一样,但是他们又存在什么区别呢?通过以下图例给出解释:

动态内存管理详解&柔性数组_第18张图片

        如上图所示,第一块是柔性数组分配的空间形式,第二块是代码二对应的内存分配方式。第一块是直接分配一块连续的内存空间。第二块则是将结构体先申请一块空间(包含整型 i ,指针*a),然后在对指针a随机分配一块新的内存空间。这两种内存分配方式,第一种相对于第二种一共有两个好处:

        1.方便内存释放,对于代码一我们只用了一次malloc函数,说明在释放时也只需要free一次,而对于代码二,我们分配了两次的内存,先分配结构体的内存,然后分配结构体内整型指针的内存,那么我们在释放内存的时候也要先将结构体内整型指针的内存释放,然后释放结构体的内存。        

        2.利于增加访问速度,连续的内存有益于提高访问的速度,同时也有益于减少内存碎片。

内存碎片:

动态内存管理详解&柔性数组_第19张图片

        在内存中用malloc分配的内存空间,会在内存中随机分配一块地址,地址与地址之间的小空间就是内存碎片,这些内存碎片通常很难在被利用,所以malloc次数越多,对于空间的利用率越小。

提升访问速度:

动态内存管理详解&柔性数组_第20张图片

        以上这个图是CPU在计算机上读取的顺序显示图,首先会从寄存器中读,寄存器没有则向下读取,以此类推,但是在读取数据时存在空间局部性,也就是读取到一个数据会接着沿着这个数据的周围读取数据,对于代码一中的数据类型,一次性就可以将所有的数据读取完。

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

        在c/c++中对于代码中对应变量的存储区域如下图

动态内存管理详解&柔性数组_第21张图片

        内核空间:该空间是留给计算机操作系统内核的,我们写出的代码以及其他需要内存的变量都不会存储在这。

        代码段:用于存储可执行代码,计算机在执行代码前需要将代码存储转化为二进制序列,其中二进制序列就是存储这一代码段,还有常量字符串这一类的只读常量。

        栈区:函数内局部变量的存储单元在栈区存储创建,当函数执行结束时,这些存储单元会自动被释放掉,还会存储函数参数、返回数据、返回地址等。

        堆区:用于存储动态开辟的内存变量,程序结束时,系统会自动回收内存,但也很有可能存在内存泄漏的问题。

        数据段:数据段就是静态区,可用于存储全局变量,以及静态数据(不管是全局的静态数据还是局部的静态变量,程序结束之后由系统自动释放掉。

         

你可能感兴趣的:(数据结构,c++,c语言)