指针(三):指针与函数、函数指针作为参数、回调函数、动态内存分配
好,咱们继续往下走。前两章我们把指针的基本功打扎实了,这一章要聊的,是真正让 C 语言变得灵活、高效、甚至有点「高级」的东西。说白了,就是指针怎么跟函数配合,怎么在运行时动态地要内存、还内存。
我个人觉得,这部分内容是区分「会写 C」和「写好 C」的一道分水岭。你想想看,一个程序如果连内存大小都不能在运行时决定,那得多死板?
1. 指针作为函数参数
先从一个最基础的问题说起:为什么有时候函数参数要用指针?
C 语言里,函数参数默认是「值传递」。什么意思?就是你把一个变量扔进函数,函数拿到的是它的副本。你在函数里改来改去,外面的原变量纹丝不动。
核心概念:如果想在函数内部修改外部变量的值,就必须传递变量的地址(指针)。
举个例子,写一个交换两个整数的函数:
#include <stdio.h>
// 错误示范:值传递,改不了外面
void swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// 正确示范:传指针,改地址里的内容
void swap_right(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swap_wrong(x, y);
printf("swap_wrong 之后: x=%d, y=%d\n", x, y); // 还是 10, 20
swap_right(&x, &y);
printf("swap_right 之后: x=%d, y=%d\n", x, y); // 变成 20, 10
return 0;
}
我在项目中遇到过好几次新手犯这个错。嗯,其实我自己刚学的时候也栽过跟头。记住一句话:想改谁,就传谁的地址。
2. 函数指针
函数也有地址?没错。函数编译之后,代码会放在内存的代码段里,函数名就是这段代码的入口地址。我们可以用一个指针变量来存这个地址,然后通过这个指针去调用函数。
声明一个函数指针,语法有点绕:
// 声明一个函数指针,指向「返回类型为 int,参数为两个 int」的函数
int (*func_ptr)(int, int);
注意括号不能省。如果写成 int *func_ptr(int, int),那就变成了「一个返回 int* 的函数声明」,完全两码事。
看个完整例子:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
int (*op)(int, int); // 声明函数指针
op = add; // 指向 add 函数
printf("add: %d\n", op(10, 5));
op = sub; // 指向 sub 函数
printf("sub: %d\n", op(10, 5));
return 0;
}
小技巧:可以用 typedef 给函数指针起个别名,代码会清爽很多。
typedef int (*Operation)(int, int);
Operation op = add;
3. 函数指针作为参数 & 回调函数
函数指针最常见的用法,就是作为参数传给另一个函数。这个被传进去的函数,就叫「回调函数」。
为什么要这么干?说白了,就是把「做什么」和「怎么做」分开。你写一个通用的排序函数,它负责「怎么排」(比如冒泡排序的逻辑),但具体「怎么比大小」由调用者决定。这样代码就活了。
来看一个例子:
#include <stdio.h>
// 比较函数:升序
int ascending(int a, int b) {
return a > b;
}
// 比较函数:降序
int descending(int a, int b) {
return a < b;
}
// 通用冒泡排序,比较逻辑由回调函数决定
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])) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
void print_array(int arr[], int n) {
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr1[] = {3, 1, 4, 1, 5, 9};
int arr2[] = {3, 1, 4, 1, 5, 9};
int n = 6;
bubble_sort(arr1, n, ascending);
print_array(arr1, n); // 升序
bubble_sort(arr2, n, descending);
print_array(arr2, n); // 降序
return 0;
}
你看,排序的逻辑没变,变的只是那个比较函数。这就是回调的威力。我在写嵌入式驱动的时候,经常用回调来处理中断——中断来了,调用你注册的那个函数,至于函数里干什么,我不关心。
注意:回调函数的类型必须严格匹配。参数个数、类型、返回值,差一点都不行。编译器会报错,但有时候报的错很隐晦,我曾经在这上面浪费过一下午。
4. 动态内存分配
好,接下来是重头戏。前面我们用的数组、变量,都是在编译时就已经确定好大小的。但很多时候,程序跑起来才知道要多大空间。比如你要读一个文件,文件多大?不知道。这时候就需要动态内存分配。
C 语言提供了四个函数:malloc、calloc、realloc、free。它们都声明在 <stdlib.h> 里。
4.1 malloc
malloc 分配一块连续的内存,返回指向这块内存的指针。它不会初始化内存,里面的数据是随机的。
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
// 分配失败,处理错误
printf("内存分配失败!\n");
return -1;
}
// 使用 p[0] 到 p[9]
铁律:每次 malloc 之后,必须检查返回值是否为 NULL。内存不是无限的,分配失败是真实存在的。
4.2 calloc
calloc 跟 malloc 很像,但有两点不同:
- 它接受两个参数:元素个数和每个元素的大小
- 它会自动把分配的内存初始化为 0
int *p = (int *)calloc(10, sizeof(int));
// 等价于 malloc + memset(p, 0, 10 * sizeof(int))
我个人习惯:如果分配后马上要全部赋值,用 malloc;如果希望初始是干净的 0,用 calloc。省得自己再写一遍清零。
4.3 realloc
这个函数很实用。你之前 malloc 了 10 个 int,现在发现不够,想扩展到 20 个。用 realloc:
int *p = (int *)malloc(10 * sizeof(int));
// ... 使用中 ...
int *new_p = (int *)realloc(p, 20 * sizeof(int));
if (new_p == NULL) {
// realloc 失败,原来的 p 仍然有效
printf("扩展内存失败!\n");
free(p);
return -1;
}
p = new_p; // 更新指针
注意:realloc 可能会移动内存块。如果原位置后面空间不够,它会找一块新的更大的空间,把旧数据复制过去,然后释放旧空间。所以一定要用返回值更新指针。
我曾经犯过的错:直接写 p = realloc(p, new_size); 如果 realloc 失败返回 NULL,原来的 p 就丢了,造成内存泄漏。正确做法是先用临时变量接返回值,检查后再赋值。
4.4 free
有借有还,再借不难。动态分配的内存,用完之后必须释放,否则就会造成内存泄漏。
free(p);
p = NULL; // 好习惯:释放后把指针置空
释放后把指针置 NULL,可以防止「野指针」问题。你想想看,free 之后那块内存可能已经被别人用了,但你的指针还指着那里,万一不小心又去读写,后果不堪设想。
5. 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
6. 综合示例:动态数组 + 回调排序
最后,我们把今天学的知识串起来,写一个完整的例子:动态分配一个数组,用回调函数决定排序方式。
#include <stdio.h>
#include <stdlib.h>
int ascending(int a, int b) { return a > b; }
int descending(int a, int b) { return a < b; }
void sort(int *arr, int n, int (*cmp)(int, int)) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (cmp(arr[j], arr[j + 1])) {
int t = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = t;
}
}
}
}
int main() {
int n;
printf("请输入数组大小: ");
scanf("%d", &n);
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存分配失败!\n");
return 1;
}
printf("请输入 %d 个整数: ", n);
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
sort(arr, n, ascending);
printf("升序结果: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
sort(arr, n, descending);
printf("降序结果: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
free(arr);
arr = NULL;
return 0;
}
这个例子麻雀虽小,五脏俱全。你看到了动态分配、指针传参、回调函数、释放内存。嗯,把这些吃透了,C 语言的指针这块,你就基本过关了。
我的建议:自己动手把这段代码敲一遍,然后试着改一改——比如改成对字符串排序,或者加一个「按绝对值排序」的回调。动手是最好的学习方式。
公众号:蓝海资料掘金营,微信deep3321