【C语言】深入理解指针(四)

前言
经过前面三讲的铺垫,我们已经掌握了指针的核心概念、与数组/函数的结合用法。这一讲作为指针系列的收尾,将聚焦实战与巩固,先厘清sizeof与strlen的核心差异,再深入回调函数的本质的,通过qsort函数的使用与模拟实现掌握泛型编程思路,最后解析高频数组与指针笔试题,助力你彻底打通指针的“任督二脉”,应对面试与开发中的各类场景。
一、sizeof与strlen的核心差异
sizeof与strlen是C语言中最易混淆的两个“长度相关工具”,但二者本质完全不同:sizeof是计算内存大小的操作符,strlen是统计字符串长度的库函数。
1.1 核心区别对比
| 特性 | sizeof | strlen |
|---|---|---|
| 本质 | 操作符(编译器内置) | 库函数(需包含<string.h>头文件) |
| 功能 | 计算操作数占用的内存大小(单位:字节) | 统计字符串中\0之前的字符个数 |
| 关注对象 | 只看内存大小,不关心内存中存储的数据 | 依赖\0终止符,无\0则越界查找 |
| 适用场景 | 任意变量、数组、类型(如sizeof(int)) | 仅适用于字符串(char数组/字符指针) |
1.2 代码示例
我们通过代码直观感受一下差异:
#include<stdio.h>#include<string.h>// strlen需包含头文件intmain(){// 无\0的字符数组char arr1[]={'a','b','c'};// 有\0的字符串(编译器自动添加)char arr2[]="abc";// 测试strlen:找\0终止符printf("strlen(arr1) = %zd\n",strlen(arr1));// 随机值(越界查找)printf("strlen(arr2) = %zd\n",strlen(arr2));// 3('a'/'b'/'c'后接\0)// 测试sizeof:计算内存大小printf("sizeof(arr1) = %zd\n",sizeof(arr1));// 3(3个char,1字节/个)printf("sizeof(arr2) = %zd\n",sizeof(arr2));// 4(3个字符+1个\0)return0;}输出结果:
strlen(arr1) = 35
strlen(arr2) = 3
sizeof(arr1) = 3
sizeof(arr2) = 4
关键结论:
arr1无\0,strlen会一直向后找\0,导致越界(结果随机)。arr2是字符串常量,编译器自动在末尾添加\0,strlen统计到\0停止,sizeof包含\0的1字节。
二、回调函数
回调函数是指针的高级应用,核心逻辑是:将函数的地址作为参数传递给另一个函数,在需要时通过该地址调用目标函数。简单说,就是“委托别人办事,办完后回调通知”。
2.1 回调函数的定义及其核心价值
- 本质:通过函数指针实现“动态调用”——调用者无需关心被调用函数的具体实现,只需约定参数和返回值类型。
- 核心价值:减少代码冗余,提升程序灵活性(如支持自定义规则)。
2.2 案例:用回调函数改造计算器
回顾我们前一讲的计算器案例,我们会发现传统的switch实现方式会存在大量的重复代码(输入操作数、打印结果)。但我们若用回调函数来实现的话,则可将重复逻辑抽离为通用函数calc,仅通过函数指针传递不同运算逻辑,提高编程效率。
#include<stdio.h>// 四则运算函数(参数/返回值类型一致,符合回调约定)intadd(int a,int b){return a + b;}intsub(int a,int b){return a - b;}intmul(int a,int b){return a * b;}intdiv(int a,int b){return a / b;}// 回调函数的载体:接收函数指针,执行通用逻辑voidcalc(int(*pf)(int,int)){int x, y, ret;printf("输入操作数:");scanf("%d %d",&x,&y); ret =pf(x, y);// 调用传递进来的函数(回调)printf("ret = %d\n", ret);}intmain(){int input =1;do{printf("*************************\n");printf(" 1:add 2:sub 3:mul 4:div\n");printf(" 0:exit\n");printf("*************************\n");printf("请选择:");scanf("%d",&input);switch(input){case1:calc(add);break;// 传递add函数地址case2:calc(sub);break;// 传递sub函数地址case3:calc(mul);break;// 传递mul函数地址case4:calc(div);break;// 传递div函数地址case0:printf("退出程序\n");break;default:printf("选择错误\n");}}while(input !=0);return0;}关键点:
- 通用逻辑(输入、打印)抽离到
calc函数,减少重复代码。 - 新增运算时,只需添加函数并在
switch中传递地址,无需修改通用逻辑(回调函数的灵活性)。
三、qsort函数
qsort是C标准库中的快速排序函数,支持对任意类型数据排序(int、结构体、字符串等),其核心是通过回调函数实现“自定义比较规则”(本质是泛型编程)。
3.1 qsort函数的使用格式
voidqsort(void*base,size_t nmemb,size_t size,int(*compar)(constvoid*,constvoid*));参数解析:
base:待排序数组的首地址(任意类型,故用void*)。nmemb:数组元素个数;size:每个元素的字节大小(如sizeof(int))。compar:比较函数指针(回调函数),定义两个元素的比较规则。
比较函数规则:
- 若
a > b,返回正数。 - 若
a == b,返回0。 - 若
a < b,返回负数。
3.2 qsort案例:排序不同类型数据
示例1:排序整型数组
#include<stdio.h>#include<stdlib.h>// qsort所在头文件// 比较函数:升序排序intintint_cmp(constvoid*p1,constvoid*p2){// void*需强制转换为int*,再解引用return*(int*)p1 -*(int*)p2;}intmain(){int arr[]={1,3,5,7,9,2,4,6,8,0};int sz =sizeof(arr)/sizeof(arr[0]);// 调用qsort:传递数组首地址、元素个数、元素大小、比较函数qsort(arr, sz,sizeof(int), int_cmp);// 打印结果for(int i =0; i < sz; i++){printf("%d ", arr[i]);// 输出:0 1 2 3 4 5 6 7 8 9}return0;}示例2:排序结构体数组(按年龄/名字)
#include<stdio.h>#include<stdlib.h>#include<string.h>// strcmp所在头文件// 定义学生结构体structStu{char name[20];// 名字int age;// 年龄};// 比较函数1:按年龄升序intcmp_stu_age(constvoid*p1,constvoid*p2){return((structStu*)p1)->age -((structStu*)p2)->age;}// 比较函数2:按名字字典序升序(strcmp比较字符串)intcmp_stu_name(constvoid*p1,constvoid*p2){returnstrcmp(((structStu*)p1)->name,((structStu*)p2)->name);}intmain(){structStu s[]={{"zhangsan",20},{"lisi",30},{"wangwu",15}};int sz =sizeof(s)/sizeof(s[0]);// 按年龄排序qsort(s, sz,sizeof(s[0]), cmp_stu_age);// 按名字排序qsort(s, sz,sizeof(s[0]), cmp_stu_name);return0;}3.3 模拟实现qsort
qsort的核心是“通用排序逻辑+自定义比较规则”,我们用冒泡排序思想模拟实现,关键在于:
- 用
void*接收任意类型数据。 - 按字节交换元素(适配任意类型)。
- 通过回调函数获取比较结果。
#include<stdio.h>// 辅助函数:按字节交换两个元素(核心:适配任意类型)void_swap(void*p1,void*p2,int size){for(int i =0; i < size; i++){// 强制转换为char*,每次交换1字节char tmp =*((char*)p1 + i);*((char*)p1 + i)=*((char*)p2 + i);*((char*)p2 + i)= tmp;}}// 模拟qsort:冒泡排序+回调函数voidmy_qsort(void*base,int nmemb,int size,int(*compar)(constvoid*,constvoid*)){// 冒泡排序外层循环:控制趟数for(int i =0; i < nmemb -1; i++){// 内层循环:控制每趟比较次数for(int j =0; j < nmemb - i -1; j++){// 计算第j个和第j+1个元素的地址void*elem1 =(char*)base + j * size;void*elem2 =(char*)base +(j+1)* size;// 回调比较函数:若elem1>elem2,交换if(compar(elem1, elem2)>0){_swap(elem1, elem2, size);}}}}// 测试:用模拟的my_qsort排序整型数组intint_cmp(constvoid*p1,constvoid*p2){return*(int*)p1 -*(int*)p2;}intmain(){int arr[]={1,3,5,7,9,2,4,6,8,0};int sz =sizeof(arr)/sizeof(arr[0]);my_qsort(arr, sz,sizeof(int), int_cmp);for(int i =0; i < sz; i++){printf("%d ", arr[i]);// 输出:0 1 2 3 4 5 6 7 8 9}return0;}关键点:
void*无法直接解引用,需强制转换为char*(按字节操作)。- 交换逻辑按字节实现,无论元素是int(4字节)、结构体(N字节),都能适配。
- 比较规则由回调函数决定,实现“排序逻辑通用,比较规则自定义”。
四、高频笔试题精析
数组与指针的笔试题是我们求职就业中面试的高频考点,核心考察“数组名的不同含义”“指针运算的步长”“类型转换的影响”。我们将通过解析以下7道经典题,助你举一反三。
4.1 数组名的3种含义
sizeof(数组名):数组名表示整个数组,计算数组总大小。&数组名:数组名表示整个数组,取出整个数组的地址。- 其他场景(如
数组名+1、数组传参):数组名表示首元素地址。
4.2 题目1:一维数组sizeof计算
int a[]={1,2,3,4};printf("%zd\n",sizeof(a));// 16(4个int,4字节/个,总大小4*4)printf("%zd\n",sizeof(a+0));// 4/8(a是首元素地址,a+0仍是地址,指针大小)printf("%zd\n",sizeof(*a));// 4(a是首元素地址,*a是首元素,int大小)printf("%zd\n",sizeof(&a));// 4/8(&a是数组地址,本质是指针,指针大小)printf("%zd\n",sizeof(&a+1));// 4/8(&a+1是下一个数组的地址,仍是指针)考点:数组名的含义、指针大小与平台相关(32位4字节,64位8字节)。
4.3 题目2:字符数组strlen计算
char arr[]={'a','b','c','d','e','f'};printf("%zd\n",strlen(arr));// 随机值(无\0,越界查找)printf("%zd\n",strlen(&arr));// 随机值(&arr是数组地址,仍无\0)printf("%zd\n",sizeof(arr));// 6(6个char,1字节/个)考点:strlen依赖\0,sizeof计算实际内存大小。
4.4 题目3:指针运算与类型转换
int a[5]={1,2,3,4,5};int*ptr =(int*)(&a +1);printf("%d,%d\n",*(a +1),*(ptr -1));// 2,5解析:
&a是数组地址,&a+1跳过整个数组(5个int,20字节),指向数组末尾后。ptr是int*类型,ptr-1跳过1个int(4字节),指向数组最后一个元素5。a+1是首元素地址+1,指向第二个元素2,*a+1是2。
4.5 题目4:结构体指针运算
// x86环境(32位),结构体大小20字节structTest{int Num;char*pcName;short sDate;char cha[2];short sBa[4];}*p =(structTest*)0x100000;printf("%p\n", p +0x1);// 0x100014(p是结构体指针,+1跳过20字节=0x14)printf("%p\n",(unsignedlong)p +0x1);// 0x100001(强制转为长整型,+1是数值+1)printf("%p\n",(unsignedint*)p +0x1);// 0x100004(强制转为int*,+1跳过4字节)考点:指针运算的步长由指针类型决定(结构体指针步长=结构体大小)。
4.6 题目5:二维数组的指针访问
int a[3][2]={(0,1),(2,3),(4,5)};// 注意:逗号表达式,实际初始化{1,3,5}int*p = a[0];printf("%d\n", p[0]);// 1解析:
- 逗号表达式
(0,1)结果为1,数组实际初始化是{{1,3}, {5,0}, {0,0}}。 a[0]是第一行首元素地址,p[0]是第一行第一个元素1。
4.7 题目6:指针数组与二级指针
char*a[]={"work","at","alibaba"};// 指针数组:每个元素是字符串首地址char**pa = a;// pa指向a[0]("work"的地址) pa++;// pa指向a[1]("at"的地址)printf("%s\n",*pa);// 输出"at"考点:指针数组的存储逻辑(元素是地址)、二级指针的运算。
4.8 题目7:三级指针复杂运算
char*c[]={"ENTER","NEW","POINT","FIRST"};char**cp[]={c+3,c+2,c+1,c};// cp是二级指针数组,元素是c的地址char***cpp = cp;// cpp是三级指针,指向cp[0]printf("%s\n",**++cpp);// POINT(++cpp指向cp[1]=c+2,**cp[1]是c[2]="POINT")printf("%s\n",*--*++cpp+3);// ER(++cpp指向cp[2]=c+1,--*cp[2]=c+0,*c+0是"ENTER"+3="ER")printf("%s\n",*cpp[-2]+3);// ST(cpp[-2]=cp[0]=c+3,*c+3是"FIRST"+3="ST")printf("%s\n", cpp[-1][-1]+1);// EW(cpp[-1]=cp[1]=c+2,cp[1][-1]=c+1,*c+1是"NEW"+1="EW")考点:多级指针的运算、指针数组的地址访问,需画图梳理内存关系。
至此,我们的C语言指针系列章节已全部结束!从内存与地址的底层逻辑出发,我们一步步攻克了指针变量、数组与指针的绑定、函数指针与回调函数、二级指针与指针数组等核心知识点,最终通过qsort实战与经典笔试题完成了知识闭环。
指针作为C语言的“灵魂”,其核心本质是对内存地址的直接操作,而掌握其这几章的主要内容,便打通了指针的学习脉络。它不仅能帮你写出更高效、灵活的代码,更能让你看透计算机内存管理的底层逻辑,为后续操作系统、嵌入式等进阶学习打下坚实基础。
指针的学习没有捷径,唯有“理解概念+多写代码+调试观察”三者结合。希望这一系列讲解能帮你告别指针恐惧,真正驾驭这把C语言的“利器”,在编程之路上走得更稳、更远!
以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正。
