指针(下):指针与函数、字符串、多级指针与void指针

好,咱们接着聊指针。上一章我们把指针的基础和数组部分讲透了,这一章我带你看看指针更“野”的一面——跟函数结合、跟字符串纠缠、还有多级指针和void指针这些“老油条”才玩得转的东西。

说实话,指针这部分,我当年刚学的时候也绕晕过。但后来在项目里踩过几次坑,才真正明白这些设计背后的道理。你跟着我走一遍,保证比你自己啃书效率高得多。

1. 函数指针:把函数当变量用

函数指针是什么?说白了,就是指向函数的指针。函数本身也有地址,存在代码段里。你可以把这个地址存下来,然后通过它去调用函数。

为什么要这么干?我举个例子你就明白了。假设你要写一个排序函数,今天想按升序排,明天想按降序排,后天可能按某种自定义规则排。你总不能每次都重写排序逻辑吧?这时候函数指针就派上用场了——你把比较规则作为一个函数指针传进去,排序函数内部通过这个指针调用比较函数。

核心概念:函数指针的类型由返回值类型和参数列表决定。比如 int (*func)(int, int) 表示一个指向“返回int、接受两个int参数”的函数的指针。

#include <stdio.h>

// 两个比较函数
int ascending(int a, int b) {
    return a - b;
}

int descending(int a, int b) {
    return b - a;
}

// 排序函数,接受函数指针作为参数
void bubble_sort(int arr[], int n, int (*compare)(int, int)) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (compare(arr[j], arr[j + 1]) > 0) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {5, 3, 8, 1, 9};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 传入升序比较函数
    bubble_sort(arr, n, ascending);
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    // 传入降序比较函数
    bubble_sort(arr, n, descending);
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

你看,同一个排序函数,传不同的函数指针进去,行为就完全变了。我在做嵌入式协议栈的时候,经常用这种模式来处理不同设备的回调——比如收到数据包后,根据包类型调用不同的处理函数。用函数指针数组来管理,代码干净得很。

小技巧:typedef 给函数指针起个别名,代码可读性会好很多。比如 typedef int (*CompareFunc)(int, int);,然后直接用 CompareFunc 声明参数。

2. 指针与字符串:C语言的“爱恨情仇”

C语言里没有真正的字符串类型。字符串其实就是字符数组,以'\0'结尾。而操作字符串,几乎离不开指针。

我个人习惯用指针来遍历字符串,而不是用数组下标。为什么?因为指针操作更底层、更高效,尤其在嵌入式环境里,能省几条指令就省几条。

#include <stdio.h>

// 用指针实现字符串长度计算
int my_strlen(const char *str) {
    const char *p = str;
    while (*p != '\0') {
        p++;
    }
    return p - str;  // 指针相减得到元素个数
}

// 用指针实现字符串拷贝
void my_strcpy(char *dest, const char *src) {
    while (*dest++ = *src++);  // 经典写法,赋值表达式返回被赋的值
}

int main() {
    char str[] = "Hello, pointer!";
    printf("Length: %d\n", my_strlen(str));

    char buf[32];
    my_strcpy(buf, str);
    printf("Copied: %s\n", buf);

    return 0;
}

这里有个坑,我曾经踩过——char *p = "hello"; 这种写法,p指向的是字符串常量,存在只读区。你要是试图通过p修改内容,程序直接崩溃。而 char arr[] = "hello"; 是在栈上分配了可修改的数组。这两者区别很大,面试也常考。

警告:永远不要试图修改字符串常量!如果你需要修改字符串内容,请使用字符数组。

3. 多级指针:指针的指针

多级指针,说白了就是“指向指针的指针”。二级指针最常见,三级以上我几乎没用过——说实话,超过三级指针的代码,可读性已经烂到没法维护了。

二级指针最常见的用途有两个:一是修改指针本身的值(比如在函数里重新分配内存),二是处理指针数组。

#include <stdio.h>
#include <stdlib.h>

// 通过二级指针修改指针本身
void allocate_memory(int **ptr, int size) {
    *ptr = (int *)malloc(size * sizeof(int));
    if (*ptr != NULL) {
        for (int i = 0; i < size; i++) {
            (*ptr)[i] = i * 10;
        }
    }
}

int main() {
    int *arr = NULL;
    allocate_memory(&arr, 5);  // 传指针的地址

    if (arr != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        free(arr);
    }

    return 0;
}

你想想看,如果只传一级指针进去,函数内部修改的是指针的副本,外面的指针还是NULL。这就是为什么需要二级指针——你要改的是指针变量本身,而不是它指向的内容。

我在做嵌入式文件系统的时候,需要动态管理多个文件句柄。每个句柄是一个结构体指针,我用一个二级指针数组来管理它们。嗯,这种场景下二级指针几乎是唯一的选择。

4. void指针:泛型的“瑞士军刀”

void* 是C语言里最灵活的类型——它可以指向任何类型的数据。但代价是,你不能直接解引用它,也不能对它做指针运算。必须先转换成具体类型。

void指针最常见的用途是实现泛型函数。比如标准库里的 memcpyqsort,参数都是void指针,这样它们才能处理任意类型的数据。

#include <stdio.h>

// 一个简单的泛型打印函数(仅演示void*用法)
void print_value(void *data, char type) {
    switch (type) {
        case 'i':
            printf("Integer: %d\n", *(int *)data);
            break;
        case 'f':
            printf("Float: %f\n", *(float *)data);
            break;
        case 'c':
            printf("Char: %c\n", *(char *)data);
            break;
        default:
            printf("Unknown type\n");
    }
}

int main() {
    int a = 42;
    float b = 3.14f;
    char c = 'X';

    print_value(&a, 'i');
    print_value(&b, 'f');
    print_value(&c, 'c');

    return 0;
}

重要提醒:void指针虽然灵活,但滥用会让代码变得难以理解和调试。我建议只在需要“真正泛型”的场景下使用,比如写通用数据结构或库函数。日常业务代码里,尽量用具体类型。

知识体系总览

下面这张图把本章的核心知识点串起来了。你看一眼,心里就有谱了。

指针(下) 函数指针 回调机制 策略模式 函数指针数组 指针与字符串 字符指针 vs 字符数组 字符串常量不可修改 指针遍历字符串 多级指针 二级指针修改指针本身 指针数组管理 动态内存分配 void指针 泛型编程 需强制类型转换后使用

这张图把四个核心知识点串在了一起。函数指针让你能把行为参数化;指针与字符串让你真正理解C语言的字符串本质;多级指针帮你突破“只能改值不能改指针”的限制;void指针则给了你泛型的能力。这四个东西组合起来,你就能写出非常灵活、可复用的C代码。

我的建议:学指针不要死记语法。你去找一个开源的小项目(比如某个嵌入式驱动库),看看别人是怎么用函数指针做回调、怎么用二级指针管理内存的。读代码比写代码更能培养“指针感”。

好了,这一章的内容就到这儿。指针这部分确实需要多练,尤其是函数指针和二级指针,刚开始用的时候容易懵。但只要你亲手写过几个例子,后面就会越来越顺。记住,指针是C语言的灵魂,搞懂了它,你才算真正入了C的门。


公众号:蓝海资料掘金营,微信deep3321