当前位置: 首页 > news >正文

深入理解C语言指针:从回调函数到数组指针笔试题全解析(下)

指针是C语言的灵魂,也是初学者的难点。本文将结合回调函数、qsort使用与实现、sizeofstrlen对比、数组指针笔试题等核心知识点,通过代码示例与深度解析,帮你彻底掌握指针的核心用法。

一、回调函数:函数指针的高级应用

1.1 什么是回调函数?

回调函数是通过函数指针调用的函数。当我们将函数的地址(指针)作为参数传递给另一个函数,且该指针被用来调用目标函数时,这个被调用的函数就是回调函数。其核心特点是:不由实现方直接调用,而是由其他方在特定条件下触发调用

1.2 回调函数实战:计算器代码优化

在未使用回调函数时,计算器的输入输出逻辑存在大量冗余。以下是优化前后的代码对比:

优化前:冗余代码严重
#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 menu()
{printf("********************\n");printf("*** 1:add  2:sub ***\n");printf("*** 3:mul  4:div ***\n");printf("*** 0.exit *********\n");printf("********************\n");
}int main() {int x, y, ret, input = 1;do {menu();printf("请选择:");scanf("%d", &input);// 重复的输入输出逻辑printf("输入操作数:");scanf("%d %d", &x, &y);switch (input) {case 1: ret = add(x, y); break;case 2: ret = sub(x, y); break;case 3: ret = mul(x, y); break;case 4: ret = div(x, y); break;case 0: printf("退出程序\n"); break;default: printf("选择错误\n"); break;}printf("ret = %d\n", ret);} while (input);return 0;
}
优化后:用回调函数消除冗余
#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 menu()
{printf("********************\n");printf("*** 1:add  2:sub ***\n");printf("*** 3:mul  4:div ***\n");printf("*** 0.exit *********\n");printf("********************\n");
}// 回调函数的接收者:通过函数指针调用目标函数
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 {menu();printf("请选择:");scanf("%d", &input);switch (input) {case 1: calc(add); break;  // 传递函数地址case 2: calc(sub); break;case 3: calc(mul); break;case 4: calc(div); break;case 0: printf("退出程序\n"); break;default: printf("选择错误\n"); break;}} while (input);return 0;
}

优化核心:将重复的输入输出逻辑封装到calc函数中,通过函数指针pf动态调用不同的计算函数(add/sub等),减少冗余代码。

二、qsort函数:回调函数的经典应用

qsort是C语言标准库中的快速排序函数,支持任意类型数据的排序,其核心原理就是通过回调函数实现自定义比较逻辑。

2.1 qsort函数原型

void qsort(void* base,        // 待排序数组的首地址size_t nmemb,      // 数组元素个数size_t size,       // 单个元素的大小(字节)int (*compar)(const void*, const void*)  // 比较函数(回调函数)
);

2.2 qsort使用举例

例1:排序整型数组
#include <stdio.h>
#include <stdlib.h>  // qsort所在头文件// 比较函数:升序排序
int int_cmp(const void* p1, const void* p2) {// void*指针需强转为具体类型才能解引用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:排序结构体数组

按年龄或姓名排序学生结构体:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>  // strcmp所在头文件// 学生结构体
struct Stu {char name[20];  // 姓名int age;        // 年龄
};// 按年龄比较
int cmp_stu_by_age(const void* e1, const void* e2) {return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}// 按姓名比较(使用strcmp库函数(字符串比较))
int cmp_stu_by_name(const void* e1, const void* e2) {return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}// 按年龄排序测试
void test_age() {struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);// 排序后年龄顺序:15(wangwu)、20(zhangsan)、30(lisi)
}// 按姓名排序测试
void test_name() {struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);// 排序后姓名顺序:lisi、wangwu、zhangsan(按ASCII码)
}int main() {test_age();test_name();return 0;
}

2.3 模拟实现qsort(冒泡排序版)

核心思路:用void*接收任意类型数组,通过回调函数比较元素,按字节交换元素。

#include <stdio.h>// 按字节交换两个元素
void _swap(void* p1, void* p2, int size) {int i = 0;for (i = 0; i < size; i++) {// 强转为char*实现单字节操作char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}// 模拟qsort(冒泡排序)
void bubble_sort(void* base,        // 数组首地址int count,         // 元素个数int size,          // 单个元素大小int (*cmp)(void*, void*)  // 比较函数
) {int i = 0;for (i = 0; i < count - 1; i++) {  // 控制趟数int j = 0;for (j = 0; j < count - i - 1; j++) {  // 每趟比较次数// 比较第j和j+1个元素if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}// 整型比较函数
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]);bubble_sort(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*后按字节操作。
  • 按字节交换:通过char*指针逐个字节交换,支持任意大小的元素(如int、结构体)。

三、sizeof与strlen:易混淆的"长度计算"工具

3.1 核心区别对比

特性sizeofstrlen
本质操作符(不是函数)库函数(需包含<string.h>
功能计算变量/类型占用的内存大小(字节)计算字符串中\0之前的字符个数
关注对象内存大小,与数据内容无关字符串内容,依赖\0作为结束标志
越界风险无(编译期确定大小)有(无\0时会持续越界查找)
语法示例sizeof(arr)sizeof(int)strlen("abc")strlen(p)

3.2 实战解析:数组的sizeof与strlen

例1:一维整型数组
int a[] = {1, 2, 3, 4};
printf("%d\n", sizeof(a));      // 16(数组总大小:4元素×4字节)
printf("%d\n", sizeof(a + 0));  // 4/8(首元素地址,指针大小)
printf("%d\n", sizeof(*a));     // 4(首元素大小,int类型)
printf("%d\n", sizeof(a[1]));   // 4(第二个元素大小)
printf("%d\n", sizeof(&a));     // 4/8(数组地址,指针大小)
printf("%d\n", sizeof(*&a));    // 16(*&a等价于a,数组总大小)
printf("%d\n", sizeof(&a + 1)); // 4/8(跳过整个数组的地址)
例2:字符数组(无显式\0
char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
// sizeof:不依赖\0,计算内存大小
printf("%d\n", sizeof(arr));      // 6(6个char元素)
printf("%d\n", sizeof(arr + 0));  // 4/8(首元素地址)// strlen:依赖\0,无\0则越界,结果随机
printf("%d\n", strlen(arr));       // 随机值(无\0结束符)
printf("%d\n", strlen(&arr));      // 随机值(同arr,无\0)
例3:字符串数组(含隐式\0
char arr[] = "abcdef";  // 实际存储:'a','b','c','d','e','f','\0'
// sizeof:包含\0
printf("%d\n", sizeof(arr));      // 7(6字符+1个\0)// strlen:统计\0之前的字符
printf("%d\n", strlen(arr));       // 6(\0前共6个字符)
printf("%d\n", strlen(&arr[0] + 1));// 5(从'b'开始统计)
例4:字符指针指向字符串
char* p = "abcdef";  // 字符串常量,末尾有\0
printf("%d\n", sizeof(p));      // 4/8(指针变量大小)
printf("%d\n", sizeof(*p));     // 1(首字符'a'的大小)
printf("%d\n", strlen(p));       // 6(\0前字符数)
printf("%d\n", strlen(p + 1));   // 5(从'b'开始统计)
例5:字符指针数组和多级指针(重点理解)
#include <stdio.h>int main()
{// 定义指针数组 c,每个元素是字符串字面量的首地址// c[0] = "ENTER", c[1] = "NEW", c[2] = "POINT", c[3] = "FIRST"char* c[] = { "ENTER", "NEW", "POINT", "FIRST" };  // 定义指针数组 cp,每个元素是 c 数组元素的地址(二级指针)// cp[0] = &c[3](即 c + 3)// cp[1] = &c[2](即 c + 2)// cp[2] = &c[1](即 c + 1)// cp[3] = &c[0](即 c)char** cp[] = { c + 3, c + 2, c + 1, c };        // 定义三级指针 cpp,指向 cp 数组的起始地址(cp 是指针数组,类型为 char**[],所以 cpp 是 char***)char*** cpp = cp;                                // 1. ***++cpp 解析://    - ++cpp:cpp 原本指向 cp[0],自增后指向 cp[1](改变 cpp 的指向)//    - *cpp:  解引用得到 cp[1] 的值,即 c + 2(c[2] 的地址)//    - **cpp: 解引用得到 c[2] 的值,即字符串 "POINT" 的首地址//    - ***cpp:直接访问字符串 "POINT",输出 POINTprintf("%s\n", ***++cpp);// 2. *--*++cpp + 3 解析://    - ++cpp:cpp 从 cp[1] 自增到 cp[2]//    - *++cpp:解引用得到 cp[2] 的值,即 c + 1(c[1] 的地址)//    - --*++cpp:对 c + 1 做自减,得到 c + 0(c[0] 的地址)//    - *--*++cpp:解引用得到 c[0] 的值,即字符串 "ENTER" 的首地址//    - + 3:   地址偏移 3 个 char 长度,"ENTER" 偏移后指向 "ER",输出 ERprintf("%s\n", *--*++cpp + 3);// 3. *cpp[-2] + 3 解析://    - cpp[-2]:等价于 *(cpp - 2),cpp 当前指向 cp[2],减 2 后指向 cp[0]//    - *cpp[-2]:解引用 cp[0] 得到 c + 3(c[3] 的地址)//    - + 3:   地址偏移 3 个 char 长度,"FIRST" 偏移后指向 "ST",输出 STprintf("%s\n", *cpp[-2] + 3);// 4. cpp[-1][-1] + 1 解析://    - cpp[-1]:等价于 *(cpp - 1),cpp 当前指向 cp[2],减 1 后指向 cp[1]//    - cpp[-1][-1]:等价于 *(*(cpp - 1) - 1),先解引用 cp[1] 得到 c + 2,再减 1 得到 c + 1(c[1] 的地址)//    - + 1:   地址偏移 1 个 char 长度,"NEW" 偏移后指向 "EW",输出 EWprintf("%s\n", cpp[-1][-1] + 1);return 0;
}
}

四、数组与指针笔试题精析

4.1 经典题目1:指针运算与数组越界

#include <stdio.h>
int main() {int a[5] = {1, 2, 3, 4, 5};int* ptr = (int*)(&a + 1);  // &a+1跳过整个数组printf("%d,%d", *(a + 1), *(ptr - 1));  // 2,5return 0;
}

解析

  • *(a + 1):等价于a[1],值为2。
  • &a + 1:指向数组末尾后一位,ptr-1回退一个int,指向a[4],值为5。

4.2 经典题目2:结构体指针运算

// X86环境(32位),结构体大小20字节
struct Test {int Num;char* pcName;short sDate;char cha[2];short sBa[4];
}*p = (struct Test*)0x100000;int main() {printf("%p\n", p + 0x1);       // 0x100014(+20字节,结构体大小)printf("%p\n", (unsigned long)p + 0x1);  // 0x100001(直接+1)printf("%p\n", (unsigned int*)p + 0x1);  // 0x100004(+4字节,int*步长)return 0;
}

解析:指针运算的步长由指针类型决定,结构体指针p+1偏移整个结构体大小(20字节)。

4.3 经典题目3:二维数组初始化陷阱

#include <stdio.h>
int main() {// 注意:(0,1)是逗号表达式,值为1;实际初始化:{1,3,5}int a[3][2] = {(0, 1), (2, 3), (4, 5)};int* p = a[0];  // 指向第一行首元素printf("%d", p[0]);  // 1return 0;
}

解析:逗号表达式(0,1)结果为1,数组实际初始化为{{1,2}, {3,4}, {5,0}}p[0]即第一行首元素1。

五、总结与拓展

本文通过回调函数、qsort实现、sizeofstrlen对比、指针笔试题四个核心模块,深入解析了C语言指针的关键知识点。核心要点:

  1. 回调函数:通过函数指针实现动态调用,减少冗余代码(如qsort的比较函数)。
  2. qsort原理:用void*兼容任意类型,通过回调函数定制比较逻辑,按字节交换元素。
  3. sizeof与strlen:前者计算内存大小,后者依赖\0统计字符串长度,需注意越界风险。
  4. 数组与指针:数组名在sizeof&后表示整个数组,其他情况表示首元素地址;指针运算步长由类型决定。

掌握这些知识点后,建议多做指针笔试题,通过实战巩固对指针类型、运算规则的理解。指针虽难,但只要理清类型与内存的关系,就能逐步攻克!

http://www.dtcms.com/a/310335.html

相关文章:

  • 遥控器信号捕获
  • [CISCN 2022 初赛]online_crt
  • 基于react的YAPI实战指南
  • JavaWeb--Student2025项目:增删改查
  • 光纤网络FTTx(光接入网的应用类型)
  • 标准项目-----网页五子棋(4)-----游戏大厅+匹配+房间代码
  • Qt Quick 性能优化方法
  • WPF TreeView自带自定义滚动条
  • 云计算k8s集群部署配置问题总结
  • 铁皮矫平机冷知识·第三弹
  • 网站QPS多少才算高并发
  • A∗算法(A-star algorithm)一种在路径规划和图搜索中广泛使用的启发式搜索算法
  • 利用CompletableFuture优化查询效率
  • 1.2.4 砌体结构设计构造要求
  • Dify知识库分段策略详解:通用分段 vs 父子分段
  • 开源框架推荐:API数据批处理与爬虫集成
  • 前端开发一百问(动态更新)
  • 【0基础PS】PS工具详解--仿制图章工具
  • RustFS:高性能文件存储与部署解决方案(MinIO替代方案)
  • MySQL锁的分类 MVCC和S/X锁的互补关系
  • QT6.5.3 vs2022 pcl1.14.1窗体界面打开pcd点云文件
  • PAT 1022 Digital Library
  • nodejs最近开发过程中的总结
  • 【LeetCode】算法详解#11 ---相交链表
  • 智能Agent场景实战指南 Day 29:Agent市场趋势与前沿技术
  • 一篇文章读懂AI Agent(智能体)
  • spring boot 启动报错---java: 无法访问org.springframework.boot.SpringApplication 错误的类文件
  • 获取LLM 内部的结构信息和矩阵维度信息
  • LeetCode 热题100:206. 反转链表
  • 【AI问答】PromQL中interval和rate_interval的区别以及Grafana面板的配置建议