ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言指针从入门到精通:内存地址、数组、函数指针与动态内存管理

C语言指针从入门到精通:内存地址、数组、函数指针与动态内存管理 1. 从“地址”到“指针”一个必须跨越的认知门槛很多刚开始接触C语言的朋友一听到“指针”两个字就头大感觉这东西抽象、难懂一不小心就“段错误”让人望而生畏。我刚开始学的时候也一样对着int *p;和*p 10;这样的代码发懵不明白这个星号到底在干嘛。后来在项目里被指针的bug折磨得死去活来才逼着自己把这块硬骨头啃下来。现在回头看指针之所以难是因为我们一开始就把它想复杂了。它本质上就是一个记录地址的变量和你用笔记本记下朋友的住址没有任何区别。今天我就用最生活化的方式把这个“纸老虎”拆开揉碎了讲给你听目标只有一个让天下没有难懂的指针。想象一下你住在一个巨大的小区里这个小区就是计算机的内存。每一户人家都有一个唯一的门牌号这就是内存地址。你家的具体位置比如3栋502室是固定的这个地址值本身是客观存在的。现在你想去朋友家串门你有两种方式第一种你直接把朋友家的详细地址3栋502室背下来每次去都现场回忆第二种你拿一张小纸条把“3栋502室”这个地址写下来然后把纸条揣兜里。这张写着地址的小纸条就是指针变量。你不需要记住朋友家具体在哪个经纬度你只需要保管好这张纸条需要的时候拿出来看一眼就能找到地方。指针干的就是这么一件事——存地址。所以当你写下int a 10;时相当于在内存的某个“房间”比如门牌号0x7ffeed42里放了一个整数10。而int *p a;这条语句就是做了两件事第一声明p是一个专门用来存放“整数类型房间地址”的纸条指针变量第二用取地址操作符获取了房间a的门牌号0x7ffeed42然后把这个门牌号写在了纸条p上。从此p这个变量里存储的值就是0x7ffeed42而不是10。注意这里有一个初学者100%会混淆的点。int *p;这行声明里的*和后面*p 20;里的*完全不是同一个操作声明里的*是“类型修饰符”它和int结合在一起共同说明了p的类型是“指向整型的指针”。而表达式里的*是“解引用操作符”意思是“请根据我手上纸条写的地址去找到那个房间并对房间里的东西进行操作”。一个是“身份声明”一个是“寻址动作”务必分清。理解了指针是“地址的容器”我们就迈出了最关键的一步。接下来所有令人困惑的语法和现象都可以从这个最朴素的模型出发去推导。2. 指针的核心操作取址、解引用与空指针指针的玩法看似花样繁多但最核心的操作只有三个获取地址、根据地址访问目标*以及处理“地址不存在”的特殊情况NULL。把这三点吃透指针的基础就牢了。2.1 取址操作符告诉我你的门牌号取址操作符又叫“address-of”运算符。它的作用极其单纯获取一个变量在内存中的起始地址。这个地址是一个数值通常以十六进制表示。int score 95; // 在内存中开辟一个房间房间名叫score里面放了95 printf(变量score的值是%d\n, score); // 输出95 printf(变量score的地址是%p\n, (void*)score); // 输出类似0x7ffeea5c8a3c第二行打印的就是score这个“房间”的门牌号。这个地址值是由操作系统在程序运行时动态分配的每次运行可能都不一样所以我们编程时从不关心具体的地址值是多少我们只关心“有没有拿到这个地址”。为什么需要地址因为C语言函数传参默认是“值传递”。这意味着当你把一个变量传给函数时函数内部得到的是这个变量的一个副本。你对副本的修改不会影响外面的原件。void try_to_change(int copy) { copy 100; // 修改的是副本 printf(函数内 copy %d\n, copy); // 输出 100 } int main() { int original 50; try_to_change(original); printf(函数外 original %d\n, original); // 输出 50原值未变 return 0; }如果我们希望函数内部能直接修改外部的变量就必须把外部变量的“地址”传给函数。函数拿到地址后就能找到原始的房间并进行修改。这就是指针最经典的应用场景之一。2.2 解引用操作符*按图索骥与登门拜访解引用操作符*是的逆操作。如果是“问你要地址”那么*就是“按照你给的地址去找人”。int money 1000; // 本金1000块 int *wallet money; // wallet是一张纸条上面写着money房间的地址 // 情况一查看纸条上写的地址是什么即指针变量本身的值 printf(钱包里记的地址是%p\n, (void*)wallet); // 输出money的地址 // 情况二根据纸条地址去找到那个房间看看里面有多少钱读取 int cash *wallet; // *wallet 等价于 money printf(根据地址找到的钱是%d\n, cash); // 输出 1000 // 情况三根据纸条地址去找到那个房间把钱换成新的写入 *wallet 2000; // *wallet 2000; 等价于 money 2000; printf(修改后本金变成了%d\n, money); // 输出 2000这个过程就像快递送货。wallet是快递单上面写着收件地址money。*wallet这个操作就是快递员拿着单子按地址找到你家money的内存空间然后把包裹数据放进去或者取出来。一个极其重要的实操心得在声明指针变量时养成立刻初始化的习惯。未初始化的指针被称为“野指针”它指向一个随机的、未知的内存地址。对野指针进行解引用就像让快递员按照一张胡乱写的地址去送件极有可能送到别人的家里非法内存访问导致程序崩溃段错误。最安全的做法是如果暂时不知道指针该指向哪里就把它设为NULL。2.3 空指针NULL明确的“无地址”状态NULL在C语言中通常是一个定义为((void*)0)的宏。它表示指针不指向任何有效的内存地址。这是一个非常有用的状态标志。int *p1; // 危险野指针指向随机地址 int *p2 NULL; // 安全明确表示“我目前不指向任何地方” // 在使用指针前先检查是否为NULL是好习惯 if (p2 ! NULL) { *p2 10; // 因为p2是NULL所以不会执行这行危险的代码 } else { printf(指针p2是空的不能使用。\n); }使用NULL初始化指针并在解引用前检查是避免程序因非法内存访问而崩溃的最基本、最有效的防御性编程手段。很多高级的指针问题比如链表、树等数据结构的实现都依赖于对NULL状态的正确判断。3. 指针与数组孪生兄弟的相爱相杀指针和数组的关系是C语言中最微妙也最核心的部分。很多人说“数组名就是指针”这个说法对了一半也错了一半。更准确的说法是在大多数表达式中数组名会被编译器自动转换为指向其首元素的指针。3.1 数组名的“退化”规则看下面这个例子int arr[5] {1, 2, 3, 4, 5}; int *ptr arr; // 这里arr“退化”为 arr[0] printf(arr[2] %d\n, arr[2]); // 输出 3 printf(*(arr 2) %d\n, *(arr 2)); // 输出 3等价于 arr[2] printf(ptr[2] %d\n, ptr[2]); // 输出 3指针可以像数组一样用下标 printf(*(ptr 2) %d\n, *(ptr 2)); // 输出 3这里揭示了两个关键点下标运算符[]的本质arr[i]在编译器看来完全等价于*(arr i)。它先计算首地址偏移i个元素后的新地址然后解引用。这就是为什么指针也能用[]的原因。数组名arr与指针ptr的细微差别虽然arr在很多情况下像指针但它不是指针变量。ptr是一个变量它的值可以改变ptr而arr是一个常量代表一块固定内存的首地址它的值不能改变arr是非法的。你可以把arr理解为一个“地址标签”贴在了数组内存的起始处。3.2 指针运算不是数学是导航指针的加减运算是理解它和数组关系的关键。指针加1不是地址值加1个字节而是加一个它所指向类型的大小。int int_arr[3] {10, 20, 30}; int *int_ptr int_arr; // 指向10 char char_arr[3] {A, B, C}; char *char_ptr char_arr; // 指向A printf(int_ptr地址: %p\n, (void*)int_ptr); printf(int_ptr 1 地址: %p\n, (void*)(int_ptr 1)); // 假设int占4字节输出结果后者比前者大40x4。 printf(char_ptr地址: %p\n, (void*)char_ptr); printf(char_ptr 1 地址: %p\n, (void*)(char_ptr 1)); // char占1字节输出结果后者比前者大10x1。这就是指针运算的“智能”之处。(ptr i)直接把你导航到第i个元素的位置省去了手动计算偏移量的麻烦。在遍历数组时指针移动比下标访问有时更高效因为省去了一次乘法运算计算arr[i]的地址需要基地址 i * sizeof(type)。3.3 数组作为函数参数必然退化为指针这是另一个关键陷阱。当把数组传递给函数时它百分之百会退化为指针。void print_array_size(int arr[]) { // 看起来像数组其实是指针 // WRONG! 这里sizeof(arr)得到的是指针的大小如8字节不是数组总大小 printf(函数内 sizeof(arr) %zu\n, sizeof(arr)); } int main() { int my_arr[10] {0}; printf(函数外 sizeof(my_arr) %zu\n, sizeof(my_arr)); // 输出 40 (10 * 4) print_array_size(my_arr); // 输出 8 (64位系统指针大小) return 0; }所以在函数内部你无法通过sizeof获取传入数组的真实长度。这就是为什么需要额外传递一个length参数的原因。void func(int arr[], int len)这种写法本质上和void func(int *arr, int len)是完全一样的。认识到这一点就能理解为什么在函数里修改arr[i]会直接影响外部的数组——因为你操作的就是原始内存。4. 二级指针与指针数组管理指针的指针当指针本身也需要被追踪和管理时我们就需要更高级的工具二级指针和指针数组。它们是多维动态结构的基础。4.1 二级指针指针的地址如果指针变量p本身也住在内存的一个房间里那么这个房间也有地址。存储这个地址的指针就是二级指针。int value 888; int *p value; // p是一级指针存放value的地址 int **pp p; // pp是二级指针存放指针p的地址 // 访问路径 // 1. 直接访问value printf(value %d\n, value); // 888 // 2. 通过p间接访问value printf(*p %d\n, *p); // 888 // 3. 通过pp双重间接访问value printf(**pp %d\n, **pp); // 888 // 修改路径 **pp 999; // 等价于 *p 999; 也等价于 value 999; printf(现在 value %d\n, value); // 999你可以把它想象成公司架构。value是一线员工p是他的直接经理手里有员工的联系方式地址。pp是部门总监手里有经理p的联系方式。总监pp想给员工value派活他不需要知道员工的直接电话他只需要联系经理*pp得到p然后让经理去联系员工*(*pp)即**pp操作到value。二级指针的核心应用场景在函数内部修改外部的一级指针。回想一下如果我们想用函数修改一个int需要传int*。同理如果想修改一个int*比如让外部的指针指向新分配的内存就需要传int**。void allocate_memory(int **ptr_inside_func) { *ptr_inside_func (int*)malloc(sizeof(int) * 10); // 修改了外部指针的指向 if (*ptr_inside_func ! NULL) { (*ptr_inside_func)[0] 100; // 使用分配的内存 } } int main() { int *external_ptr NULL; allocate_memory(external_ptr); // 传入一级指针的地址 if (external_ptr ! NULL) { printf(%d\n, external_ptr[0]); // 输出 100 free(external_ptr); } return 0; }4.2 指针数组 vs. 数组指针一场名字引发的“血案”这是笔试面试的经典坑点关键在于*、[]和标识符的结合顺序。记住口诀看最后一步它是什么整体就是什么。指针数组首先它是一个数组数组里的每个元素都是指针。写法int *arr[10];结合顺序[]的优先级高于*。所以arr先与[10]结合说明arr是一个有10个元素的数组。再与int *结合说明数组的每个元素都是int*类型。应用常用于存储多个字符串字符串常量本质是字符指针。char *str_list[] {Hello, World, C};数组指针首先它是一个指针这个指针指向一个数组。写法int (*ptr)[10];结合顺序因为有括号*先与ptr结合说明ptr是一个指针。再与[10]结合说明这个指针指向一个包含10个整数的数组。应用常用于处理二维数组特别是当二维数组作为函数参数时需要传递“行指针”。// 指针数组示例 int a1, b2, c3; int *ptr_arr[3] {a, b, c}; // 数组里存了三个地址 for(int i0; i3; i) { printf(%d , *ptr_arr[i]); // 输出 1 2 3 } // 数组指针示例 int matrix[3][4] {...}; int (*row_ptr)[4] matrix; // row_ptr指向一个包含4个int的数组即一行 // row_ptr 1 会跳过一整行4*416字节指向下一行 for(int i0; i3; i) { for(int j0; j4; j) { printf(%d , (*(row_ptr i))[j]); // 等价于 matrix[i][j] } }实操中的避坑指南当你看到一个复杂的声明时不要慌从标识符开始按照优先级()[]*一步一步往外读。对于int (*p)[10]先看括号里*p说明p是个指针再看右边[10]说明指针指向一个大小为10的数组最后看左边int说明数组里每个元素是int。所以p是“指向含有10个整数的数组的指针”。5. 函数指针将函数作为数据传递如果说变量指针是C语言的灵魂那么函数指针就是灵魂的翅膀。它允许我们将函数像数据一样存储、传递和调用是实现回调、策略模式等高级功能的基石。5.1 函数指针的声明与使用一个函数在内存中也有地址函数指针就是存储这个地址的变量。#include stdio.h int add(int x, int y) { return x y; } int subtract(int x, int y) { return x - y; } int main() { // 声明一个函数指针pfunc // 它指向一个“接受两个int参数返回int”的函数 int (*pfunc)(int, int); // 让指针指向add函数 pfunc add; // 等价于 pfunc add; 函数名本身也代表地址 printf(10 5 %d\n, pfunc(10, 5)); // 通过指针调用函数输出15 // 让指针指向subtract函数 pfunc subtract; printf(10 - 5 %d\n, pfunc(10, 5)); // 输出5 return 0; }声明函数指针的语法看起来有点怪int (*pfunc)(int, int)。拆解一下(*pfunc)说明pfunc是一个指针(int, int)说明这个指针指向的函数接受两个int参数最前面的int说明这个函数返回int。5.2 函数指针的典型应用回调函数回调函数是“你定义函数但由别人在合适的时机调用”。库函数qsort快速排序就是经典例子。#include stdlib.h #include stdio.h // 比较函数决定排序规则 int compare_ints(const void *a, const void *b) { // 先把void*指针转换为int*指针再取值比较 int arg1 *(const int*)a; int arg2 *(const int*)b; if (arg1 arg2) return -1; if (arg1 arg2) return 1; return 0; } int main() { int arr[] {42, 8, 15, 23, 4}; int n sizeof(arr) / sizeof(arr[0]); // qsort需要知道数组首地址、元素个数、元素大小、比较函数指针 qsort(arr, n, sizeof(int), compare_ints); for (int i 0; i n; i) { printf(%d , arr[i]); // 输出 4 8 15 23 42 } return 0; }qsort函数内部实现了高效的排序算法但它不知道你想按升序排还是降序排也不知道你比较两个元素的具体规则。所以它要求你提供一个函数指针compare_ints。在排序过程中qsort会多次调用你提供的这个函数根据返回值负、零、正来决定元素的相对顺序。这就是“回调”——你的代码比较函数被系统的代码qsort调用了。使用函数指针的注意事项类型必须严格匹配函数指针的类型返回值、参数列表必须和它要指向的函数完全一致。void*的妙用像qsort这样的通用函数为了能处理任何类型的数据使用void*作为参数类型。在回调函数内部你需要先将void*转换回具体的指针类型再解引用。void*是“通用指针”可以指向任何数据类型但不能直接解引用。typedef简化声明复杂的函数指针声明可以用typedef简化提高可读性。typedef int (*CompareFunc)(const void*, const void*); CompareFunc cmp compare_ints; // 现在声明就清晰多了函数指针打开了面向接口编程和动态行为配置的大门是理解Linux内核、事件驱动编程等高级主题的必备钥匙。6. 动态内存管理指针的主战场指针的威力在动态内存管理上体现得淋漓尽致。静态数组的大小在编译时就必须确定而实际开发中我们经常需要在运行时才知道需要多少内存。这时就需要用到malloc、calloc、realloc和free这一组标准库函数。6.1 malloc与free借与还的艺术malloc的功能是向操作系统“借”一块指定大小的连续内存。如果成功它返回这块内存起始地址的void*指针如果失败比如内存不足则返回NULL。#include stdlib.h int main() { // 申请一块可以存放100个int的内存 int *dynamic_arr (int*)malloc(100 * sizeof(int)); // 必须检查是否申请成功 if (dynamic_arr NULL) { fprintf(stderr, 内存分配失败\n); return 1; } // 使用这块内存就像使用普通数组一样 for (int i 0; i 100; i) { dynamic_arr[i] i * i; } // ... 使用完毕 ... // 最关键的一步释放内存还给操作系统 free(dynamic_arr); // 最佳实践释放后立即将指针置为NULL防止“悬空指针” dynamic_arr NULL; return 0; }关于malloc的几个核心细节它不初始化内存malloc返回的内存区域内容是未初始化的“垃圾值”。如果需要清零请使用calloc。它返回void*void*可以自动转换为任何类型的指针但显式转换(int*)是更好的习惯代码更清晰。sizeof是安全网在计算大小时务必使用sizeof(type) * count而不是直接写数字。这样即使类型变了比如从int改成long代码也无需修改。free之后free只是告诉操作系统“这块内存我不用了”但不会清空其中的数据也不会将指针自动置NULL。此时dynamic_arr变成了“悬空指针”指向一块已释放的无效内存。再次访问或释放它会导致未定义行为。所以free后立刻置NULL是个铁律。6.2 calloc与realloc更精细的控制calloc在分配内存的同时将其内容全部初始化为0。参数略有不同它接受两个参数元素个数和每个元素的大小。calloc(num, size)等价于malloc(num * size)后再用0填充。int *zeroed_arr (int*)calloc(50, sizeof(int)); // 分配并清零适合存储计数或标志位realloc调整已分配内存块的大小。这是动态数组扩容的关键。int *arr (int*)malloc(10 * sizeof(int)); // ... 使用中发现10个不够需要20个 ... int *new_arr (int*)realloc(arr, 20 * sizeof(int)); if (new_arr ! NULL) { arr new_arr; // 扩容成功使用新指针 // 注意原arr指向的10个int数据已被自动拷贝到新内存的前10个位置 } else { // 扩容失败原内存块arr仍然有效需按原大小使用 free(arr); // 最终还是要释放 return 1; }realloc的陷阱它可能返回一个新的地址。如果原内存块后面有足够的连续空间就在原地扩大如果没有它会找一块新的足够大的内存把旧数据拷贝过去然后释放旧内存。所以必须用另一个指针接收realloc的返回值并检查是否为NULL不能直接arr realloc(arr, new_size)因为如果失败arr会被赋值为NULL导致旧内存地址丢失造成内存泄漏。6.3 内存泄漏与野指针动态内存的两大噩梦不正确地使用动态内存会导致两个最令人头疼的问题。内存泄漏申请了内存但忘记释放或者因为逻辑错误导致无法释放。程序运行时间一长可用内存就被慢慢耗光。void leaky_function() { int *p malloc(1000); // ... 使用p ... // 函数结束p是局部变量被销毁但malloc的1000字节内存再也没有指针能引用到它了 // 这块内存永久泄漏直到程序结束。 }解决方法确保malloc/calloc和free成对出现像借书还书一样。对于复杂的数据结构如链表要遍历所有节点逐一释放。野指针/悬空指针指针指向的内存已经被释放但指针本身还在被使用。int *p malloc(sizeof(int)); *p 100; free(p); // 内存已释放 // 此时p是悬空指针 // *p 200; // 危险操作已释放内存行为未定义可能导致崩溃或数据损坏。 // printf(%d\n, *p); // 同样危险解决方法free后立即将指针置NULL。在使用指针前尤其是解引用前检查是否为NULL。管理动态内存是C程序员的成年礼。严谨的“申请-检查-使用-释放”流程配合良好的编程习惯如free后置NULL是写出稳健C程序的基础。7. 指针与结构体构建复杂数据模型的基石结构体将多个不同类型的变量打包成一个整体而指针则让我们能高效地操作和传递这个整体。两者的结合是构建链表、树、图等复杂数据结构的唯一途径。7.1 结构体指针与箭头操作符访问结构体成员我们通常用点操作符.。但当拥有的是一个指向结构体的指针时就需要先解引用再用点操作符语法上比较繁琐。C语言提供了更简洁的箭头操作符-。typedef struct { char name[50]; int age; float score; } Student; int main() { Student stu {Alice, 20, 95.5}; Student *p_stu stu; // 指向结构体的指针 // 三种访问方式等价 printf(Name: %s\n, stu.name); // 直接访问 printf(Name: %s\n, (*p_stu).name); // 先解引用再点操作 printf(Name: %s\n, p_stu-name); // 使用箭头操作符推荐 // 通过指针修改成员 p_stu-age 21; p_stu-score 96.0; return 0; }p_stu-age完全等价于(*p_stu).age。箭头操作符-是“解引用”和“取成员”两个操作的结合体写起来更直观也避免了忘记写括号(*p).的错误。7.2 结构体指针在函数传参中的巨大优势结构体可能很大包含很多成员。如果按值传递给函数会发生整个结构体的内存拷贝开销很大。传递结构体指针则只拷贝一个地址通常4或8字节效率极高并且函数内部能直接修改原结构体。void print_student(const Student *s) { // 传指针高效。const表示函数内不会修改s指向的内容 if (s ! NULL) { printf(Name: %s, Age: %d\n, s-name, s-age); } } void birthday(Student *s) { // 传指针以便修改 if (s ! NULL) { s-age 1; } } int main() { Student stu {Bob, 19}; print_student(stu); // 传递地址 birthday(stu); print_student(stu); // 年龄已增加 return 0; }这是一个非常重要的编程实践对于结构体除非明确需要副本否则一律使用指针传递。如果函数不需要修改结构体就用const修饰指针参数这是一个良好的契约告诉调用者“我保证不会动你的数据”。7.3 结构体内包含指针深拷贝与浅拷贝的陷阱当结构体的成员本身是指针时拷贝这个结构体要格外小心。typedef struct { char *name; // 指针成员指向动态分配的内存 int id; } Person; void shallow_copy_problem() { Person p1; p1.name malloc(20); strcpy(p1.name, John); p1.id 1; Person p2 p1; // 浅拷贝只拷贝了p1的每个字节包括name指针的值地址 // 此时 p1.name 和 p2.name 指向同一块内存 strcpy(p2.name, Mike); // 修改p2的namep1的name也跟着变了 printf(p1.name: %s\n, p1.name); // 输出 Mike这不是我们想要的 free(p1.name); // p2.name 现在成了悬空指针如果再 free(p2.name) 会导致双重释放程序崩溃。 }浅拷贝只复制了指针的值地址导致两个结构体的指针成员指向同一块内存。这会引起两个问题1. 通过一个结构体修改数据会影响另一个2. 释放内存时容易造成双重释放或悬空指针。解决方案是深拷贝不仅拷贝结构体本身还要为其指针成员重新申请内存并拷贝指针所指的内容。Person deep_copy_person(const Person *src) { Person dest; dest.id src-id; // 深拷贝关键步骤为新对象的指针成员申请新内存 dest.name malloc(strlen(src-name) 1); if (dest.name ! NULL) { strcpy(dest.name, src-name); } else { // 处理内存分配失败 } return dest; }在C等高级语言中可以通过拷贝构造函数和赋值运算符重载来自动实现深拷贝。但在C语言里你必须手动管理这一切。这是使用C语言构建复杂数据结构时必须时刻绷紧的一根弦。8. 指针的复杂声明与右左法则面对int (*(*fp)(int))[10];这样的“天书”如何解读C语言复杂的声明确实令人困惑但掌握“右左法则”这个工具就能像解密一样层层剥开。右左法则Right-Left Rule步骤从要解读的标识符开始。先看标识符右边如果是()则表示是函数如果是[]则表示是数组。再看标识符左边如果是*则表示是指针。如果右边遇到)或者左边解读完就跳出当前层级重复步骤2和3继续向外层解读。最后解读剩下的基础类型。让我们用它来解析几个例子例1int *p[10];标识符是p。看右边[10]-p是一个有10个元素的数组。看左边*- 数组的每个元素都是指针。看剩下的左边int- 指针指向int类型。结论p是一个由10个指向int的指针组成的数组指针数组。例2int (*p)[10];标识符是p。右边是)先看左边因为括号优先级高*-p是一个指针。跳出括号看右边[10]- 这个指针指向一个大小为10的数组。看剩下的左边int- 数组的每个元素是int。结论p是一个指向含有10个int的数组的指针数组指针。例3int (*(*fp)(int))[10];挑战一下标识符是fp。右边是)先看左边*-fp是一个指针。跳出括号看右边(int)- 这个指针指向一个函数该函数接受一个int参数。看左边函数返回类型部分*- 这个函数返回一个指针。我们得到了“函数返回指针”继续解读这个返回的指针。看右边[10]- 函数返回的指针指向一个大小为10的数组。看剩下的左边int- 数组的每个元素是int。结论fp是一个函数指针它指向的函数接受一个int参数并返回一个指向含有10个int的数组的指针。虽然这种声明在实战中很少见但理解右左法则能让你在面对任何复杂声明时都不再恐惧。在真正开发中如果声明过于复杂应该用typedef来分层简化极大提高代码可读性。// 将上面那个怪物声明拆解 typedef int (*ArrayPointer)[10]; // ArrayPointer 是“指向含10个int的数组的指针”类型 ArrayPointer (*fp)(int); // fp是一个函数指针函数接受int返回ArrayPointer类型指针是C语言的灵魂它赋予了程序员直接操作内存的能力带来了无与伦比的效率和灵活性但也带来了复杂性和风险。从理解“指针即地址”这个基本模型开始到熟练运用指针与数组、函数、结构体的关系再到驾驭动态内存管理和解读复杂声明每一步都需要结合具体的代码去实践和思考。我个人的体会是指针本身并不难难的是建立起清晰的内存模型。每当你写下一行指针代码时都在脑子里画一画内存布局图哪里是变量哪里是指针指针指向哪里数据如何流动。这个习惯坚持下来指针就会从梦魇变成你最得力的工具。最后关于智能指针、restrict关键字、多级指针的更多玩法那是C和更深入C99/C11标准的内容了但只要你牢牢掌握了本文这些核心概念那些进阶知识也不过是水到渠成。
返回列表