资讯专栏INFORMATION COLUMN

动态内存管理与柔性数组

ghnor / 1937人阅读

摘要:动态内存开辟申请的空间是在堆区上,堆区上,堆区上。亿,重新分配已申请的空间函数形式参数指向动态内存块的开头。传入的指针是动态内存的地址。柔性数组序柔性数组是标准增加的一类只能在结构体中定义的特殊数组。计算结构体大小时不包含柔性数组。

前言

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

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

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

思维导图

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/121793.html

相关文章

  • 动态内存管理动态内存分配、常见错误、经典笔试题、柔性数组

    摘要:如果开辟失败,则返回一个指针,因此的返回值一定要做检查。函数用来释放动态开辟的内存。 目录 一、动态内存分配1、为什么存在动态内存分配 二、malloc1、m...

    Songlcy 评论0 收藏0
  • C语言进阶:动态内存管理

    摘要:释放不完全导致内存泄漏。既然把柔性数组放在动态内存管理一章,可见二者有必然的联系。包含柔性数组的结构用进行动态内存分配,且分配的内存应大于结构大小,以满足柔性数组的预期。使用含柔性数组的结构体,需配合以等动态内存分配函数。 ...

    shinezejian 评论0 收藏0
  • C语言篇 + 内存管理柔性数组话题

    摘要:动态内存函数的介绍和语言提供了一个动态内存开辟的函数,它的函数原型是这样的这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。 目录 为什么存在动态...

    VishKozus 评论0 收藏0
  • 【C语言进阶】动态内存管理/分配

    摘要:栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量函数参数返回数据返回地址等。 C语言动态内存分配篇 目录 一、为什么存在动态内存管理/分配?         内存的存储形式划分 二、动态内存函数的介绍         malloc ...

    Carson 评论0 收藏0
  • 动态内存管理

    摘要:函数用来释放动态开辟的内存。每次完,切记要把函数置成,函数的参数于不同,会开辟个大小为的空间,并且会把这些空间的内容初始化为举个例子函数的出现让动态内存管理更加灵活。 ...

    YuboonaZhang 评论0 收藏0

发表评论

0条评论

阅读需要支付1元查看
<