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

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


在这里插入图片描述

前言

经过前面三讲的铺垫,我们已经掌握了指针的核心概念、与数组/函数的结合用法。这一讲作为指针系列的收尾,将聚焦实战与巩固,先厘清sizeofstrlen的核心差异,再深入回调函数的本质的,通过qsort函数的使用与模拟实现掌握泛型编程思路,最后解析高频数组与指针笔试题,助力你彻底打通指针的“任督二脉”,应对面试与开发中的各类场景。


一、sizeof与strlen的核心差异

sizeofstrlen是C语言中最易混淆的两个“长度相关工具”,但二者本质完全不同:sizeof是计算内存大小的操作符,strlen是统计字符串长度的库函数。

1.1 核心区别对比

特性sizeofstrlen
本质操作符(编译器内置)库函数(需包含<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\0strlen会一直向后找\0,导致越界(结果随机)。
  • arr2是字符串常量,编译器自动在末尾添加\0strlen统计到\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的核心是“通用排序逻辑+自定义比较规则”,我们用冒泡排序思想模拟实现,关键在于:

  1. void*接收任意类型数据。
  2. 按字节交换元素(适配任意类型)。
  3. 通过回调函数获取比较结果。
#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种含义

  1. sizeof(数组名):数组名表示整个数组,计算数组总大小。
  2. &数组名:数组名表示整个数组,取出整个数组的地址。
  3. 其他场景(如数组名+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依赖\0sizeof计算实际内存大小。

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字节),指向数组末尾后。
  • ptrint*类型,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语言的“利器”,在编程之路上走得更稳、更远!


以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正。

在这里插入图片描述

Read more

使用 VS Code 连接 MySQL 数据库

使用 VS Code 连接 MySQL 数据库

文章目录 * 前言 * VS Code下载安装 * 如何在VS Code上连接MySQL数据库 * 1、打开扩展 * 2、安装MySQL插件 * 3、连接 * 导入和导出表结构和数据 前言 提示:这里可以添加本文要记录的大概内容: 听说VS Code不要钱,功能还和 Navicat 差不多,还能在上面打游戏 但是没安装插件是不行的 发现一个非常牛的博主 还有一个非常牛的大佬 提示:以下是本篇文章正文内容,下面案例可供参考 VS Code下载安装 VS Code下载安装 如何在VS Code上连接MySQL数据库 本篇分享是在已有VS Code这个软件的基础上,数据库举的例子是MySQL 1、打开扩展 2、安装MySQL插件 在搜索框搜索 MySQL和 MySQL Syntax,下载这三个插件 点击下面的插件,选择【install】安装

By
RustFS 保姆级上手指南:国产开源高性能对象存储

RustFS 保姆级上手指南:国产开源高性能对象存储

最近在给项目选型对象存储的时候,发现一个挺有意思的现象:一边是MinIO社区版功能逐渐“躺平”,另一边是大家对存储性能和安全性的要求越来越高。就在这时,一个叫 RustFS 的国产开源项目闯入了我的视野。 折腾了一阵子后,我感觉这玩意儿确实有点东西。它用Rust语言写,天生就带着高性能和内存安全的基因,性能号称比MinIO快一大截,而且用的是对商业友好的Apache 2.0协议。今天,我就手把手带大家从零开始,搭建一个属于自己的RustFS服务,体验一下国产存储的威力。 一、 RustFS是什么?为什么值得你关注? 简单说,RustFS是一个 分布式对象存储系统 。你可以把它理解成一个你自己搭建的、功能跟阿里云OSS、亚马逊S3几乎一样的“私有云盘”。 但它有几个非常突出的亮点,让我觉得必须试试: * 性能猛兽 :基于Rust语言开发,没有GC(垃圾回收)带来的性能抖动,官方数据显示在4K随机读场景下,性能比MinIO高出40%以上,内存占用还不到100MB,简直是“小钢炮”。 * 100%S3兼容 :这意味着你现有的所有使用S3 API的代码、工具(比如AWS

By