尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“

C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“ 个人主页GeekZHR 欢迎各位大佬互三25级机器人工程N非本科生在读自学C/C嵌入式软硬件等等ing欢迎各位大佬评论区互动交流写在前面前五讲留的尾巴都在这一篇补上前五讲我们一路从地址 指针变量讲到数组名 首元素地址再到函数指针、回调函数、笔试题把指针的常规内容基本走完了。但有几个地方系列里其实是留了伏笔、没来得及展开的——比如指针(4)里提过一句void*在内存函数里怎么用指针(3)里也提过一句复杂声明的右左法则怎么读。更重要的是还有一整块动态内存管理malloc/free前五讲压根没碰而它恰恰是面试和真实项目里指针最要命的地方。这篇补充篇我就把这三块盲区一次补齐动态内存管理 → mem 系列内存函数 → 复杂指针声明配好图争取给指针系列真正画上句号。一、动态内存管理指针 堆1.1 为什么需要动态内存先分清三个区到目前我们写过的变量要么是局部变量函数里定义要么是全局变量。它们都有一个共同点大小在编译期就确定了。可真实项目里经常有数组多大要等程序跑起来才知道的场景——比如读一个文件、接收用户输入事先根本不知道有多少数据。内存里大致分成这么几个区不同平台叫法略有差异抓住本质即可栈区stack放局部变量、函数参数编译器自动分配、自动回收用完就没了堆区heap放动态内存由程序员手动malloc申请、free释放不用了就泄漏静态区static放全局变量、static修饰的变量程序运行期间一直存在。 关键区分栈上的东西自动管堆上的东西手动管。动态内存分配就是在堆区上手动开辟一块空间大小可以在运行时才决定。1.2 malloc 和 freemalloc在堆上申请一块连续空间函数原型void* malloc(size_t size); // 参数要申请的字节数参数是字节数不是元素个数想开 10 个int就传10 * sizeof(int)返回值是这块空间的首地址void*所以能赋给任意指针类型分配失败返回NULL所以用之前一定要判断。free用来释放malloc/calloc/realloc申请的空间void free(void* ptr); // 释放 ptr 指向的动态内存看一个完整例子——申请 10 个int用完释放#include stdio.h #include stdlib.h // malloc/free 在 stdlib.h 里 int main() { int *p (int*)malloc(10 * sizeof(int)); // 申请 10 个 int 的空间 if (p NULL) // 先判断是否申请成功 { perror(malloc); return 1; } int i 0; for (i 0; i 10; i) p[i] i 1; // 当作数组用 for (i 0; i 10; i) printf(%d , p[i]); free(p); // 用完释放 p NULL; // 释放后置 NULL防止野指针 return 0; }两个必须记住的要点malloc用完一定要free否则这块内存会一直占着程序也不还——这就是内存泄漏free(p)之后要把p置成NULL否则p还留着那块已释放的地址成了悬垂指针再碰它就危险。 一句话malloc和free必须成对出现。申请了不释放是内存泄漏释放了不置 NULL 是野指针。1.3 calloc申请 自动清零calloc也是申请动态内存原型void* calloc(size_t num, size_t size); // num 个元素每个 size 字节它和malloc的区别有两处一是参数拆成了元素个数 × 单个大小二是申请出来的内存会被自动初始化为 0。#include stdio.h #include stdlib.h int main() { int *p (int*)calloc(10, sizeof(int)); // 等价于 10 个 int且全被置 0 if (p NULL) return 1; // 此时 p[0] ~ p[9] 全都是 0不用自己再清零 free(p); p NULL; return 0; } 选择需要申请完就是 0的场景比如计数器、标志位数组用calloc更省事否则malloc就行malloc不保证初始值是什么。1.4 realloc调整已申请空间的大小有时候一开始申请小了后面想扩容。这时用reallocvoid* realloc(void* ptr, size_t size); // 把 ptr 指向的空间调整成 size 字节它做的事在ptr的基础上把空间调整到size字节。注意几个坑返回值不一定是原来的ptr如果原空间后面还有空余就原地扩容返回原地址如果后面不够就另找一块更大的内存、把原数据拷过去返回新地址所以必须用返回值重新赋值不能假设它还是原来那个地址稳妥写法先用临时指针接成功后再赋值给原指针防止扩容失败时把原来的地址也弄丢了。#include stdio.h #include stdlib.h int main() { int *p (int*)malloc(5 * sizeof(int)); // 先申请 5 个 if (p NULL) return 1; for (int i 0; i 5; i) p[i] i; // 扩容到 10 个用临时指针 tmp 接 int *tmp (int*)realloc(p, 10 * sizeof(int)); if (tmp NULL) // 扩容失败p 还指向原来的 5 个 { free(p); return 1; } p tmp; // 成功后再把 p 换成新地址 for (int i 5; i 10; i) p[i] i; // 后面 5 个也填上 for (int i 0; i 10; i) printf(%d , p[i]); free(p); p NULL; return 0; }1.5 动态内存的五大常见错误面试高频这些错误几乎每一条都被问过务必一条条对照排查① 对NULL解引用——忘了判断malloc返回值int *p (int*)malloc(1000000000 * sizeof(int)); *p 10; // ⚠️ 如果 malloc 失败返回 NULL这里就是对 NULL 解引用② 越界访问——申请了 N 个却访问到 N 个之外int *p (int*)malloc(10 * sizeof(int)); for (int i 0; i 10; i) // ⚠️ 多访问了 p[10]越界 p[i] i;③ 忘记free内存泄漏——尤其是循环里反复malloc却不释放while (1) { int *p (int*)malloc(1024 * 1024); // 每次申请 1MB 都不释放 // ⚠️ 内存越用越少最终耗尽 }④free之后没置NULL悬垂指针int *p (int*)malloc(sizeof(int)); free(p); // 释放了 // p 还留着已释放的地址变成悬垂指针 *p 10; // ⚠️ 非法访问已释放的内存⑤ 重复free或free非动态内存int a 10; int *p a; free(p); // ⚠️ free 栈上的变量非法 int *q (int*)malloc(sizeof(int)); free(q); free(q); // ⚠️ 重复 free 同一块内存 五个错误的应对口诀判 NULL、不越界、记得 free、free 后置 NULL、别重复 free 也别 free 栈上变量。1.6 柔性数组C99结构体里可以有一个大小待定的数组成员叫柔性数组。它是结构体中最后一个成员写成类型 名[]不写大小struct S { int n; // 普通成员 int arr[]; // 柔性数组成员大小待定 };柔性数组不能单独用要配合malloc一起把结构体和数组一次性申请出来#include stdio.h #include stdlib.h struct S { int n; int arr[]; // 柔性数组 }; int main() { // 一次性申请结构体大小 10 个 int 的大小 struct S *ps (struct S*)malloc(sizeof(struct S) 10 * sizeof(int)); if (ps NULL) return 1; ps-n 10; for (int i 0; i 10; i) ps-arr[i] i; // arr 就是紧跟在结构体后面的那 10 个 int for (int i 0; i 10; i) printf(%d , ps-arr[i]); free(ps); ps NULL; return 0; }好处结构体和它的数组在内存里是连续的一块free一次就全释放不用分开管也避免了结构体里写死大数组的浪费。 柔性数组的要点必须是最后一个成员、必须写成arr[]、必须配合malloc一次申请sizeof(struct) n * sizeof(元素)。二、mem 系列内存函数void* 的实战指针(4)里我们认识了void*——先接住任意类型的地址。库函数里有一整套以void*为核心的内存函数就是void*最经典的实战它们不管数据类型直接按字节操作内存。都在string.h里。2.1 memcpy按字节拷贝不处理重叠void* memcpy(void* dest, const void* src, size_t num);把src开始的num个字节拷贝到dest开始的位置返回dest。它不负责处理内存重叠的情况。#include stdio.h #include string.h int main() { int arr1[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int arr2[10] { 0 }; memcpy(arr2, arr1, 10 * sizeof(int)); // 拷贝 10 个 int共 40 字节 for (int i 0; i 10; i) printf(%d , arr2[i]); // 1 2 3 4 5 6 7 8 9 10 return 0; }自己模拟实现一个memcpy核心就是转成char*按字节一个个搬#include stdio.h void* my_memcpy(void* dest, const void* src, size_t num) { char* d (char*)dest; const char* s (const char*)src; while (num--) *d *s; // 一个字节一个字节地拷贝 return dest; }2.2 memmove按字节拷贝处理重叠memcpy的短板是不处理重叠。如果dest和src指向的区间有重叠直接从前往后拷贝会把还没拷贝的源数据先覆盖掉结果就错了。这时用memmovevoid* memmove(void* dest, const void* src, size_t num);memmove会判断拷贝方向从而正确处理重叠#include stdio.h #include string.h int main() { int arr[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 把 1 2 3 4 5 整体往后挪两个位置变成 1 2 1 2 3 4 5 8 9 10 memmove(arr 2, arr, 5 * sizeof(int)); for (int i 0; i 10; i) printf(%d , arr[i]); return 0; }这里srcarr和destarr2有重叠memmove能正确处理换成memcpy结果就可能不对。模拟实现的关键当dest在src前面从前向后拷贝当dest在src后面从后向前拷贝#include stdio.h void* my_memmove(void* dest, const void* src, size_t num) { char* d (char*)dest; const char* s (const char*)src; if (d s) // dest 在 src 前面从前往后 { while (num--) *d *s; } else // dest 在 src 后面从后往前 { while (num--) *(d num) *(s num); } return dest; } 一句话记住memcpy不处理重叠memmove处理重叠。拿不准会不会重叠时用memmove更稳妥。两者的模拟实现都靠转char*按字节操作。2.3 memset把内存批量设为某个值void* memset(void* ptr, int value, size_t num);把ptr开始的num个字节每个字节都设成value注意是按字节设值value只取低 8 位。#include stdio.h #include string.h int main() { char arr[] hello world; memset(arr 6, x, 5); // 从第 6 个字符开始5 个字节都设成 x printf(%s\n, arr); // hello xxxxx return 0; }模拟实现void* my_memset(void* ptr, int value, size_t num) { char* p (char*)ptr; while (num--) *p (char)value; // 按字节设置 return ptr; }⚠️ 注意memset按字节设值所以用memset(arr, 1, sizeof(arr))给整型数组设成 1是错的——每个int的 4 个字节都会变成0x01结果每个元素是0x01010101而不是 1。给整型数组清零用memset(arr, 0, sizeof(arr))没问题因为 0 的 4 个字节都是 0。2.4 memcmp按字节比较int memcmp(const void* ptr1, const void* ptr2, size_t num);比较ptr1和ptr2开头的num个字节返回值和strcmp一个约定p1 - p2的符号#include stdio.h #include string.h int main() { int arr1[] { 1, 2, 3, 4, 5 }; int arr2[] { 1, 2, 3, 4, 6 }; int ret memcmp(arr1, arr2, 5 * sizeof(int)); printf(%d\n, ret); // 负数arr1 最后一个元素 5 arr2 的 6 return 0; }模拟实现int my_memcmp(const void* ptr1, const void* ptr2, size_t num) { const char* p1 (const char*)ptr1; const char* p2 (const char*)ptr2; while (num--) { if (*p1 ! *p2) return *p1 - *p2; // 第一个不相等的字节谁大谁小 p1; p2; } return 0; // num 个字节全相等 } 这一节四个函数本质都是转char* 按字节循环。看懂这个套路void*泛型指针的实战就算入门了——它不关心你传的是int*、char*还是结构体一律按字节处理。三、复杂指针声明与右左法则前面几讲我们见过int (*p)[5]、int (*pf)(int,int)这些已经有点绕了。可真实代码尤其看库、看内核、看面试题里还有更天书的比如void (*signal(int , void(*)(int)))(int); int (*(*p)[5])(int);这一节就用一个通用方法把任意复杂声明读出来。3.1 记住两条优先级读声明前先记住符号的优先级和运算符优先级一致括号()优先级最高优先看括号里的[]数组和()函数同级都高于*指针。所以int *p[5]里p先和[5]结合是数组int (*p)[5]里括号让p先和*结合是指针。这条前几讲已经反复用了。3.2 右左法则从变量名出发先右后左右左法则的步骤从变量名最内层的名字出发先向右看遇到)就向左看遇到(再向右看……像之字形一样交替直到读完整个声明。先拿两个已经熟悉的来练手例 1int (*p)[5]从p出发右边是)于是向左看看到*→p 是指针再向右看看到[5]→ 指针指向一个有 5 个元素的数组剩下的int是数组元素类型。读作p 是一个指针指向有 5 个 int 的数组即数组指针。例 2int (*p[5])(int)从p出发p先和[5]结合 →p 是数组有 5 个元素再看数组的元素类型(*...)(int)→ 是函数指针指向参数 int、返回 int 的函数。读作p 是一个数组有 5 个元素每个元素是指向 int 函数的指针即函数指针数组指针(3)里的转移表。3.3 上强度拆一个真正复杂的三级嵌套看这个int (*(*p)[5])(int);用右左法则一步步拆从p出发右边是)向左看是*→p 是指针向右看是[5]→ 指针指向有 5 个元素的数组数组元素是什么往左看是*(*p)[5]里去掉p还剩(*)[5]元素是*→ 数组元素是指针这个指针指向什么向右看是(int)→ 指向参数为 int 的函数左边剩下的int是函数返回类型。读作p 是一个指针指向有 5 个元素的数组这个数组的每个元素又是一个指针指向参数 int、返回 int的函数。写成类型就是int (*(*)[5])(int)。看着吓人用右左法则之字形一读就通了。 右左法则的口诀变量名出发右左右左交替走。看到[n]说数组看到*说指针看到(参数)说函数最后剩下的类型是返回值/元素类型。3.4 用 typedef 把复杂声明化繁为简右左法则能读懂但真写代码时没人愿意每次写一大坨。更实用的做法是用typedef给复杂类型起个简单名字指针(3)里也提过新名字要写在*的右边。先看库里的经典signal函数用来注册信号处理函数原型void (*signal(int , void(*)(int)))(int);第一次看基本懵。用 typedef 拆两步// 第 1 步把函数指针类型 void(*)(int)重命名成 sighandler_t typedef void (*sighandler_t)(int); // 第 2 步signal 就变成——参数是 int 和一个函数指针返回也是函数指针 sighandler_t signal(int signum, sighandler_t handler);这样一眼就懂signal接收一个信号编号int和一个处理函数sighandler_t返回之前的那个处理函数也是sighandler_t。再比如上一节那个三级嵌套int (*(*p)[5])(int)用 typedef 也能拆typedef int (*pfun_t)(int); // 指向int - int函数的指针类型 typedef pfun_t arr5_t[5]; // 存 5 个函数指针的数组类型 arr5_t *p; // 指向这个数组的指针三行清楚多了。 复杂声明的终极心法读用右左法则写用typedef。右手法则负责看懂天书typedef 负责别自己写天书。总结最后把这一篇补充的骨架拎出来动态内存分配栈区自动管理、堆区手动管理malloc(size)申请、free(ptr)释放必须成对。四个分配函数malloc不初始化、calloc自动清零、realloc调整大小要用临时指针接、成功后重新赋值、free释放。五大常见错误对 NULL 解引用、越界访问、忘记 free泄漏、free 后没置 NULL悬垂指针、重复 free / free 栈上变量。柔性数组结构体最后一个成员写成int arr[]配合malloc(sizeof(struct) n*sizeof(元素))一次性申请成连续一块。mem 系列内存函数memcpy/memmove拷贝后者处理重叠、memset按字节设值、memcmp按字节比较本质都是转char*按字节循环。复杂声明读用右左法则从变量名出发右左右左交替写用typedef新名字写在*右边。一句话收尾指针学到深处就三件事——堆上内存要手动申请手动还、内存函数要按字节操作、复杂声明要右左法则拆着读把这三块补齐指针才算是真正毕业了。拓展学习动态内存与结构体的结合链表、栈、队列这些数据结构都是靠结构体指针 malloc堆出来的数据结构课的核心。内存泄漏排查工具valgrind、AddressSanitizerASan能帮你自动查出哪块内存没释放、哪次越界了。restrict关键字C99 里给指针加不重叠的提示帮助编译器优化memcpy的形参就用了restrict。上一讲深入理解指针(5)sizeof 与 strlen 对比、数组名三大规则、指针运算笔试题
返回列表