三、常见的动态内存错误
💦 对NULL指针的解引用操作
#include<stdio.h> #include<stdlib.h> int main01() { int* p = (int*)malloc(10000000000); int i = 0; for(i = 0; i < 10; i++) { *(p + i) = i;//int* p = NULL; 如果开辟失败,就会非法访问内存 } return 0; } /*--------------------改正--------------------*/ int main() { int* p = (int*)malloc(10000000000); if(p == NULL) { perror("main"); return 0; } int i = 0; for(i = 0; i < 10; i++) { *(p + i) = i;//int* p = NULL; 如果开辟失败,就会非法访问内存 } return 0; }
小结
1️⃣ 对于malloc、calloc、realloc的返回值要作判空理
💦 对动态开辟空间的越界访问
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(10 * sizeof(int)); if(p == NULL) { return 0; } int i = 0; for(i = 0; i < 40; i++) { *(p + i) = i;//越界访问 } free(p); p = NULL; return 0; }
💦 使用free释放非动态开辟的空间
#include<stdio.h> #include<stdlib.h> int main() { int arr[10] = { 0 }; int* p = arr; int i = 0; for(i = 0; i < 10; i++) { *(p + i) = i; } free(p);//这是err的 p = NULL; return 0; }
💦 使用free释放一块动态开辟内存的一部分
#include<stdio.h> #include<stdlib.h> int main01() { int* p = (int*)malloc(10 * sizeof(int)); if(p == NULL) { return 0; } int i = 0; for(i = 0; i < 5; i++) { *p++ = i; } free(p);//没有完全回收 p = NULL; return 0; } /*--------------------改正--------------------*/ int main() { int* p = (int*)malloc(10 * sizeof(int)); if(p == NULL) { return 0; } //无非就是想让数组的的前5个元素初始化为0 1 2 3 4,只要不让p真实的往后走即可 int i = 0; for(i = 0; i < 5; i++) { //1. p[i] = i; //2. //*(p + i) = i; } free(p); p = NULL; return 0; }
小结
1️⃣ 未完全释放动态开辟的空间是err的
2️⃣ 可能会造成内存泄漏,因为没有人再能记住开辟的起始空间了
💦 对同一块动态内存多次释放
#include<stdio.h> #include<stdlib.h> int main01() { int* p = (int*)malloc(100); //使用 //... //释放 free(p); //... //... free(p);//err return 0; } /*--------------------改正--------------------*/ #include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(100); //使用 //... //释放 free(p); p = NULL; //... //... free(p);//无意义 return 0; }
小结
1️⃣ 在free完malloc、calloc、realloc开辟空间后,要及时置为NULL
💦 动态开辟内存忘记释放 (内存泄漏)
#include<stdio.h> #include<stdlib.h> void test() { int* p = (int*)malloc(100);//p是局部变量 if(p == NULL) return; //使用 //... } int main01() { test(); //... //这里就内存泄漏了: //在test内动态开辟了空间,忘了释放。且局部变量p也没留下任何遗言,所以在函数外部也释放不了。只要程序没有死,这块空间就没人能找到 return 0; }
💦 C/C++中程序内存区域划分示意图
1️⃣ 栈区( stack ):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2️⃣ 堆区( heap ) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3️⃣ 数据段(静态区 ) ( static )存放全局变量、静态数据。程序结束后由系统释放。
4️⃣ 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
四、几个经典的笔试题
💦 1.
#include<stdio.h> #include<stdlib.h> #include<string.h> void GetMemory(char* p)//p是str的一份临时拷贝 { p = (char*)malloc(100);//2.动态开辟空间后没有释放;p为局部变量,出了范围就找不到开辟的空间了,所以内存泄漏 } void Test(void) { char* str = NULL; GetMemory(str);//值传递 strcpy(str, "hello world");//1.同strcpy(NULL, "hello world") printf(str); } int main01() { Test(); return 0; } /*--------------------改正1--------------------*/ #include<stdio.h> #include<stdlib.h> #include<string.h> char* GetMemory(char* p) { p = (char*)malloc(100); return p;//在局部变量p销毁前,把指向动态开辟好的空间的地址返回回来 } void Test(void) { char* str = NULL; str = GetMemory(str); strcpy(str, "hello world"); printf(str); free(str); str = NULL; } int main02() { Test(); return 0; } /*--------------------改正2--------------------*/ #include<stdio.h> #include<stdlib.h> #include<string.h> void GetMemory(char** p) { *p = (char*)malloc(100); //*p找到str } void Test(void) { char* str = NULL; GetMemory(&str);//传址 strcpy(str, "hello world"); printf(str); free(str); str = NULL; } int main() { Test(); return 0; }
💦 2.
#include<stdio.h> char* GetMemory(void) { char p[] = "hello world";//p是局部变量,但是接收的内容是在栈区创建的 return p;//这里虽然返回p的地址,但是这块空间的内容已经销毁了 } void Test(void) { char* str = NULL; str = GetMemory(); printf(str);//非法访问内存:现在str的空间就不是自己的了,所以打印的时候,就烫烫烫了 } int main() { Test(); return 0; }
💦 3.
#include<stdio.h> int* f2(void) { int* ptr; *ptr = 10;//野指针问题:ptr没有初始化,这时候去解引用就出问题了 return ptr; } int main() { f2(); return 0; }
💦 5.
#include<stdio.h> #include<string.h> void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void* Test(void) { char* str = NULL; GetMemory(&str, 100) strcpy(str, "hello"); printf(str); } int main01() { Test();//没有free return 0; } /*--------------------改正--------------------*/ #include<stdio.h> #include<string.h> 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(str);//改正处 str = NULL; } int main() { Test(); return 0; }
💦 6.
#include<stdio.h> #include<stdlib.h> void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello") free(str); if(str != NULL)//free不会主动置为NULL { strcpy(str, "world");//str已经释放了,非法访问内存 printf(str); } } int main01() { Test(); return 0; } /*--------------------改正--------------------*/ #include<stdio.h> #include<stdlib.h> void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello") free(str); str = NULL;//主动置NULL if(str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
五、柔性数组
💦 什么是柔性数组
🎗 想必很多人都未听说过柔性数组 (flexible array) 这个概念,但是它确实存在。C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做 “柔性数组” 成员。注:不代表所有编译器都能支持
struct S1 { int n; int arr[];//这就叫做 "柔性数组" 成员 }; struct S2 { int n; int arr[0];//也可以这样写 };
💦 柔性数组的特点
1️⃣ 结构体中的柔性数组成员前面必须至少一个成员
2️⃣ sizeof返回的这种结构体大小不包括柔性数组的内存
3️⃣ 包含柔性数组成员的结构体使用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
4️⃣ 柔性数组的成员都在堆上开辟
#include<stdio.h> #include<stdlib.h> struct S { int n; int arr[];//1.在之前必须有1个成员以上 }; int main() { struct S s = { 0 }; printf("%d\n", sizeof(s));//2.4Byte //3.期望arr的大小是10个整型 struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int)); if(ps == NULL) { return 0; } //赋值 ps->n = 10; int i = 0; for(i = 0; i < 10; i++) { ps->arr[i] = 0; } //调整 struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int)); if(ptr != NULL) { ps = ptr; } //释放 free(ps); ps = NULL; return 0; }
🎗 看到这里,是不是觉得柔性数组没必要存在,因为只要让指针指向动态开辟的空间即可 (模拟柔性数组)
#include<stdio.h> #inlude<stdlib.h> struct S { int n; int* arr; }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S)); if(ps == NULL) return 0; ps-> n = 10; ps->arr = (int*)malloc(10 * sizeof(int)) if(ps->arr == NULL) return 0; //赋值 int i = 0; for(i = 0; i < 10; i++) { ps->arr[i] = i; } //增加 int* ptr = (int*)realloc(ps->arr, 20 * sizeof(int)); if(ptr != NULL) { ps->arr = ptr; } //释放 //这里需要回收2个空间,且回收必须有先后 free(ps->arr); ps->arr = NULL; free(ps); ps = NULL; return 0; }
对比:柔性数组➰指针模拟柔性数组
1️⃣ 指针模拟的方式需要2次malloc、2次free,也就意味着容易出错;而柔性数组只要1次malloc、1次free即可
2️⃣ 其次malloc多了,内存碎片相对的也变多了,内存的利用率就降低了
💨 小结柔性数组的好处:
1️⃣ 方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
2️⃣ 有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,也没多高,反正你跑不了要用做偏移量的加法来寻址)
推荐一篇关于柔性数组的文章