指针(下):指针与函数、字符串、多级指针与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指针最常见的用途是实现泛型函数。比如标准库里的 memcpy、qsort,参数都是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指针虽然灵活,但滥用会让代码变得难以理解和调试。我建议只在需要“真正泛型”的场景下使用,比如写通用数据结构或库函数。日常业务代码里,尽量用具体类型。
知识体系总览
下面这张图把本章的核心知识点串起来了。你看一眼,心里就有谱了。
这张图把四个核心知识点串在了一起。函数指针让你能把行为参数化;指针与字符串让你真正理解C语言的字符串本质;多级指针帮你突破“只能改值不能改指针”的限制;void指针则给了你泛型的能力。这四个东西组合起来,你就能写出非常灵活、可复用的C代码。
我的建议:学指针不要死记语法。你去找一个开源的小项目(比如某个嵌入式驱动库),看看别人是怎么用函数指针做回调、怎么用二级指针管理内存的。读代码比写代码更能培养“指针感”。
好了,这一章的内容就到这儿。指针这部分确实需要多练,尤其是函数指针和二级指针,刚开始用的时候容易懵。但只要你亲手写过几个例子,后面就会越来越顺。记住,指针是C语言的灵魂,搞懂了它,你才算真正入了C的门。
公众号:蓝海资料掘金营,微信deep3321