【C语言】动态内存管理

文章目录

  • 一、为什么存在动态内存分配
  • 二、动态内存开辟函数
    • 1、malloc
    • 2、free
    • 3、calloc
    • 4、realloc
  • 三、常见的动态内存错误
    • 1、对NULL指针的解引用操作
    • 2、对动态开辟空间的越界访问
    • 3、使用free释放非动态开辟的空间
    • 4、使用free释放一块动态内存的一部分
    • 5、对同一块动态内存多次释放
    • 6、动态内存忘记释放(内存泄漏)
  • 四、经典笔试题练习
    • 1、笔试题1
    • 2、笔试题2
    • 3、笔试题3
    • 4、笔试题4
  • 五、C/C++程序的内存开辟
  • 六、柔性数组
    • 1、什么是柔性数组
    • 2、柔性数组成员的定义
    • 3、柔性数组的使用
    • 4、柔性数组的优势

一、为什么存在动态内存分配

C语言学习到现在,我们已经掌握和了解到的内存开辟方式是通过数据类型来定义变量,然后操作系统在栈区、静态区或者字符常量区上为该变量分配空间,例如:

int a;         //在栈区上为 a 变量分配4个字节的空间
char arr[10];  //在栈区上为 arr 变量分配10个字节的空间
static int c;  //在静态区上为 a 变量分配4个字节的空间
char* p = "abcdef";  //在栈区上为 p 变量分配4/8个字节的空间,在字符常量区上为常量字符串分配空间

但是上述的开辟空间的方式有两个特点:

  1. 空间开辟大小是固定的;
  2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配;

但是对于空间的需求,不仅仅是上述的情况;有时候我们需要的空间大小在程序运行的时候才能知道, 那数组编译时开辟空间的方式就不能我们的需求了,所以C语言有了动态内存开辟(动态开辟的空间都是在堆区上的)。


二、动态内存开辟函数

1、malloc

函数功能

向内存申请一块连续可用的空间,并返回指向这块空间的指针。

函数参数

void* malloc (size_t size);
# void* 函数返回值,申请成功返回指向开辟的空间的指针,申请失败则返回NULL;
# size_t size 参数,指定要开辟多少个字节的空间;

函数使用

#include 
#include   //动态内存管理对应头文件
#include   //strerror对应头文件
#include    //errno对应头文件

int main()
{
	//申请40个字节的空间,交由指针变量p来管理
	int* p = (int*)malloc(10 * sizeof(int));
	//malloc申请空间可能会失败,所以要进行判断
	//申请失败:打印错误信息并退出
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//申请成功:使用
	for (int i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//使用完:释放
	free(p);   //释放动态内存开辟的空间
	p = NULL;  //将p置空,防止野指针
}

【C语言】动态内存管理_第1张图片

注意事项

  1. malloc 如果开辟成功,则返回一个指向成功开辟空间的指针;如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;

  2. malloc 的返回值类型是 void* ,因为 malloc 函数并不知道需要开辟的空间的类型,所以我们在具体使用的时候需要进行一下强转;

  3. 如果给 malloc 的第二个参数 size 传一个0,这种行为是标准是未定义的,取决于编译器;

2、free

我们前面提到,动态内存空间的开辟都是在堆区的,在堆区上开辟的空间有一个特点,那就是堆区上的空间使用完之后不会自己主动释放,而是设计了一个释放动态内存的函数:free,需要程序员主动调用这个函数来释放空间;

当然,当我们关闭整个程序的时候,操作系统是会自动回收动态开辟的内存的(这就是为什么有的电脑故障关机重启之后问题就解决了);但是,在一些公司的大项目中,有的程序是需要7*24小时运行的,就比如腾讯云和阿里云的云服务器;

而一旦我们使用动态内存开辟的函数,比如malloc、realloc、calloc 开辟空间使用完忘记释放时,就会造成内存泄露(相当于你向内存申请了一块空间,但是你使用完之后不归还,这样别人也用不了这块空间了,虽然这块空间还存在,但是相当于没有了),这是我们就会发现,随着程序的持续运行,可供我们使用的内存会变得越来越少;

内存泄露是我们进行动态内存管理是最容易犯的错误,需要大家高度重视。

函数功能

用来释放动态开辟的内存。

函数参数

void free (void* ptr);
# void* ptr 你要释放的空间的起始地址;

函数使用

在上面 malloc 函数的使用中我们已经演示了,将 p 的地址传递给 free 函数即可。

注意事项

  1. 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的;
  2. 如果参数 ptr 是NULL指针,则函数什么都不做;

3、calloc

函数功能

calloc 函数的功能和 malloc 十分相似,都是向堆区申请一块空间并返回空间的起始地址,但是 calloc 函数比 malloc 函数多了一个操作,那就是会将申请的空间里面数据全部初始化为0。

函数参数

void* calloc (size_t num, size_t size);
# void* 函数返回值,申请成功返回动态开辟的空间的起始地址,申请失败则返回NULL;
# size_t num 函数参数,用于指定要申请的元素个数:
# size_t size 函数参数,用于指定每一个元素的大小(字节为单位);

函数使用

#include 
#include   //动态内存管理对应头文件
#include   //strerror对应头文件
#include    //errno对应头文件

int main()
{
	//申请40个字节的空间,交由指针变量p来管理
	int* p = (int*)calloc(10, sizeof(int));
	//calloc申请空间可能会失败,所以要进行判断
	//申请失败:打印错误信息并退出
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//申请成功:使用
	for (int i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//使用完:释放
	free(p);   //释放动态内存开辟的空间
	p = NULL;  //将p置空,防止野指针
}

【C语言】动态内存管理_第2张图片

4、realloc

函数功能

调整已开辟的动态空间的大小。

函数参数

void* realloc(void* ptr, size_t size);
# void* 函数返回值,开辟成功返回动态开辟的空间的起始地址,开辟失败则返回NULL;
# void* ptr 函数参数,表示要调整的空间的起始地址;
# size_t size 函数参数,新的空间的大小;

函数使用

#include 
#include   //动态内存管理对应头文件
#include   //strerror对应头文件
#include    //errno对应头文件

int main()
{
	//先开辟一块空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//扩容
	//由于realloc可能会开辟失败,为了防止p指向realloc开辟失败的空间,从而丢失原来空间的情况,这里我们使用临时变量接受realloc的返回值
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	//申请失败:打印错误信息并退出
	if (ptr == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//申请成功:让p指向该空间并使用
    p = ptr;
	for (int i = 0; i < 20; i++)
	{
		p[i] = i;
	}
	for (int i = 0; i < 20; i++)
	{
		printf("%d ", p[i]);
	}
	//使用完:释放
	free(p);   //释放动态内存开辟的空间
	p = NULL;  //将p置空,防止野指针
}

【C语言】动态内存管理_第3张图片

注意事项

  1. realloc函数的出现让动态内存管理更加灵活;

  2. 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整;realloc 函数就可以做到对动态开辟内存大小的调整;

  3. realloc 函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间;

  4. 当 realloc 函数的第一个参数为NULL时,realloc 当作 malloc 函数使用;

  5. realloc在调整内存空间的时候存在两种情况:

    情况1:原有空间的后面有足够大的空间,可以让我们申请。这时扩展内存就在原有内存之后直接追加空间,原来空间的数据不发生变化。

    情况2:原有空间的后面没有足够大的空间让我们申请。这时 realloc 函数会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址;

    所以我们在使用 realloc 函数的时候不要直接将重新调整的空间地址直接赋值给源空间地址,而是应该先进行空指针判断,避免开辟失败的同时还将源空间搞丢,造成内存泄漏;【C语言】动态内存管理_第4张图片


三、常见的动态内存错误

1、对NULL指针的解引用操作

上面我们提到,malloc、calloc、realloc 这些函数向内存申请空间是有可能会失败的,申请失败函数就会返回空指针,如果我们不对函数的返回值进行判断,而直接对其解引用的话,就会造成程序崩溃;例如:

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

【C语言】动态内存管理_第5张图片

解决办法:在使用动态内存管理函数申请动态内存时,一定要记得检查函数的返回值是否为空。

2、对动态开辟空间的越界访问

这种情况和数组的越界访问十分相似,我们直接看示例:

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for (i = 0; i <= 10; i++)  
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
}

【C语言】动态内存管理_第6张图片

3、使用free释放非动态开辟的空间

free 函数是专门用于释放动态开辟的空间的,如果对非动态开辟的空间进行 free 操作,会造成程序崩溃,示例:

void test()
{
	int a = 10;
	int* p = &a;  //在栈区上开辟空间
	free(p);
}

【C语言】动态内存管理_第7张图片

4、使用free释放一块动态内存的一部分

当我们成功开辟一块动态空间并将它交由一个指针变量来管理时,我们可能会在后面的程序中让该指针变量自增,从而让其不再指向该动态空间的起始位置,而是指向中间位置或者结尾,这时我们在对其进行free操作时,也会导致程序崩溃,因为free函数必须释放一整块动态内存,而不能释放它的一部分。示例如下:

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for (i = 0; i < 5; i++)
	{
		*p = i;
		p++;  //指针变量p自增导致其丢失动态内存的起始地址
	}
	free(p);
}

【C语言】动态内存管理_第8张图片

解决办法:将申请的动态内存交由两个指针变量进行管理,其中一个用于各种操作,另外一个用于记录空间的起始地址。

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

我们在写程序的时候可能在程序中的某一位置已经对动态内存进行释放了,但是随着后面代码的展开,我们可能忘记了而重复对一块动态内存进行释放。示例如下:

void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		exit(-1);
	}
	free(p);

	//.......

	free(p);//重复释放
}

【C语言】动态内存管理_第9张图片

解决办法:每次free掉一块动态内存时,都将相应的指针变量置空,这样即使后面重复释放,free(NULL) 也没有任何影响。

6、动态内存忘记释放(内存泄漏)

在讲解free函数的时候我们已经说过了内存泄漏的原因以及危害,对于内存泄露这个问题,可以说是防不胜防,我们只能谨慎的写好每一行代码,最大程度上避免内存泄漏。下面我举一个可能造成内存泄漏的经典案例:

void test()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		exit(-1);
	}
	int flag = 0;
	scanf("%d", &flag);
	if (flag == 2)
	{
		//...... --程序逻辑
		return;  //内存泄漏
	}
	else
	{
		//...... --程序逻辑
	}
	free(p);
	p = NULL;
}

我们发现,代码编写者以及十分注意内存泄露的问题了,在test函数的末尾对动态开辟的空间进行了释放,还把指针变量p置为了空,但是这个函数还是可能会造成内存泄露,因为当函数从flag == 2 的路径返回时,test函数不会对该空间进行释放,所以说,内存泄漏真的是防不胜防。


四、经典笔试题练习

执行下面的Test函数,分析会输出上面结果以及代码中存在什么问题。

1、笔试题1

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(str);  
	strcpy(str, "hello world");
	printf(str);  //将str的首地址传给printf函数,可行
}

分析:

上面这段程序有三个问题:

第一:在Test函数中调用GetMemory函数时,传递的是str的值,所以GetMemory函数的参数p只是str的一份临时拷贝,与str没有任何关系,将动态开辟的100个字节交由指针p管理并不会对str产生任何影响;而且就算传递的是str的地址,malloc函数申请空间也有可能失败;

第二:由于GetMemory函数并没有能让str获得空间,所以str仍为NULL,这时调用strcpy函数会导致程序错误;

第三:代码中并没有对动态开辟的100个字节空间进行free,会导致内存泄漏;

2、笔试题2

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

void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

分析:

上面的程序有两个问题:

第一:在GetMemory函数中,p是一个数组,是在栈区上开辟空间,而不是在堆区上动态开辟的,所以当GetMemory函数被调用完毕后其函数栈帧会立即销毁,所以GetMemory函数并不能使str指向一块可用内存;

第二:GetMemory返回了p的地址,并将其赋值给了str,由于GetMemory函数调用完毕后其函数栈帧销毁,所以原本属于p的那块空间现在已经不能使用了,而str保存了p那块空间的起始地址并在printf函数中进行了访问,就造成了野指针问题;

3、笔试题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);
}

分析:

上面程序有两个问题:

第一:虽然这里把str的地址传递给了GetMemory函数,让其指向了一块动态开辟的空间,但是这里没有对malloc函数的返回值进行检查,当malloc失败的时候还是会产生空指针问题;

第二:程序中没有对malloc的空间进行free,造成了内存泄漏;

4、笔试题4

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

分析:

什么程序存在两个问题:

第一:没有对malloc的返回值进行空指针检查,使得strcpy函数可能执行失败;

第二:在free掉动态开辟的内存之后没有把相应的指针变量置空,导致if条件成立,造成野指针问题;


五、C/C++程序的内存开辟

前面我们一直说C语言的内存空间一共分为栈区、堆区、静态区这三个区域,其实这只是简略的说法,内存空间的具体划分如下:(注意:这里我们只需要了解即可,关于内存空间划分的知识我们会在操作系统模块进行深入学习)【C语言】动态内存管理_第10张图片


六、柔性数组

1、什么是柔性数组

在C99标准中,结构体中的最后一个元素允许是未知大小的数组,这就是柔性数组;柔性数组的特点如下:

  1. 柔性数组只能在结构体中使用;
  2. 结构体中的柔性数组成员前面必须至少有一个其他成员;
  3. sizeof 计算含柔性数组成员的结构体大小时,不会包含柔性数组的大小;
  4. 包含柔性数组成员的结构体需要用 malloc 函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小;
  5. 当我们用为柔性数组成员开辟的空间大小不合适时,我们还可以使用 realloc 函数对其进行扩容,真正的做到 “柔性”;

例如:

struct S
{
	int i;
	int a[];  //柔性数组成员
};

int main()
{
	printf("%d\n", sizeof(struct S));//输出的是4
	return 0;
}

【C语言】动态内存管理_第11张图片

2、柔性数组成员的定义

柔性数组成员的定义方式有两种:一种是把数组大小定义为0,这种定义方式在某些编译器下会报错;

struct S
{
	int n;
	int arr[0];  //柔性数组成员,把数组大小指定为0
};

另一种方式是直接不指定柔性数组的大小,这种方式比较通用,在支持C99标准的编译器下都不会报错;

struct S
{
	int n;
	int arr[];  //柔性数组成员,直接不指定数组大小
};

3、柔性数组的使用

我们在上面柔性数组的特点里面已经提到,包含柔性数组成员的结构体需要在堆区上开辟空间,其实这里开辟出来的空间会被分为两部分:一部分分配给结构体中普通成员变量使用,剩余的部分全部分配给柔性数组成员变量使用;

struct S
{
	int n;
	int arr[];  //柔性数组成员
};

int main()
{
	//开辟
	//malloc 中加号前面的部分为结构体普通成员开辟空间,加号后面部分为柔性数组成员开辟空间
	struct S* s = (struct S*)malloc(sizeof(struct S) + 40);
	if (s == NULL)
	{
		perror("malloc");  //perror函数,找到错误信息并打印
		return 1;
	}

	//扩容
	struct S* ptr = (struct S*)realloc(s, sizeof(struct S) + 80);
	if (ptr == NULL)
	{
		perror("malloc");  //perror函数,找到错误信息并打印
		return 1;
	}
	s = ptr;
    
	//使用
	s->n = 0;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		s->arr[i] = i;
		printf("%d ", s->arr[i]);
	}

	//销毁
	free(s);
	s = NULL;
	return 0;
}

【C语言】动态内存管理_第12张图片

4、柔性数组的优势

如果动态内存管理基础学的扎实的同学可能会发现,我们完全可以在结构体中定义一个int*类型的成员变量,然后为此变量开辟一块空间,也可以达到柔性数组的效果,即如下面代码所示:

struct S
{
	int n;
	int* arr;  //指针变量
};

int main()
{
	//开辟:由于含柔性数组的结构体的空间完全是在堆区上开辟的,所以为了最大程度上模拟其效果,这里我们也在堆区上开辟空间
	//为结构体开辟空间
	struct S* s = (struct S*)malloc(sizeof(struct S));
	if (s == NULL)
	{
		perror("malloc");  //perror函数,找到错误信息并打印
		return 1;
	}

	//让结构体中的int*变量指向另一块开辟的空间
	s->arr = (int*)malloc(20);
	if (s->arr == NULL)
	{
		perror("malloc");
		return 1;
	}
	//扩容为int*指向的空间
	int* ptr = (int*)realloc(s->arr, 40);
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	s->arr = ptr;

	//使用
	s->n = 0;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		s->arr[i] = i;
		printf("%d ", s->arr[i]);
	}

	//销毁:这里注意要开辟的两块空间全部销毁掉
	free(s->arr);
	free(s);
	s = NULL;
	return 0;
}

【C语言】动态内存管理_第13张图片

我们可以看到,上面的方法也可以实现柔性数组的效果,那为什么还要存在柔性数组呢?其实是因为柔性数组有着如下优势:

  1. 方便内存释放。对于第二种代码,如果我们的代码是在一个给别人用的函数中,我们在里面做了二次内存分配,并把整个结构体返回给用户;用户调用 free 函数可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,我们不能指望用户来发现这个事;所以,如果我们像第一种代码那样,把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉,很大程度上避免了内存泄露。
  2. 有利于提高访问速度。第二种方法我们用了两次 malloc 函数,这会一定程度上导致内存碎片;而第一种我们只使用了一次 malloc 函数,提高了内存的连续性;连续的内存有利于提高访问速度。

扩展阅读:C语言结构体里的成员数组和指针


你可能感兴趣的:(C语言进阶,c语言,动态规划)