C语言动态内存管理【进阶--5--】

发布于:2024-12-18 ⋅ 阅读:(45) ⋅ 点赞:(0)



动态内存管理

请添加图片描述

一、作用即意义

  • 静态内存分配是在编译时确定的,而动态内存管理是在程序运行时动态的分配和释放内存

  • 动态内存管理允许程序根据需要分配和释放内存

  • 动态内存管理在处理大小未知的数据在程序执行过程中改变数据结构的情况中非常实用


已学的开辟内存空间的方式
  • 在栈空间上开辟4字节的空间:

    •   int i = 0;
      
  • 在栈空间上开辟10字节的连续空间:

    •   char arr[10] = { 0 };
      

以上的空间开辟方式有如下特点:

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

二、动态内存函数的介绍

主要函数
  1. malloc():分配指定大小的内存块

    void *malloc(size_t size);
    
    • malloc函数返回一个指向分配的内存块的指针
    • 如果分配失败,它会返回NULL
  2. calloc():分配指定数量和大小的内存块,并初始化为零

    void *calloc(size_t num, size_t size);
    
    • calloc函数返回一个指向分配的内存块的指针,所有字节都被初始化为零
    • 如果分配失败,它会返回NULL
  3. realloc():重新分配内存块的大小

    void *realloc(void *ptr, size_t size);
    
    • realloc函数可以增加或减少之前分配的内存块的大小

    • 如果ptrNULL,它的行为类似于malloc

    • 如果分配失败它会返回NULL,并且原始内存块保持不变

  4. free():释放之前分配的内存

    void free(void *ptr);
    
    • free函数释放之前通过malloccallocrealloc分配的内存
    • 释放后,指针ptr不再指向有效的内存

Ⅰ、malloc()函数、free()函数

  • malloc()定义在<stdlib.h>中用于动态内存分配的函数

函数原型:

void *malloc(size_t size);
  • size:需要分配的内存块的大小,单位是字节

返回值:

  • malloc函数返回一个指向分配的内存块的指针。如果分配成功,返回的指针指向一块至少为size字节的内存区域
  • 如果分配失败(通常是因为内存不足),malloc返回NULL
  • free()定义在<stdlib.h>中用于释放之前通过malloccallocrealloc分配的动态内存的函数

函数原型:

void free(void *ptr);
  • ptr:指向要释放的内存块的指针

功能:

  • free函数释放之前分配的内存块,使其重新成为可用内存,可以被后续的内存分配请求使用

注意事项:

  • 不应该多次释放同一块内存,多次释放同一块内存可能会导致程序崩溃或内存损坏
  • free(NULL)函数将不进行任何操作
  • 释放内存后,应该将指针设置为NULL,以避免野指针问题
  • free()只能释放动态开辟的空间
  • 访问或操作已经被free释放的内存,会导致未定义行为,致使程序崩溃

使用示例:

#include<errno.h>
#include<string.h>
#include<stdlib.h>

int main()
{
	//动态开辟内存
	int arr[10] = { 0 };//在栈区申请空间
	int *p = (int*)malloc(40);//malloc()在堆区申请空间
	if (p == NULL) 
	{
		printf("%s\n", strerror(errno));//如果开辟失败则打印错误信息,并返回1
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++) {
		*(p + i) = i + 1;
	}
	for (i = 0; i < 10; i++) {
		printf("%d ", *(p + i));
	}
  
  free(p);//释放内存
  p = NULL;//将指针设置为NULL,避免野指针
  
	return 0;
}
  • 当程序退出时,系统会自动回收内存空间

  • free函数需要与相应的内存分配器(如malloccallocrealloc)一起使用

  • 释放内存后,因及时将指针设置为NULL,避免野指针

  • int arr[10] = { 0 };是在 栈区(Stack) 申请空间,int *p = (int*)malloc(40); malloc()是在 堆区(Heap) 申请空间

Ⅱ、calloc()函数

  • 定义在<stdlib.h>中用于动态内存分配的函数
  • malloc()不同的是,calloc()在分配内存成功后,会将所有字节都初始化为0

函数原型:

void *calloc(size_t num, size_t size);
  • num:要分配的元素数量
  • size:每个元素的大小,单位是字节

返回值:

  • calloc函数返回一个指向分配的内存块的指针
    • 如果分配成功,返回的指针指向一块至少为num * size字节的内存区域,并且所有字节都被初始化为0
    • 如果分配失败(通常是因为内存不足),calloc()将返回NULL

使用示例:

#include<stdlib.h>

int main()
{
	int* p = (int*)calloc(10, sizeof(int));

	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
  
  free(p);//释放内存
  p = NULL;//将指针设置为NULL,避免野指针
  
	return 0;
}
  • 输出结果:0 0 0 0 0 0 0 0 0 0

可以这样认为 : calloc() = malloc() + memset()

Ⅲ、realloc()函数

  • 定义在<stdlib.h>中用于动态内存分配的函数

  • realloc()函数可以在不丢失原有数据的前提下,增加或减少内存块的大小

函数原型:

void *realloc(void *ptr, size_t new_size);
  • ptr:指向之前分配的内存块的指针
    • 如果ptrNULL,则realloc的行为类似于malloc,即分配一个新的内存块
  • new_size:新的内存块大小,单位是字节

返回值:

  • realloc函数返回一个指向调整大小后的内存块的指针
    • 如果调整大小成功,返回新内存块的首地址
    • 如果调整大小失败(通常是因为内存不足),realloc返回NULL(在这种情况下,原始的内存块保持不变,不会被释放或修改)
    • 因此不能用原指针来接收realloc()函数返回的首地址
  • 如果ptrNULLrealloc将分配一个新的内存块,并返回指向它的指针

请添加图片描述

  • 原始p指针ptr指针

如果realloc()之后能够在原有的内存块附近找到足够的空间来扩展内存,它会返回原来的指针(即p

如果不能,它会在堆上找到足够大的新内存块,将原有数据复制过去,并返回新内存块的地址(即ptr

  • 确定pptr是否指向同一块内存的步骤:
    1. 比较指针地址:在调用realloc之后,检查返回的指针ptr是否与原始指针p相等
    2. 处理不同情况
      • 如果ptr等于p,说明realloc在原有内存块旁边找到了足够的空间,没有分配新的内存块,因此不需要复制数据
        • 此时释放内存只需释放p,因为pptr指向同一块内存
      • 如果ptr不等于p,说明realloc分配了新的内存块,需要将原有数据复制到新的内存块,并释放原始内存块

使用示例:

int main()
{
	//动态开辟内存
	int *p = (int*)malloc(40);//malloc()在堆区申请空间
	if (p == NULL) 
	{
		printf("%s\n", strerror(errno));//如果开辟失败则打印错误信息,并返回1
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)//初始化申请的内存
	{
		*(p + i) = i + 1;
	}
	//使用realloc扩容
	int* ptr = (int*)realloc(p, 80);
	if (ptr == NULL) {
		// 如果扩容失败,释放原始内存并返回错误
		free(p);
		printf("%s\n", strerror(errno));
		return 1;
	}
	if (p == ptr)//如果能够在原有的内存块附近找到足够的空间来扩展内存,返回原来的指针
	{

	}
	else {
		p = ptr; // 更新p指向扩容后的内存
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	free(p);//p = ptr因此只需释放p
	p = NULL;
	return 0;
}

三、常见的动态内存错误

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

int main()
{
	int* p = (int*)malloc(sizeof(int));
	*p = 10;
	return 0;
}
  • 如果malloc()开辟动态内存失败,则会返回空指针(NULL),此时对p解引用(*p)就会发生错误

  • 在使用时应加上判断:

    •   int main()
        {
        	int* p = (int*)malloc(sizeof(int));
        	if (p == NULL)
        		return 1;
        	*p = 10;
          
          free(p);
          p=NULL;
        	return 0;
        }
      

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

越界访问的例子:

#include <stdio.h>
#include <stdlib.h>

int main() 
{
	int* array = malloc(10 * sizeof(int));
	if (array == NULL) 
	{
		return 1;
	}

	// 初始化数组
	for (int i = 0; i <= 10; i++) 
	{
		array[i] = i;//当i=10时,会造成越界访问
	}

	free(array);
	array = NULL;
	return 0;
}

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

举例:

int main()
{
	int a = 10;
	int* p = &a;
	free(p);//error
	return 0;
}

Ⅳ、使用free释放动态开辟内存的一部分

即:使用free()释放内存时,()内的参数指针未指向所开辟内存的首地址

举例:

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
	for (int i = 0; i < 3; i++)
	{
		*p = i;
		p++;//改变了p所指向的位置
	}
	free(p);//此时的指针p并不指向开辟空间的首地址
	return 0;
}

Ⅴ、对同一块内存空间多次释放

  • 这种错误被称为“双重释放”(Double Free)

  • 当程序试图释放已经释放过的内存时,会发生未定义行为,这可能导致程序崩溃、内存损坏、安全漏洞等问题

避免双重释放:

  • 指针置空:释放内存后,将指针立即置为NULL
free(p);
p = NULL;

示例:

#include <stdio.h>
#include <stdlib.h>

int main() 
{
    void *p = malloc(10 * sizeof(int));
    if (p == NULL) 
        return 1;

    // 使用内存...

    // 释放内存
    free(p);
  	p = NULL;

    // 尝试再次释放,但由于p已经是NULL,所以不会发生双重释放
    free(p);

    return 0;
}

Ⅵ、未释放动态开辟的内存(内存泄漏)

示例 1 :

void test()
{
	int* p = (int*)malloc(40);

	int n = 0;
	scanf("%d", &n);
	if (n == 4)
		return;

	free(p);//程序有概率无法到达free处,造成内存泄露
	p = NULL;
}
int main()
{
	test();

	
	return 0;
}

示例 2 :

int* test()
{
	int* p = (int*)malloc(40);

	if (p == NULL)
		return 1;
	return p;	
}
int main()
{
	int* ret = test();
	//未释放会导致内存泄露
	//free(ret);
	//ret = NULL;
	
	return 0;
}

四、柔性数组

  • 柔性数组(Flexible Array)是C语言中的一种特性,它允许在结构体的末尾定义一个大小不固定的数组

使用条件

  • 柔性数组必须为结构体的 最后一个成员
  • 结构体中必须至少包含一个非柔性数组的成员
  • 在结构体中只能存在一个柔性数组
  • 编译器需要支持C99标准

示例:

typedef struct {
    int id;
    int grade;
    char name[]; // 柔性数组
} S1;
  • 在这个例子中,char name[] 是一个柔性数组,因为它没有指定具体的大小,可以根据需要动态分配内存

注意事项

  • sizeof操作符返回的结构体大小不包括柔性数组的大小
  • 结构体中必须至少包含一个非柔性数组的成员
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
  • 使用柔性数组可以减少内存碎片,提高内存利用率,并且使得内存释放更加方便

柔性数组的使用

#include <stdio.h>
#include <stdlib.h>
struct S
{
	int n;
	int arr[];//柔性数组成员
};

int main()
{
	//分配内存:结构体的大小加上柔性数组需要的大小
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
	if (ps == NULL)
	{
		//...分配失败
		return 1;
	}
	//赋值
	ps->n = 10;
	for (int i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//柔性数组扩容
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 + 12);
	if (ptr == NULL)
	{
		//...分配失败
		return 2;
	}
	ps = ptr;//更新指针
	for (int j = 0; j < 3; j++)//对扩容的内存赋值
	{
		ps->arr[10 + j] = 10 + j;
	}
	//打印
	printf("%d\n", ps->n);
	for (int k = 0; k < 13; k++)
	{
		printf("%d ", ps->arr[k]);
	}

	//释放
	free(ps);
	ps = NULL;
	ptr = NULL;
	return 0;
}
  •   10
      0 1 2 3 4 5 6 7 8 9 10 11 12
    
  • narr都存放在堆区,且是连续的内存

  • 请添加图片描述

  • 给一个结构体内的数据分配一个连续的内存,优点:

      1. 方便内存释放
      1. 有利于访问速度

若不使用柔性数组:

#include <stdio.h>
#include <stdlib.h>

struct S
{
	int n;//大小:4
	int* arr;//大小:4
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	//sizeof(struct S)结构体的大小不包含
	if (ps == NULL)
	{
		//...处理内存分配失败
		return 1;
	}
	ps->n = 10;
	ps->arr = (int*)malloc(40);//分配10个int的空间
	if (ps->arr == NULL)
	{
		//...处理内存分配失败
		return 1;
	}
	//使用
	for (int i = 0; i < 10; i++)//初始化数组
		ps->arr[i] = i;

	//打印
	printf("%d \n", ps->n);
	for (int i = 0; i < 10; i++)
		printf("%d ", ps->arr[i]);
	
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}
  •   10
      0 1 2 3 4 5 6 7 8 9
    
  • 虽然narr都存放在堆区,但并不连续

  • 请添加图片描述

  • 不使用柔性数组会产生较多的内存碎片,且释放内存相较复杂

拓展阅读

C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell


网站公告

今日签到

点亮在社区的每一天
去签到