动态内存管理与柔性数组

Posted 花嵩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态内存管理与柔性数组相关的知识,希望对你有一定的参考价值。

前言

  • 博主实力有限,博文有什么错误,请你斧正。

  • 本文讨论动态内存开辟的事情与注意点

  • 本文需要函数栈帧的知识,见我另外一篇博客

思维导图

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

  • 内存中有这几个区:栈区,堆区,代码段(也称谓常量区),数据段(也称谓静态区)
  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  • 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

  • 动态内存开辟申请的空间是 在堆区上,堆区上,堆区上。!!!!
  • 一旦申请空间 ,无论何时都要立刻检测是否申请成功。

动态申请 :malloc ,calloc,realloc

malloc

函数体形式

void * malloc(size_t size);

size :申请的空间大小,单位字节

注意点

  • 返回值的类型是 void* ,malloc函数并不知道开辟空间的类型,因此需要强制转换
  • 如果申请的空间满足要求,返回申请空间的起始地址,反之 返回NULL
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
  • size_t 说明 一次性开辟内存是有上限的。0~4,294,967,295(42亿)

EXP

int * p=(int * )malloc( 100*sizeof(int ));

struct st t =(struct st * )malloc(100 sizeof(struct st));

calloc

函数形式

​ void*calloc(size_t num,size_t size);

num:申请数组元素个数,

size : 数组元素的大小,单位字节

注意点

  • 返回值的类型是 void* ,calloc函数并不知道开辟空间的类型,因此需要强制转换
  • 如果申请的空间满足要求,返回申请空间的起始地址,反之 返回NULL
  • calloc申请的空间会初始化0;
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
  • size_t 说明 一次性开辟内存是有上限的。0~4,294,967,295(42亿)

EXP

int * p =(int *)calloc(100,sizeof(int ));

struct st *T =(struct st *) calloc(100,sizeof(struct st ));

realloc(重新分配已申请的空间)

函数形式

void * realloc(void*memblock,size_t size);

  • Memblock参数指向动态内存块的开头。

  • 如果memblock为NULL,realloc的行为与malloc相同,并分配一个新的大小为字节的块,返回起始地址。

  • 如果memblock不为空,则它应该是上一次调用calloc、malloc或realloc返回的指针。

  • Size参数提供块的新大小(以字节为单位)。

  • 如果size 大小为零且Buffer参数不为空,则返回值为NULL 且原始块释放

  • 如果size 不为0且memblock参数不为NULL,

    • 若在memblock后面不存在 连续的 size 个空间,编译器会在堆中找寻新的合适大小的空间,将原始块内容拷贝到新的位置,并free原始块,如果后面存在就返回原始块地址。

    • 若没有足够的空间区分配,那么返回NULL,且原始块保持不变

注意点

  • 如果membloc 为NULL,realloc行为与 malloc相同
  • realloc再找寻新的地址时,成功就会free原始块

三者联系

  • malloc 与calloc 行为基本一样,只是 calloc会初始化 开辟的内存为0 .另外calloc开辟的形式是数组,数组中的元素类型要一致。
  • realloc 传入的指针 是动态内存的地址。
  • realloc 内部有free功能

动态释放关键字:free

free

函数形式

void* free(void* memblock);

注意点

  • free释放的必须是动态内存的地址

  • free只是释放了堆区空间,不会改变membloc,

因此memblock记住了堆区空间的地址,但是地址的内容是随机的。这就意味着free后 memblock 成为了危险的 ,危险的,危险的野指针!!!!!!。

对于这种情况一般我们free后将memblock置为NULL

  • 向堆区动态申请的空间,要时刻及时free,不然程序过大,会出现严重的问题:内存泄漏

内存泄露

指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

常见动态内存错误

忘记free(内存泄漏)!!!!!!!!

int main()
{
	int* p = (int*)malloc(100);
   printf("hello\\n");
   //忘记free,导致堆区一直被占用,如果遇到到处malloc,那么堆区的内存会被耗干,导致内存泄漏
}

不及时检测是否申请成功(对NULL的解引用)

error

int main()
{

	int* p = (int*)malloc(100 * sizeof(int));
	*p = 100;
//如果malloc返回NULL呢?程序会crash



}

cor

int main()
{

	int* p = (int*)malloc(100 * sizeof(int));
	if (NULL == p)
	{
		printf("%s\\n", strerror(errno));//打印错误信息
		assert(NULL);//通过assert,终止程序。
	}
	else
	{
		*p = 100;
	}

}

越界访问开辟的空间

error

int main()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);//EXIT_FAILURE ==1,exit(1).结束程序
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
}

cor

int main()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);//EXIT_FAILURE ==1,exit(1).结束程序
	}
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
}


对非动态开辟空间的指针free

int main()
{
	char* str = "hello";

	free(str);//str是栈区局部变量
	
}

未完全释放动态申请的空间

int main()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

free后忘记将野指针置为NULL

int main()
{
	int* p = (int*)malloc(100);
	free(p);//忘记置NULL
	//p =NULL;

}

重复free同一块动态内存

int main()
{
	int* p = (int*)malloc(100);
	free(p);
	//第一free后,p成为了野指针,对野指针的任何操作都会导致程序 crash
	free(p);
}

经典面试题

面试题一

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

问题:请问运行Test 函数会有什么样的结果

分析:

  • str只是将值NULL,传给形参变量p。

因此在GetMemory 函数栈帧后,p虽然被回收了,

但是申请的空间忘记free了,会导致内存泄漏!!!

  • GetMemory后 str 仍为NULL,因此strcpy会导致程序crash

  • 另外 printf传入NULL也会导致crash

面试题二

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

问题:请问运行Test 函数会有什么样的结果

分析:

  • GetMemory函数虽然返回了 字符数组的首导致,

但是GetMemory函数栈帧结束后,为栈帧开辟的空间收回,那部分地址的内容的不变的。因此可以* str,这是虽然 printf栈帧,但是我已经把一个字符传进去了,因此可以打印。

但是当 传入一个 str时,printf栈帧后,访问地址内容是随机的。

因此我们说 str这个是非常危险,非常危险,非常危险的野指针。

面试题三

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

问题:请问运行Test 函数会有什么样的结果

分析:

  • 输出 :hello,但是没有free(str)

  • GetMemory 的参数是二级指针。因此需要传入 一级指针的地址,对二进指针解引用一次可以找寻到实参一级指针。因此通过这种方法可以改变实参str的指向。这是通过函数改变实参的一种方法。

  • 因此 str就会指向堆中开辟的空间,str成为了有效指针。因此strcpy,printf 都没有问题。但是忘记free(str)了

面试题四

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

问题:请问运行Test 函数会有什么样的结果

分析:

  • 未检测是否成功分配成功,free后 str成为了野指针,对它的任何操作都是非法的,不合规则的。虽然后面的strcpy和printf看似没有问题。

柔性数组

柔性数组是C99标准增加的 一类只能结构体中 定义的特殊数组。

形式:

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

//有些编译器 会报错,可以改成
struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

柔性数组的特点

  • 结构中的柔性数组成员前面必须有至少一个其他成员。
  • sizeof 计算结构体大小时不包含柔性数组

柔性数组的使用

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

EXA

struct st_type
{
	int t;
	int a[];//柔性数组成员
}type_a;

int main()
{
	struct st_type* T1 = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	if (NULL == T1)
	{
		exit(1);
	}

	struct st_type* T2 = (struct st_type*)calloc(1, sizeof(struct st_type) + 10 * sizeof(int));
	if (NULL == T2)
	{
		exit(1);
	}

	struct st_type* T3 = (struct st_type*)realloc(T1, sizeof(struct st_type) + 20 * sizeof(int));
	if (NULL == T3)
	{
   exit(1);
	}
	//输出T1
	T1->t = 100;
	printf("%d\\n", T1->t);
	for (size_t i = 0; i < 10; i++)
	{
		*(T1->a + i) = i;
		printf("%d  ", *(T1->a + i));
	}
	printf("\\n");
	//输出T2
	T2->t = 1000;
	printf("%d\\n", T2->t);
	for (size_t i = 0; i < 10; i++)
	{
		*(T2->a + i) = i;
		printf("%d  ", *(T2->a + i));
	}
	printf("\\n");
	//输出T3
	T3->t = 1000;
	printf("%d\\n", T3->t);
	for (size_t i = 0; i < 20; i++)
	{
		*(T3->a + i) = i;
		printf("%d  ", *(T3->a + i));
	}
	printf("\\n");

//在本例子,T3重新分配空间时,已free过 T1.因此不需要再出free野指针。
	free(T2);
	T2 = NULL;
	free(T3);
	T3 = NULL;
	
}

柔性数组的优势

  • 柔性数组这种只能结构体动态开辟空间时,才用到的特点,类似下面这种方式
typedef struct st_type
{
	int i;
    int* p_a;
}type_a;
int main() {


	type_a* p = (type_a*)malloc(sizeof(type_a));
	if (NULL == p)
	{
	
		exit(1);
	}
	p->i = 100;
	printf("%d\\n", p->i);

	p->p_a = (int*)malloc(p->i * sizeof(int));
	if (NULL == (p->p_a))
	{
		exit(1);
	}
	for (size_t i = 0; i < 100; i++)
	{
		p->p_a[i] = i;
		printf("%d  ", p->p_a[i]);
	}

	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
}

  • 虽然2种方式都可以完成相同功能。但是柔性数组的方式有二个优势:
  • 方便内存释放
    • 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回
      给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所
      以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分
      配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉
  • 访问速度快,开辟的内存是连续的
    • 连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反
      正你跑不了要用做偏移量的加法来寻址

总结

  • 无论何时借别人的东西(动态开辟内存),都要记得还(free)

  • 任何对野指针的运算都是非法的,都会导致程序crash

以上是关于动态内存管理与柔性数组的主要内容,如果未能解决你的问题,请参考以下文章

⭐️欢度国庆-共约C语言进阶⭐️ 动态内存管理+柔性数组 建议收藏

C语言之动态内存管理(动态内存分配+经典笔试题+柔性数组)[建议收藏]

C语言——动态内存管理经典笔试题+柔性数组

C语言篇 + 内存管理及柔性数组话题

剖析c语言动态内存管理

动态内存管理