目录
1.为什么要有动态内存分配
我们已经掌握的内存开辟方式有
int val=20;//栈空间开辟4个字节 char arr[10]={0};开辟10字节 这两个内存开辟是固定的 数组声明的时候必须说明数组长度,一旦确定长度就不能更改 但对于空间的需求是会发生变化的,因此编译时的开辟空间方式就不合适了 因此需要动态内存开辟,让程序员自己申请和释放空间,相对灵活 即使是变长数组,一旦被确定大小,也不能再改变了。
2.1malloc和free
2.1malloc
c语言提供了动态内存开发的函数:
void * malloc(size_t size); 函数是向内存申请一块连续可用的空间,并返回指向这块空间的指针 如果开辟成功,则返回一个指向开辟好空间的指针 开辟失败,返回NULL指针,因此malloc的返回值要检查 返回类型是void,malloc函数并不知道开辟空间的类型,具体使用的时候使用者自己来决定 如果参数size为0,malloc的行为是标准是未定义的,取决于编辑器 可以将一个返回值赋给一个整型指针,这个指针变量的变量名可以理解为数组名( 如果设置了大于整型的字节数) 空间的释放,可以是用free函数,或者程序结束后,操作系统回收 malloc是在堆区上申请的内存空间 栈区上放:局部变量、形式参数、临时的变量 堆区:动态分配、malloc、free、calloc、realoc 静态区:静态变量、全局变量
2.2free
void free(void * ptr); ptr指向的空间如果不是动态开辟的,那free函数的行为是未定义的 如果是NULL指针,则什么事都不做 malloc和free都在stdlib.h头文件中 #include <stdio.h> #include <stdlib.h> int main() { int num = 0; scanf("%d", &num); int arr[num] = {0}; int* ptr = NULL; ptr = (int*)malloc(num*sizeof(int)); if(NULL != ptr)//判断ptr指针是否为空 { int i = 0; for(i=0; i<num; i++) { *(ptr+i) = 0; } } free(ptr);//释放ptr所指向的动态内存 ptr = NULL;//避免成为野指针 return 0;
3.calloc和realloc
3.1calloc
用于动态内存分配
函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0.
#include<stdio.h> #include<stdio.h> int main() { int *p=(int *)calloc(10,sizeof(int));//申请有10个元素的整型数组空间 if(NULL!= p)//假如p不是空指针 { int i=0; for(i=0;i<10;i++) { printf("%d ",*(p+i));//将这10个整型大小的空间进行打印 } } free(p);//释放p指针指向的空间 p=NULL;//将p指针变成空指针 return 0; }
3.2realloc
realloc函数的出现让动态内存管理更加灵活
有时我们在之前进行动态内存申请后,发现申请的空间还是小了,那么就可以用realloc调整动态内存开辟的大小
void * realloc(void*ptr,size_t size); ptr是调整的空间的内存起始地址 size是调整之后的大小 返回值是调整之后的内存起始位置 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间 realloc在调整内存空间的是存在两种情况: 原来空间之后有足够大的空间,还有一种是之后没有足够大的空间
如果是第一种情况,会直接追加空间,数据迁移后不发生变化,返回值的地址还是原来内存的起始地址
第二种情况,原有之后的空间不够,那么就会在堆区中另找一块区域存放,迁移数据后,返回的会是新的内存起始地址,并且释放旧的空间
#include <stdio.h> #include <stdlib.h> int main() { int *ptr = (int*)malloc(100); if(ptr != NULL) { //具体内容 } else { return 1;//返回错误 } //代码1 - 直接将realloc的返回值也就是该空间的内存起始地址放到ptr中 ptr = (int*)realloc(ptr, 1000);// //代码2 - 先将realloc函数的返回值放在p中,不为NULL,在放ptr中 int*p = NULL; p = realloc(ptr, 1000); if(p != NULL) { ptr = p; } //具体内容 free(ptr);//释放空间 return 0; }
num参数部分如果传入NULL,size参数正常,那么就是等于malloc(size),也就是申请了动态内存空间
4.常见动态内存错误
4.1对NULL指针的解引用操作
void test() { int *p=(int*)malloc(INT_MAX/4); *p=20;//如果这个空间本身不存在,也就是说p指针指向的是空指针, 那么这时候解引用p指针后赋值,就会出现问题 }
4.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的时候越界访问,因为p是数组起始地址 //那么p+9就已经是数组的最后一个元素了 //p+10就会越界访问了 } free(p); }
4.3对非动态开辟内存使用free释放
void test() { int a = 10; int *p = &a; free(p);//这里对非动态内存使用了free释放 会造成编译器卡死,是错误行为 }
4.4使用free释放一块动态开辟内存的一部分
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置,也是个错误行为 free的参数必须是某个空间的起始地址 }
4.5对同一块动态内存多次释放
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 //如果想避免,可以在第二次释放前,记得将p指针指向的空间变位NULL 因为free如果指向了的是空指针,则什么都不会操作 }
4.6动态开辟内存忘记释放(内存泄漏)
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }//因为等程序结束后操作系统才会回收,而这里while条件会一直成立 因此程序不会退出,还有些24小时都执行的程序,那么申请的空间不回收 之后可能还申请新的空间,那么最后空间不足 就造成了内存泄漏。
5.笔试题分析
5.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这个变量本身存的是NULL。 调用函数时,将NULL这个值传给了p变量。 p变量本身存的值就是NULL。 后面申请了一块空间,并将申请的新空间 的起始地址传给了p变量本身,改变了p变量 本身的值,但问题是: 这个函数没有返回值,是void,且下面也没有 进行链式访问,因此当函数调用结束后,p指针本身 都会被销毁,而str指针指向的还是空指针; 这时再给空指针复制内容进去,就会报错。 而且没有对申请的空间进行释放,导致内存泄漏。
5.2题目2:
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); } 首先str本身存的是NULL,接着调用函数GetMemory之后 ,会常见一个字符数组,然后返回字符数组的首元素地址 str本身存的值就会变成字符数组的首元素地址,但问题是: 函数调用结束后,这个字符数组的空间也会被释放掉,那么 str存的地址,就变成了一个没有被申请使用的地址,也就是非法访问 那么就只会打印随机值(如果没有进行额外操作,那么可能还是原来的值, 一旦有新操作,那么这个空间可能就会被覆盖掉)了, 比如cc,打印出来就是烫烫烫
5.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); } 唯一的问题就是没有free,释放空间 容易造成内存泄漏
5.4题目4:
void Test(void) { char *str = (char *) malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf(str); } } free之后没有将str赋值NULL,造成str变成了野指针 之后strcpy的操作就是非法访问了。 虽然结果可能可以直接打印,但这是没有新操作造成覆盖的前提下
5.5题目5:
int *f1(void) { int x=10; return (&x); } 将会返回野指针,因为函数调用结束后, 会释放空间
5.6题目6:
int *f2(void) { int *ptr; *ptr=10; return ptr; } //因为没有初始化,所以ptr的值是随机的, 这时候对ptr进行解引用赋值,等于是对野指针进行 赋值操作,是错误行为。
6.柔性数组
c99中,结构中的最后一个元素允许是位置大小的数组,这就叫做柔性数组成员
typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; 如果编译器报错,则可以改成 typedef struct st_type { int i; int a[];//柔性数组成员 }type_a;
6.1柔性数组特点:
结构中的柔性数组成员前面至少一个其他成员
sizeof返回的结构大小不包括柔性数组的内存
包含柔性数组成员的结构用malloc函数进行动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小,比如1个普通成员,一个柔性数组,那么应该用动态分配,给结构一个预期足够容纳普通成员和柔性数组的内存大小
typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; int main() { printf("%d\n", sizeof(type_a));//输出的是4,是不包含柔性数组成员大小的 return 0; }
6.2柔性数组的使用
//代码1 #include <stdio.h> #include <stdlib.h> int main() { int i = 0; type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int)); //因为sizeof(type_a)给的大小是不包含柔性数组的,所以这里我们应该还要加上 一定大小,以满足容纳柔性数组 p->i = 100; for(i=0; i<100; i++) { p->a[i] = i; } free(p); return 0; } 这里我们还可以用realloc进行灵活调整空间大小。
6.3柔性数组的优势
//代码2 #include <stdio.h> #include <stdlib.h> typedef struct st_type { int i; int *p_a; }type_a; int main() { type_a *p = (type_a *)malloc(sizeof(type_a)); p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int)); for(i=0; i<100; i++) { p->p_a[i] = i; } //释放空间 free(p->p_a); p->p_a = NULL; free(p); p = NULL; return 0; } //跟之前的代码相比,这种代码需要释放两次内存空间, 而一般用户只能选择释放最外面的空间,但里面的成员的空间却不会选择释放, 容易造成内存泄漏 其次是容易造成内存碎片,让利用率变低,还有就是非连续的空间运行效率比连续空间慢
7.c/c++内存区域划分
内存空间:内核空间(用户的代码不能执行,只能执行操作系统的代码)-栈区(向下增长)-内存映射段(文件映射、动态库、匿名映射)-堆区(向上增长)-数据段-代码段。
全局变量和静态变量放在 数据段即静态区,可执行代码和只读常量放在代码段中;
局部变量放在栈区,动态开辟的空间是在堆区上开辟的。
栈区:执行函数时,函数内的局部变量的空间(存储单元)在栈上创建,函数结束,这些空间会释放掉。栈区内存分配运算内置于处理器的指令集中,效率高,但分配内存容量有限。主要存放函数内局部变量,函数参数、返回数据、返回地址
堆区:由程序员申请开辟动态空间,也由程序员释放,程序结束时由os即操作系统回收,分配方式类似于链表
数据段:存放全局变量和静态变量,程序结束后由系统释放
代码段:存放函数体(类成员函数和全局函数)的二进制代码