前言
经过前面三讲的铺垫,我们已经掌握了指针的核心概念、与数组/函数的结合用法。这一讲作为指针系列的收尾,将聚焦实战与巩固 ,先厘清sizeof与strlen的核心差异,再深入回调函数的本质的,通过qsort函数的使用与模拟实现掌握泛型编程思路,最后解析高频数组与指针笔试题,助力你彻底打通指针的"任督二脉",应对面试与开发中的各类场景。
一、sizeof与strlen的核心差异
sizeof与strlen是C语言中最易混淆的两个"长度相关工具",但二者本质完全不同:sizeof是计算内存大小的操作符,strlen是统计字符串长度的库函数。
1.1 核心区别对比
| 特性 | sizeof | strlen |
|---|---|---|
| 本质 | 操作符(编译器内置) | 库函数(需包含<string.h>头文件) |
| 功能 | 计算操作数占用的内存大小(单位:字节) | 统计字符串中\0之前的字符个数 |
| 关注对象 | 只看内存大小,不关心内存中存储的数据 | 依赖\0终止符,无\0则越界查找 |
| 适用场景 | 任意变量、数组、类型(如sizeof(int)) |
仅适用于字符串(char数组/字符指针) |
1.2 代码示例
我们通过代码直观感受一下差异:
c
#include <stdio.h>
#include <string.h> // strlen需包含头文件
int main() {
// 无\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)
return 0;
}
输出结果:
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,仅通过函数指针传递不同运算逻辑,提高编程效率。
c
#include <stdio.h>
// 四则运算函数(参数/返回值类型一致,符合回调约定)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }
// 回调函数的载体:接收函数指针,执行通用逻辑
void calc(int (*pf)(int, int)) {
int x, y, ret;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y); // 调用传递进来的函数(回调)
printf("ret = %d\n", ret);
}
int main() {
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) {
case 1: calc(add); break; // 传递add函数地址
case 2: calc(sub); break; // 传递sub函数地址
case 3: calc(mul); break; // 传递mul函数地址
case 4: calc(div); break; // 传递div函数地址
case 0: printf("退出程序\n"); break;
default: printf("选择错误\n");
}
} while (input != 0);
return 0;
}
关键点:
- 通用逻辑(输入、打印)抽离到
calc函数,减少重复代码。 - 新增运算时,只需添加函数并在
switch中传递地址,无需修改通用逻辑(回调函数的灵活性)。
三、qsort函数
qsort是C标准库中的快速排序函数,支持对任意类型数据排序(int、结构体、字符串等),其核心是通过回调函数实现"自定义比较规则"(本质是泛型编程)。
3.1 qsort函数的使用格式
c
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
参数解析:
base:待排序数组的首地址(任意类型,故用void*)。nmemb:数组元素个数;size:每个元素的字节大小(如sizeof(int))。compar:比较函数指针(回调函数),定义两个元素的比较规则。
比较函数规则:
- 若
a > b,返回正数。 - 若
a == b,返回0。 - 若
a < b,返回负数。
3.2 qsort案例:排序不同类型数据
示例1:排序整型数组
c
#include <stdio.h>
#include <stdlib.h> // qsort所在头文件
// 比较函数:升序排序int
int int_cmp(const void *p1, const void *p2) {
// void*需强制转换为int*,再解引用
return *(int*)p1 - *(int*)p2;
}
int main() {
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
}
return 0;
}
示例2:排序结构体数组(按年龄/名字)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // strcmp所在头文件
// 定义学生结构体
struct Stu {
char name[20]; // 名字
int age; // 年龄
};
// 比较函数1:按年龄升序
int cmp_stu_age(const void *p1, const void *p2) {
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
// 比较函数2:按名字字典序升序(strcmp比较字符串)
int cmp_stu_name(const void *p1, const void *p2) {
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int main() {
struct Stu 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);
return 0;
}
3.3 模拟实现qsort
qsort的核心是"通用排序逻辑+自定义比较规则",我们用冒泡排序思想模拟实现,关键在于:
- 用
void*接收任意类型数据。 - 按字节交换元素(适配任意类型)。
- 通过回调函数获取比较结果。
c
#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:冒泡排序+回调函数
void my_qsort(void *base, int nmemb, int size,
int (*compar)(const void *, const void *)) {
// 冒泡排序外层循环:控制趟数
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排序整型数组
int int_cmp(const void *p1, const void *p2) {
return *(int*)p1 - *(int*)p2;
}
int main() {
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
}
return 0;
}
关键点:
void*无法直接解引用,需强制转换为char*(按字节操作)。- 交换逻辑按字节实现,无论元素是int(4字节)、结构体(N字节),都能适配。
- 比较规则由回调函数决定,实现"排序逻辑通用,比较规则自定义"。
四、高频笔试题精析
数组与指针的笔试题是我们求职就业中面试的高频考点,核心考察"数组名的不同含义""指针运算的步长""类型转换的影响"。我们将通过解析以下7道经典题,助你举一反三。
4.1 数组名的3种含义
sizeof(数组名):数组名表示整个数组,计算数组总大小。&数组名:数组名表示整个数组,取出整个数组的地址。- 其他场景(如
数组名+1、数组传参):数组名表示首元素地址。
4.2 题目1:一维数组sizeof计算
c
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计算
c
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:指针运算与类型转换
c
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:结构体指针运算
c
// x86环境(32位),结构体大小20字节
struct Test { int Num; char *pcName; short sDate; char cha[2]; short sBa[4]; }*p = (struct Test*)0x100000;
printf("%p\n", p + 0x1); // 0x100014(p是结构体指针,+1跳过20字节=0x14)
printf("%p\n", (unsigned long)p + 0x1); // 0x100001(强制转为长整型,+1是数值+1)
printf("%p\n", (unsigned int*)p + 0x1); // 0x100004(强制转为int*,+1跳过4字节)
考点:指针运算的步长由指针类型决定(结构体指针步长=结构体大小)。
4.6 题目5:二维数组的指针访问
c
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:指针数组与二级指针
c
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:三级指针复杂运算
c
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语言的"利器",在编程之路上走得更稳、更远!