Day 10:C语言指针终极进阶:指针运算、数组指针、指针数组、函数指针(全网最细,面试必刷,含完整实战)

张开发
2026/4/21 16:16:54 15 分钟阅读

分享文章

Day 10:C语言指针终极进阶:指针运算、数组指针、指针数组、函数指针(全网最细,面试必刷,含完整实战)
本文是C语言指针系列的终极收官篇聚焦指针学习的四大核心难点——*与混合运算、数组指针、指针数组、函数指针结合底层内存原理、优先级拆解、多场景代码示例、易错点避坑、面试高频考点以及命令行参数实战、作业完整实现含注释测试用例补充拓展知识和工程实战技巧内容深度、丰富度、排版完全适配CSDN爆款博客标准。无论是C语言进阶学习、校招笔试面试还是工程开发中指针的灵活运用本文都能提供全面、细致的指导堪称“指针终极指南”。本文适合C语言入门后进阶学习者、期末复习学生、校招面试备考者、需要巩固指针基础的工程开发者建议收藏备用反复研读。一、* 和 作用于指针面试必考易错点拉满*解引用运算符和 自增运算符作用于指针时是C语言初学者最容易踩坑的场景核心矛盾在于「优先级」和「结合性」的混淆很多人因理解偏差写出bug甚至面试时直接翻车。本节将从优先级、结合性、执行步骤、代码示例、易错对比五个维度彻底讲透每一种组合的本质。核心前提必记1. * 和 均为「单目运算符」二者优先级相同2. 单目运算符的「结合性为自右向左」从右往左依次结合3. 后置p先使用后自增先取变量当前值再执行自增操作4. 前置p先自增后使用先执行自增操作再取变量自增后的值5. 关键区分作用于「指针变量本身」还是「指针指向的内容」。1.1 *p最常用高频考点解析结合优先级和结合性*p 等价于 *(p)括号可加可不加不改变执行逻辑。核心逻辑 作用于指针变量p后置* 作用于p自增前的地址。执行步骤分3步清晰无歧义1. 先取指针p当前指向的地址通过*解引用得到表达式的结果即p当前指向的值2. 指针变量p执行自增操作p p 1地址向后偏移偏移量由p的类型决定如int*偏移4字节char*偏移1字节3. 最终表达式结果是p自增前指向的值p本身的地址发生偏移指向了下一个同类型元素。代码示例带内存图解思路#include stdio.h int main() { int arr[] {10, 20, 30, 40}; int *p arr; // p指向arr[0]地址假设为0x00000000int*偏移4字节 // 执行*p int res *p; printf(表达式结果res %d\n, res); // 输出10p自增前指向arr[0]的值 printf(p的地址%p\n, p); // 输出0x00000004p自增后指向arr[1] return 0; } ​易错点误区认为*p是“先解引用再让指向的值自增”——错误作用的是指针p不是*p指向的值不会改变改变的是指针的地址。1.2 *(p)与*p完全等价解析括号的作用是“明确结合顺序”但由于*和优先级相同、结合性自右向左*(p)和*p的执行逻辑完全一致没有任何区别。补充括号仅在“改变优先级”时有用此处括号不改变任何执行顺序仅提升代码可读性建议新手加上避免混淆。代码示例验证等价性#include stdio.h int main() { int arr[] {10, 20, 30}; int *p1 arr; int *p2 arr; int res1 *p1; // 无括号 int res2 *(p2); // 有括号 printf(res1 %d, res2 %d\n, res1, res2); // 均输出10 printf(p1地址%p, p2地址%p\n, p1, p2); // 均指向arr[1]地址相同 return 0; } ​1.3 (*p)重点区分易错解析括号改变了优先级先执行*p解引用再执行自增。核心逻辑作用于*p指针指向的内容而非指针p本身。执行步骤1. 括号优先先对p解引用得到*p指向的值2. 对*p执行后置自增*p *p 1表达式的结果是「自增前的值」3. 指针p本身的地址不发生任何变化依旧指向原来的元素。代码示例对比*p#include stdio.h int main() { int arr[] {10, 20, 30}; int *p arr; // p指向arr[0]值为10 int res (*p); printf(表达式结果res %d\n, res); // 输出10自增前的值 printf(*p %d\n, *p); // 输出11指向的值自增1 printf(p的地址%p\n, p); // 地址不变仍指向arr[0] return 0; } ​面试考点高频提问*p 和 (*p) 的区别核心回答前者改变指针地址后者改变指向的值表达式结果均为自增前的值1.4 *p前置自增易错解析结合性自右向左*p 等价于 (*p)。核心逻辑作用于*p指向的内容且是前置自增——先自增后取值。执行步骤1. 先对p解引用得到*p指向的值2. 对*p执行前置自增*p *p 13. 表达式的结果是「自增后的值」指针p的地址不变。代码示例对比(*p)#include stdio.h int main() { int arr[] {10, 20, 30}; int *p arr; int res1 (*p); // 后置自增结果为自增前的值 int res2 *p; // 前置自增结果为自增后的值 printf(res1 %d, res2 %d\n, res1, res2); // 输出10, 12 printf(*p %d\n, *p); // 输出12 return 0; } ​1.5 *p前置自增指针先移解析结合性自右向左*p 等价于 *(p)。核心逻辑作用于指针p前置自增*作用于p自增后的地址。执行步骤1. 先对指针p执行前置自增p p 1地址向后偏移2. 对偏移后的p解引用得到表达式的结果自增后指向的值3. 最终指针p地址改变表达式结果是偏移后指向的值。代码示例对比*p#include stdio.h int main() { int arr[] {10, 20, 30}; int *p1 arr; int *p2 arr; int res1 *p1; // 后置自增结果10p1指向arr[1] int res2 *p2; // 前置自增p2先指向arr[1]结果20 printf(res1 %d, res2 %d\n, res1, res2); // 输出10, 20 printf(p1地址%p, p2地址%p\n, p1, p2); // 均指向arr[1]地址相同 return 0; } ​1.6 总结表格面试必背一目了然表达式作用对象执行顺序表达式结果指针p是否偏移指向的值是否改变*p指针p后置先取值后p自增p自增前指向的值是否*(p)指针p后置先取值后p自增p自增前指向的值是否(*p)指向的值*p后置先取值后*p自增*p自增前的值否是*p指向的值*p前置先*p自增后取值*p自增后的值否是*p指针p前置先p自增后取值p自增后指向的值是否二、数组指针指向数组的指针行指针适配二维数组数组指针是指针的重要应用场景也是面试高频考点很多人会把它和“指针数组”混淆后续会详细区分。核心记住数组指针是一个指针它指向的是一整个数组而非数组的单个元素常用于操作二维数组。2.1 本质与核心定义数组指针 → 指针变量专门用于存储「数组的地址」指向的是“一整个数组”而非单个元素。语法格式必须加括号否则会变成指针数组// 格式元素类型 (*指针名)[数组长度]; int (*p)[3]; // p是数组指针指向包含3个int类型元素的数组 ​关键括号()的作用是提升p的优先级确保p先被定义为指针再结合[3]表示“指向包含3个int的数组”。2.2 数组指针的类型结合二维数组讲解以二维数组为例彻底理解数组指针的类型int arr[2][3] {{1,2,3}, {4,5,6}}; ​二维数组arr的内存布局连续存储0x000000001arr[0][0] → 0x000000042arr[0][1] → 0x000000083arr[0][2] → 0x0000000C4arr[1][0] → 0x000000105arr[1][1] → 0x000000146arr[1][2]核心分析1. 二维数组名arr的本质arr代表「二维数组第一行的地址」而第一行本身是一个“包含3个int的一维数组”int [3]类型2. arr[0]的本质arr[0]是第一行一维数组的数组名代表「第一行首元素的地址」即amp;arr[0][0]类型是int *3. amp;arr[0]的本质对第一行一维数组取地址得到的是「整个第一行数组的地址」类型是int (*)[3]这就是数组指针的类型4. 结论arr 和 amp;arr[0] 地址相同但类型不同arr 类型是int (*)[3]arr[0] 类型是int *。2.3 数组指针的运算能力偏移规律指针的运算能力偏移步长遵循核心公式指针1 偏移字节数 sizeof(去掉一个*之后的类型)以数组指针int (*p)[3]为例1. 去掉一个*剩余类型是int [3]包含3个int的数组2. 偏移步长 sizeof(int [3]) 3 * 4 12 字节32位系统int占4字节3. 核心规律p 1 会直接跳过一整个行3个int元素指向二维数组的下一行。代码示例数组指针操作二维数组#include stdio.h int main() { int arr[2][3] {{1,2,3}, {4,5,6}}; int (*p)[3] arr; // 数组指针p指向二维数组第一行类型匹配 // 访问第一行元素p指向第一行*p等价于arr[0]类型int* printf(第一行%d %d %d\n, *(*p), *(*p 1), *(*p 2)); // 1 2 3 // p1 指向第二行 p; // 访问第二行元素 printf(第二行%d %d %d\n, *(*p), *(*p 1), *(*p 2)); // 4 5 6 return 0; } ​2.4 数组指针的应用场景工程实战数组指针最核心的应用是「二维数组作为函数参数传递」避免数组整体拷贝节省内存同时能正确访问二维数组的每一行。错误写法无法正确访问二维数组// 错误形参是int*无法接收二维数组首地址类型不匹配 void print_arr(int *arr, int row, int col) { // 无法正确访问arr[i][j] } ​正确写法用数组指针作为形参#include stdio.h ​ // 形参p是数组指针指向包含col个int的数组 void print_arr(int (*p)[3], int row, int col) { for (int i 0; i row; i) { for (int j 0; j col; j) { // pi 指向第i行*(pi) 是第i行首地址*(pi)j 是第i行第j个元素地址 printf(%d , *(*(p i) j)); } printf(\n); } } ​ int main() { int arr[2][3] {{1,2,3}, {4,5,6}}; print_arr(arr, 2, 3); // 实参是二维数组名类型匹配 return 0; } ​2.5 易错点数组指针 vs 指针数组面试高频区分很多人混淆二者核心区分看括号位置判断是“指针”还是“数组”用一句话记住「括号包着指针名 → 数组指针是指针括号不包指针名 → 指针数组是数组」类型定义格式本质类型示例核心区别数组指针int (*p)[3]指针指向数组int (*)[3]p是指针存储数组地址1跳一行指针数组int *p[3]数组存放指针int *[3]p是数组存放3个int*指针1跳一个指针三、指针数组存放指针的数组核心用于存储多字符串指针数组的本质是「数组」和普通数组的区别在于普通数组存储的是数据int、char等而指针数组存储的是「指针变量」地址。由于字符指针可以存储字符串首地址所以指针数组最常用的场景是「存储多个字符串」比二维字符数组更节省内存。3.1 本质与定义指针数组 → 数组数组的每一个元素都是「指针变量」可以存储任意类型的地址int*、char*、void*等。语法格式无括号指针名直接跟数组下标// 格式元素类型 *数组名[数组长度]; char *str[10]; // str是指针数组包含10个char*类型的指针每个指针可存字符串首地址 int *arr[5]; // arr是指针数组包含5个int*类型的指针每个指针可存int变量地址关键没有括号* 优先级低于 []所以str先被定义为数组再结合char *表示“数组的每个元素是char*指针”。3.2 指针数组的内存特点优势以存储多个字符串为例对比「指针数组」和「二维字符数组」的内存占用// 1. 二维字符数组固定长度浪费内存 char str1[3][10] {hello, world, csdn}; // 占用内存3*1030字节即使字符串长度不足10也会占用10字节 // 2. 指针数组灵活节省内存 char *str2[3] {hello, world, csdn}; // 占用内存3*824字节64位系统指针占8字节每个指针指向字符串常量按需占用内存优势指针数组存储多个字符串时无需提前指定固定长度每个字符串按需占用内存比二维字符数组更灵活、更节省空间。3.3 核心应用场景实战重点指针数组最核心的应用有两个1. 存储多个字符串工程开发中最常用2. 接收命令行参数argv 本质就是一个char *类型的指针数组。应用1存储多个字符串#include stdio.h int main() { // 指针数组存储3个字符串每个元素是字符串首地址 char *str[] {C语言, 指针进阶, CSDN博客}; // 遍历指针数组访问每个字符串 for (int i 0; i 3; i) { printf(str[%d] %s\n, i, str[i]); } return 0; }输出结果str[0] C语言str[1] 指针进阶str[2] CSDN博客应用2接收命令行参数argv 详解C语言main函数的标准写法支持命令行参数int main(int argc, char *argv[]);参数解析- argc命令行参数的个数包含程序名本身最少为1- argv指针数组char *argv[]每个元素是一个命令行参数的字符串首地址- argv[0]程序名本身如./a.out- argv[1]第一个命令行参数argv[2]第二个命令行参数以此类推。3.4 实战练习结合命令行参数面试常考本节两个练习均基于命令行参数实现补充完整代码、注释、测试用例直接可运行。练习1将 argv[1] 字符串转换为整型数需求运行程序时传入一个字符串形式的数字如./a.out 896程序将其转换为整型数并输出。核心知识点atoi 函数stdlib.h 头文件用于将字符串转整型命令行参数argv[1] 是字符串首地址。#include stdio.h #include stdlib.h // atoi函数所需头文件 int main(int argc, char *argv[]) { // 校验参数个数至少需要传入1个命令行参数argv[1] if (argc 2) { printf(请传入一个字符串形式的数字\n); printf(用法./a.out 数字字符串\n); return -1; // 异常退出 } // atoi(argv[1])将argv[1]指向的字符串转为整型 int num atoi(argv[1]); // 输出结果 printf(传入的字符串%s\n, argv[1]); printf(转换后的整型数%d\n, num); return 0; }测试用例1. 编译gcc test.c -o a.out2. 运行./a.out 8963. 输出传入的字符串896转换后的整型数896拓展若传入非数字字符串如./a.out abcatoi 会返回0可添加参数校验逻辑可选。练习2实现四则运算命令行参数传入运算数和运算符需求运行程序时传入两个运算数和一个运算符如./a.out 10 9、./a.out 10 x 99程序计算结果并输出。核心知识点命令行参数解析、atoi 转换、switch 判断运算符。#include stdio.h #include stdlib.h int main(int argc, char *argv[]) { // 校验参数个数需要传入3个命令行参数运算数1、运算符、运算数2总个数为4 if (argc ! 4) { printf(参数个数错误\n); printf(用法./a.out 运算数1 运算符 运算数2\n); printf(支持运算符加、-减、x乘、/除\n); return -1; } // 1. 将命令行参数中的字符串转为整型运算数 int a atoi(argv[1]); int b atoi(argv[3]); int res 0; // 存储运算结果 // 2. 判断运算符执行对应运算 // argv[2]是字符串取第一个字符argv[2][0]作为运算符 switch (argv[2][0]) { case : res a b; break; case -: res a - b; break; case x: // 用x表示乘法*在命令行中有特殊含义避免冲突 res a * b; break; case /: // 校验除数不为0 if (b 0) { printf(错误除数不能为0\n); return -1; } res a / b; break; default: printf(无效运算符支持、-、x、/\n); return -1; } // 3. 输出运算结果 printf(%d %c %d %d\n, a, argv[2][0], b, res); return 0; }测试用例1. 运行./a.out 10 9 → 输出10 9 192. 运行./a.out 10 x 99 → 输出10 x 99 9903. 运行./a.out 100 - 25 → 输出100 - 25 754. 运行./a.out 50 / 5 → 输出50 / 5 10易错点命令行中*是通配符不能直接作为运算符所以用x表示乘法。四、函数指针指向函数的指针回调函数基础函数指针是C语言中最灵活、最强大的特性之一也是面试中的“重头戏”。核心记住函数指针是一个指针它存储的是函数的入口地址通过函数指针可以间接调用函数常用于回调函数、通用算法如通用排序、模块化开发。4.1 核心概念必懂1. 函数的地址C语言中函数名本身就是函数的入口地址和数组名类似无需用amp;取地址amp;函数名 和 函数名 等价2. 函数的类型函数的类型由「返回值类型」和「参数列表」共同决定与函数名无关3. 函数指针的作用存储函数地址间接调用函数实现“函数作为参数传递”回调函数。4.2 函数指针的定义语法重点易错语法格式必须加括号否则会变成函数声明// 格式返回值类型 (*函数指针名)(参数列表); // 示例对应函数 size_t strlen(const char *p); size_t (*ptr)(const char *); // ptr是函数指针指向返回值为size_t、参数为const char*的函数关键解析- 括号()提升ptr的优先级确保ptr先被定义为指针- 指针名后面的(const char *) 是函数的参数列表- 最前面的size_t 是函数的返回值类型- 函数指针的类型必须和它指向的函数类型完全匹配返回值、参数列表一致。4.3 函数指针的赋值与调用实战示例以标准库函数strlen为例演示函数指针的赋值、调用过程#include stdio.h #include string.h // strlen函数所需头文件 int main() { // 1. 定义函数指针ptr类型匹配strlen函数 // strlen函数原型size_t strlen(const char *p); size_t (*ptr)(const char *); // 2. 赋值让ptr指向strlen函数函数名就是地址 ptr strlen; // 等价写法ptr strlen;可加可不加效果一致 // 3. 调用函数指针三种写法均等价 // 写法1直接用函数指针调用最常用 size_t len1 ptr(Hello C语言); // 写法2解引用函数指针调用*ptr和ptr等价 size_t len2 (*ptr)(Hello C语言); // 写法3解引用多次仍等价无意义但合法 size_t len3 (**ptr)(Hello C语言); // 输出结果三者一致 printf(len1 %zu, len2 %zu, len3 %zu\n, len1, len2, len3); // 均输出8 return 0; }核心结论函数指针的调用ptr()、(*ptr)()、(**ptr)() 完全等价因为函数名本身就是地址解引用不影响调用逻辑。4.4 函数指针的核心特性面试必记1. 函数指针不能进行 1、-1 等运算函数地址是固定的偏移后无意义编译器会报错或警告2. *ptr ptr函数指针解引用和不解引用完全等价均可调用函数3. amp;函数名 函数名函数名本身就是地址取地址操作不改变其值4. 函数指针的类型必须和指向的函数类型完全匹配返回值、参数个数、参数类型一致否则会出现未定义行为。4.5 函数指针的应用场景工程实战函数指针最核心的应用是「回调函数」——将一个函数作为参数传递给另一个函数在另一个函数中通过函数指针调用该函数实现灵活的逻辑扩展。示例用函数指针实现简单的回调函数加法、减法回调#include stdio.h // 回调函数类型返回int参数为两个int typedef int (*calc_func)(int, int); // 加法函数符合回调函数类型 int add(int a, int b) { return a b; } // 减法函数符合回调函数类型 int sub(int a, int b) { return a - b; } // 通用计算函数接收两个运算数和一个回调函数通过函数指针调用回调函数 int calc(int a, int b, calc_func func) { return func(a, b); // 调用回调函数 } int main() { // 调用calc传入add函数作为回调加法 int res1 calc(10, 5, add); printf(10 5 %d\n, res1); // 输出15 // 调用calc传入sub函数作为回调减法 int res2 calc(10, 5, sub); printf(10 - 5 %d\n, res2); // 输出5 return 0; }拓展后面的“通用冒泡排序”作业就是函数指针回调的典型应用。五、作业【完整版代码实现含注释测试用例易错点说明】本节所有作业均实现完整代码补充详细注释、测试用例、易错点分析直接可编译运行同时贴合工程开发规范如动态内存释放、参数校验。作业将命令行第二个参数字符串倒序生成一个新的字符串并返回函数原型char *reverse_string(const char *ptr);参数说明ptr 指向原来的字符串返回值是新字符串的地址动态开辟需手动释放。实现思路1. 先计算原字符串长度strlen2. 动态开辟新字符串空间长度原长度1预留\0空间3. 从原字符串末尾开始逐字符拷贝到新字符串4. 给新字符串末尾添加\0字符串结束标志5. 返回新字符串地址注意调用者需手动free释放内存避免内存泄漏。完整代码#include stdio.h #include stdlib.h #include string.h /** * brief 将字符串倒序生成新字符串并返回 * param ptr 指向原字符串的指针不可修改 * return 新字符串的地址动态开辟需手动free */ char *reverse_string(const char *ptr) { // 校验参数ptr为NULL直接返回NULL避免野指针 if (ptr NULL) { printf(错误传入的字符串指针为NULL\n); return NULL; } // 1. 计算原字符串长度 int len strlen(ptr); // 2. 动态开辟新字符串空间len1预留\0的位置 char *new_str (char *)malloc(len 1); // 校验内存开辟是否成功 if (new_str NULL) { perror(malloc fail); // 打印错误信息 return NULL; } // 3. 倒序拷贝从原字符串末尾开始逐字符拷贝到新字符串 char *start new_str; // 保存新字符串起始地址用于返回 for (int i len - 1; i 0; i--) { *new_str ptr[i]; new_str; // 新字符串指针后移 } // 4. 给新字符串添加结束标志\0 *new_str \0; // 5. 返回新字符串起始地址 return start; } // 测试函数结合命令行参数 int main(int argc, char *argv[]) { // 校验命令行参数个数至少传入1个参数字符串argv[1] if (argc 2) { printf(请传入一个字符串\n); printf(用法./a.out 字符串\n); return -1; } // 调用倒序函数 char *reverse_str reverse_string(argv[1]); if (reverse_str NULL) { return -1; // 开辟内存失败退出 } // 输出结果 printf(原字符串%s\n, argv[1]); printf(倒序后%s\n, reverse_str); // 手动释放动态开辟的内存避免内存泄漏 free(reverse_str); reverse_str NULL; // 避免野指针 return 0; }测试用例1. 编译gcc reverse.c -o reverse2. 运行./reverse hello → 输出原字符串hello倒序后olleh3. 运行./reverse CSDN → 输出原字符串CSDN倒序后NDSC易错点1. 忘记给新字符串添加\0导致输出乱码2. 动态开辟内存后未校验是否成功new_str NULL3. 调用函数后未free内存导致内存泄漏4. 未校验ptr是否为NULL导致野指针访问。

更多文章