C语言指针入门到理解:一篇文章系统梳理指针核心知识(3)

张开发
2026/4/17 2:20:39 15 分钟阅读

分享文章

C语言指针入门到理解:一篇文章系统梳理指针核心知识(3)
C语言指针入门到理解一篇文章系统梳理指针核心知识3前两篇文字我们已经把指针的基础和数组相关内容系统梳理过了比如指针的本质指针和数组的关系一维数组、二维数组的传参二级指针指针数组这一篇继续往下走进入指针相关的更进一步的内容字符指针变量到底存的是什么数组指针和指针数组怎么区分二维数组传参为什么可以写成指针形式函数指针到底是什么函数指针数组有什么实际用途什么是转移表这部分是 C 语言指针体系里看上去比较困难但实际上只要抓住“类型决定意义”这条主线其实并不难。一、字符指针变量它存的不是字符串本身我们先看一个最常见的字符指针写法charchw;char*pcch;*pcw;这个很好理解pc是一个字符指针里面存的是字符变量ch的地址。但很多同学真正困惑的是下面这种写法constchar*pstrhello world.;printf(%s\n,pstr);很多人第一眼会误以为是把字符串hello world.整体放进了指针变量pstr其实不是。这句代码的本质是把字符串常量hello world.的首字符地址存放到字符指针pstr中。也就是说pstr里存放的是首字符h的地址而不是整个字符串对象本身。这里为什么要写const char*因为字符串字面量通常存放在常量区不应该通过指针去修改它所以更合理的写法是constchar*pstrhello bit.;如果这里无法理解的话请联想一些常量的赋值是违规的操作例如35//这种操作二、一个经典面试题为什么有的字符串地址相同有的不同看下面这段代码#includestdio.hintmain(){charstr1[]hello world.;charstr2[]hello world.;constchar*str3hello world.;constchar*str4hello world.;if(str1str2)printf(str1 and str2 are same\n);elseprintf(str1 and str2 are not same\n);if(str3str4)printf(str3 and str4 are same\n);elseprintf(str3 and str4 are not same\n);return0;}运行结果通常是str1 and str2 are not same str3 and str4 are same为什么会这样1.str1和str2为什么不同因为charstr1[]hello world.;charstr2[]hello world.;这是用同样的内容初始化了两个不同的数组。数组初始化时会各自开辟独立空间所以str1和str2不是同一块内存。2.str3和str4为什么相同因为constchar*str3hello world.;constchar*str4hello world.;这里不是创建数组而是让两个指针都去指向同一个字符串常量。编译器通常会把相同的字符串常量放到同一块常量区内存中所以str3和str4很可能相等。ps这也同样可以进一步佐证前文为何要加const因为这是一个常量所以我们不希望它被修改。总结数组名比较的是各自数组首元素地址字符指针比较的是它们指向的常量字符串地址所以这个例子非常适合理解“字符数组”和“字符指针”虽然都能处理字符串但底层模型并不一样。三、数组指针变量它是指针不是数组这一块是最容易和“指针数组”混掉的地方。先看两个定义int*p1[10];int(*p2)[10];很多人会懵到底哪个是数组指针答案是int(*p2)[10];才是数组指针变量。四、什么是数组指针数组指针本质上是一个指针变量这个指针指向的是数组。例如int(*p)[10];怎么理解这句先看优先级因为[]的优先级高于*所以必须加括号(*p)这表示先说明p是一个指针。然后再看(*p)[10]表示p指向一个有 10 个元素的数组。如果数组元素类型是int那最终它的含义就是p是一个指针指向一个int[10]类型的数组。五、数组指针怎么初始化既然数组指针是“指向数组的指针”那它存的就应该是数组的地址。而数组的地址怎么取用数组名例如intarr[10]{0};int(*p)[10]arr;这里arr是整个数组的地址p是数组指针变量p和arr类型一致数组指针拆解理解int(*p)[10]arr;可以拆成三层p是变量名*p说明p是指针(*p)[10]说明它指向一个有 10 个int元素的数组所以记忆方法很简单数组指针是指针指针数组是数组。六、二维数组传参的本质传的是第一行的地址先看一个常见写法#includestdio.hvoidtest(inta[3][5],intr,intc){inti0;intj0;for(i0;ir;i){for(j0;jc;j){printf(%d ,a[i][j]);}printf(\n);}}intmain(){intarr[3][5]{{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};test(arr,3,5);return0;}这段代码大家通常都见过但很多人并没有真正理解二维数组传参传过去的到底是什么七、二维数组本质上是什么二维数组可以理解为每个元素都是一维数组的数组比如intarr[3][5];它可以理解成整个数组有 3 行每一行是一个int[5]的一维数组也就是说二维数组的首元素不是一个int而是第一行这个一维数组。所以根据“数组名表示首元素地址”的规则arr表示的不是单个int的地址而是第一行的地址。第一行的类型是int[5]那么第一行地址的类型就是int(*)[5]这正好就是数组指针类型。八、所以二维数组形参也可以写成数组指针于是二维数组传参完全可以写成下面这样#includestdio.hvoidtest(int(*p)[5],intr,intc){inti0;intj0;for(i0;ir;i){for(j0;jc;j){printf(%d ,*(*(pi)j));}printf(\n);}}intmain(){intarr[3][5]{{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};test(arr,3,5);return0;}这里p i表示走到第i行*(p i)表示第i行这个一维数组*(p i) j表示第i行第j个元素地址*(*(p i) j)才是真正的元素值ps这里如果忘记了*操作符的作用请回顾我上篇文章所以结论是二维数组传参时形参既可以写成inta[3][5]也可以写成int(*p)[5]但无论哪种写法本质上都是在接收第一行的地址。九、函数指针变量它存放的是函数地址前面学了很多种指针整型指针存整型变量地址字符指针存字符地址数组指针存数组地址那自然就会想到能不能有一种指针专门用来存函数地址答案当然是可以这就是函数指针变量。十、函数真的有地址吗先看例子#includestdio.hvoidtest(){printf(hehe\n);}intmain(){printf(test: %p\n,test);printf(test: %p\n,test);return0;}你会发现testtest都能打印出函数地址而且结果通常一样。这说明函数名本身就表示函数地址也可以用函数名来取地址。十一、函数指针怎么定义例如有这样一个函数intAdd(intx,inty){returnxy;}那对应的函数指针可以写成int(*pf)(int,int)Add;也可以写成int(*pf)(int,int)Add;这两种写法都可以。如何理解这个定义int(*pf)(int,int)拆开看pf是变量名*pf说明它是一个指针(int, int)说明它指向的函数参数是两个int最前面的int说明该函数返回值类型是int所以这句的完整含义是pf是一个函数指针指向的函数形参是(int, int)返回值类型是int十二、函数指针怎么调用函数有了函数指针之后可以通过它调用对应函数#includestdio.hintAdd(intx,inty){returnxy;}intmain(){int(*pf)(int,int)Add;printf(%d\n,(*pf)(2,3));printf(%d\n,pf(3,5));return0;}输出58这里两种调用方式都对(*pf)(2,3);pf(3,5);因为对于函数指针来说前面的* 在写的时候可以省略所以平时更常写的是第二种简洁一些。十三、复杂函数指针看不懂怎么办用 typedef函数指针类型一复杂代码可读性会迅速下降。比如说void(*signal(int,void(*)(int)))(int);这种写法一眼看过去确实头大。这时候最好的解决方案就是用 typedef 给复杂类型起别名例如typedefvoid(*pfun_t)(int);typedeflonglongLL;这表示把void(*)(int)重命名为pfun_t那上面那句复杂定义就能简化为pfun_tsignal(int,pfun_t);可读性一下就上来了。除了函数指针数组指针也可以这样简化例如typedefint(*parr_t)[5];以后看到parr_t你就知道它是“指向int[5]的数组指针类型”。十四、函数指针数组数组里存的是函数地址前面学过int*arr[10];这是指针数组表示数组中每个元素都是int*。那如果数组中每个元素都是“函数指针”就得到了函数指针数组例如int(*parr1[3])();这个定义中parr1先和[]结合说明它是数组数组元素类型是int (*)()也就是函数指针所以parr1是一个函数指针数组。十五、函数指针数组最典型的用途转移表函数指针数组最经典的应用场景就是转移表。比如我们要实现一个简单计算器1加法2减法3乘法4除法传统写法通常会用switch-caseswitch(input){case1:retadd(x,y);break;case2:retsub(x,y);break;case3:retmul(x,y);break;case4:retdiv(x,y);break;}这种写法当然能用但如果功能越来越多switch-case会越来越臃肿。这时就可以用函数指针数组优化。十六、用函数指针数组实现计算器#includestdio.hintadd(inta,intb){returnab;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput1;intret0;int(*p[5])(int,int){0,add,sub,mul,div};// 转移表do{printf(*************************\n);printf( 1:add 2:sub \n);printf( 3:mul 4:div \n);printf( 0:exit \n);printf(*************************\n);printf(请选择);scanf(%d,input);if(input1input4){printf(输入操作数);scanf(%d %d,x,y);ret(*p[input])(x,y);printf(ret %d\n,ret);}elseif(input0){printf(退出计算器\n);}else{printf(输入有误\n);}}while(input);return0;}这里最关键的一句int(*p[5])(int,int){0,add,sub,mul,div};这就是一个函数指针数组也就是转移表。它的含义是p[1]指向addp[2]指向subp[3]指向mulp[4]指向div之后只需要根据用户输入直接调用(*p[input])(x,y);这样就把“菜单选择 - 函数跳转”的流程做成了表驱动结构。这就是所谓的转移表它的优势在于结构更清晰扩展更方便代码更容易维护十七、这一篇的几个易错点最后把本篇最容易出错的地方集中总结一下。1. 字符指针不等于字符数组constchar*phello;这里p里存的是首字符地址不是把整个字符串“装进了指针”。2. 相同内容的字符数组不一定地址相同charstr1[]abc;charstr2[]abc;这是两个不同数组地址不同。3. 相同内容的字符串常量指针可能地址相同constchar*p1abc;constchar*p2abc;它们可能指向同一个常量区字符串。4. 数组指针是指针不是数组int(*p)[10];p是指针指向一个int[10]数组。5. 指针数组是数组不是指针int*arr[10];arr是数组元素类型是int*。6. 二维数组传参本质上传的是第一行地址所以形参可以写成inta[][5]也可以写成int(*p)[5]7. 函数指针的关键是先看pf和谁结合int(*pf)(int,int);先看(*pf)说明pf是指针再看后面的参数列表和前面的返回值类型。8. 函数指针数组的本质还是数组只是数组里的元素不再是普通数据而是“函数地址”。十八、总结这一篇我们的主要结论如下字符指针存的是字符地址字符串字面量本质上传递的是首字符地址。数组指针是“指向数组的指针”指针数组是“存放指针的数组”。二维数组传参本质上传的是第一行的地址所以形参可以写成数组指针。函数名就是函数地址函数指针变量就是用来存函数地址的。函数指针数组可以构造转移表让代码从分支驱动变成表驱动从而更好的维护自己的项目。实际上在学习指针这一块的时候有一个很大的诀窍那就是先认清变量是谁再看它指向什么。

更多文章