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

前言

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


一、sizeof与strlen的核心差异

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

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

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

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


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

相关推荐
迦蓝叶2 小时前
从繁琐到优雅:用 Project Panama 改变 Java 原生交互
java·jni·native·java新特性·原生接口·跨语言开发·projectpanama
豐儀麟阁贵2 小时前
6.3对象类型的转换
java·开发语言
四谎真好看2 小时前
Java 黑马程序员学习笔记(进阶篇27)
java·开发语言·笔记·学习·学习笔记
Abona2 小时前
自动驾驶、无人机、机器人核心技术双范式
算法·机器人·自动驾驶·无人机
草莓熊Lotso2 小时前
《算法闯关指南:优选算法--模拟》--39.替换所有问号,40.提莫攻击
开发语言·c++·算法·模拟
q***82912 小时前
Spring Boot 热部署
java·spring boot·后端
合作小小程序员小小店2 小时前
web开发,在线%农业产品销售管理%系统,基于idea,html,css,vue.js,layui,java,jdk,ssm
java·前端·jdk·intellij-idea·layui·数据库管理员
yuuki2332333 小时前
【数据结构】栈
c语言·数据结构·后端
草莓熊Lotso3 小时前
C++ STL set 系列完全指南:从底层原理、核心接口到实战场景
开发语言·c++·人工智能·经验分享·网络协议·算法·dubbo