动态内存管理与柔性数组
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语言进阶⭐️ 动态内存管理+柔性数组 建议收藏