C语言指针详解(上):指针变量、函数指针

C语言指针详解(上)— 指针变量、函数指针基础与语法

本篇讲解指针的核心概念与使用方法。关于指针安全(空指针、野指针)和嵌入式实战用法,请看《C语言指针详解(下)》

一、指针基础

1.1 定义

指针变量是一个特殊的变量,它存储的值是另一个变量的内存地址(即地址编号),而不是数据本身。我们平时说的"指针",通常指的就是这个地址值本身。

ℹ️ 指针 vs 指针变量
  • 指针:一个内存地址(一个数值),代表某块内存的编号
  • 指针变量:一个变量,专门用于存放内存地址

类比:门牌号是"指针",写着门牌号的纸条是"指针变量"。

💡 核心理解

变量存数据,指针存地址。指针变量的值有特殊意义——它代表一个地址编号,通过这个地址可以间接访问和操作对应内存中的数据。

语法格式:

数据类型 *指针变量名;
  • 数据类型:指针所指向的变量的类型
  • *:声明这是一个指针变量
  • 指针变量名:指针本身的名字
ℹ️ 关键运算符
  • &(取地址):获取变量的内存地址
  • *(解引用):获取指针指向地址上的值

指针变量的大小是固定的,不取决于它指向什么类型,而取决于系统的地址总线宽度:

系统 指针大小 原因
32 位(STM32、普通 PC) 4 字节 地址总线 32 位,可寻址 2³² = 4GB
64 位(现代 PC) 8 字节 地址总线 64 位,可寻址 2⁶⁴
// 无论指向什么类型,指针大小都一样(32位系统下均为4)
sizeof(int *)    // 4
sizeof(char *)   // 4
sizeof(double *) // 4
sizeof(void *)   // 4

Visual Studio 2022 x86/x64 系统运行截图:

指针大小对比

1.2 基本用法

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

int b = 20;                          // 全局变量,存在全局/静态区

void main(void)
{
    int a = 10;                      // 局部变量,存在栈区
    int *p1 = &a;                    // p1 → 栈上的 a
    int *p2 = &b;                    // p2 → 全局区的 b
    int *p3 = (int*)malloc(sizeof(int));  // p3 → 堆上动态分配的内存
    if (!p3) {
        return;
    }
    *p3 = 30;

    int *p;
    p = (int *)100;                  // ⚠️ 语法合法,但地址 100 不可读写!

    printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *(&b), p3[0]);
    printf("p = %p\n", p);
    printf("*p = %d\n", *p);        // ❌ 崩溃!访问了非法地址

    free(p3);
}

这段代码展示了指针的几种典型场景:

指针 指向哪里 内存区域 结果
p1 = &a 局部变量 a 栈区 ✅ 正常读写
p2 = &b 全局变量 b 全局/静态区 ✅ 正常读写
p3 = malloc(...) 动态分配 堆区 ✅ 正常读写(用完要 free
p = (int*)100 随意指定的地址 未知 程序崩溃(野指针)
ℹ️ 几个细节
  • *(&b) 等价于 b — 先取 b 的地址,再解引用拿回值
  • p3[0] 等价于 *p3 — 指针可以用数组下标语法访问
  • p = (int*)100 赋值语法合法(整数强转为指针),但地址 100 不属于你的程序,读写必崩
⚠️ 野指针预警

最后一行 *p = ... 对地址 100 解引用,这就是典型的野指针——指向一个不确定/非法的地址。关于野指针的成因与防御,详见下篇。

1.3 指针与数组

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;  // 数组名就是首元素的地址

    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, *(p+%d) = %d\n", i, arr[i], i, *(p + i));
    }
    // arr[i] 与 *(p + i) 完全等价

    return 0;
}

数组退化(Array Decay)

上面代码中 int *p = arr 能直接赋值,是因为 C 语言的一条重要规则:数组名在大多数表达式中会自动"退化"为指向首元素的指针

int arr[5] = {1, 2, 3, 4, 5};

int *p = arr;       // arr 退化成 int*,等价于 &arr[0]

void foo(int *p, int len);
foo(arr, 5);        // 传参时 arr 也会退化成 int*

不退化的三个例外:

场景 行为 示例
sizeof(arr) 返回整个数组的大小 sizeof(arr)20(5×4 字节)
&arr 得到数组指针(类型是 int(*)[5] 不是 int*
字符串字面量初始化 char[] 拷贝内容,不退化 char s[] = "hello";
⚠️ 数组传参后 sizeof 就"不对了"
void foo(int arr[]) {       // 本质上是 int *arr,已经退化了
    sizeof(arr);            // ⚠️ 得到的是指针大小(4或8),不是数组大小!
}

这就是为什么数组长度必须作为额外参数传入函数的原因。

1.4 指针作为函数参数(传址调用)

#include <stdio.h>

// 通过指针交换两个变量的值
void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 3, b = 7;
    printf("交换前: a=%d, b=%d\n", a, b);  // a=3, b=7

    swap(&a, &b);  // 传入地址
    printf("交换后: a=%d, b=%d\n", a, b);  // a=7, b=3

    return 0;
}

二、函数指针

💡 一句话理解

函数指针就是指向函数的指针——变量指针存的是数据地址,函数指针存的是代码(函数入口)地址

2.1 定义与内存视角

在 C 语言中,函数名本质上就是一个地址常量,指向该函数在内存中机器码的入口地址。函数指针是一个变量,它存储了某个函数的入口地址,通过它可以间接调用该函数。

函数指针内存示意

2.2 声明格式

返回值类型 (*指针变量名)(参数类型列表);
⚠️ 注意括号

(*指针变量名) 的括号不可省略
  • int (*p)(int, int); → ✅ 这是函数指针,p 指向一个 int(int,int) 型函数
  • int *p(int, int); → ❌ 这是返回 int* 的函数声明,不是函数指针!

2.3 常见声明示例

// 指向 "无参数、无返回值" 函数的指针
void (*p1)(void);

// 指向 "两个int参数、返回int" 函数的指针
int (*p2)(int, int);

// 指向 "一个char*参数、返回void" 函数的指针
void (*p3)(char *);

// 指向 "无参数、返回 uint8_t*" 函数的指针
uint8_t* (*p4)(void);

2.4 使用 typedef 简化声明

// 定义函数指针类型
typedef int (*MathFunc_t)(int, int);

// 用类型名声明变量,简洁清晰
MathFunc_t pAdd = add;
MathFunc_t pSub = sub;
💡 最佳实践

在嵌入式项目中,强烈建议使用 typedef 定义函数指针类型:
  • 命名加 _t 后缀或 Callback 后缀,语义更清晰
  • 多处使用时避免重复书写冗长的声明
  • HAL 库、RTOS 中大量采用此方式

2.5 赋值与调用

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main(void) {
    int (*p)(int, int);  // 声明函数指针

    p = add;             // 赋值:函数名自动退化为函数指针,不需要 &
    printf("10 + 20 = %d\n", p(10, 20));   // 输出: 30

    p = sub;             // 重新指向另一个函数
    printf("10 - 20 = %d\n", p(10, 20));   // 输出: -10

    return 0;
}
ℹ️ 函数名退化(Function Decay)

与数组名退化为指针类似,函数名在表达式中也会自动退化为函数指针
p = add;    // add 退化成函数指针 ✅
p = &add;   // 显式取地址,效果一样 ✅

这就是为什么 p = add 不需要加 & 的原因。

ℹ️ 两种等价的调用方式
p(10, 20);       // 直接调用(推荐,简洁)
(*p)(10, 20);    // 解引用调用(更显式)

2.6 回调函数(函数指针作为参数)

回调函数是函数指针最核心的应用——将"行为"作为参数传递给另一个函数。

#include <stdio.h>

typedef int (*Operation_t)(int, int);

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 calculate(int a, int b, Operation_t op) {
    return op(a, b);  // 通过函数指针调用传入的函数
}

int main(void) {
    printf("加法: %d\n", calculate(10, 5, add));  // 15
    printf("减法: %d\n", calculate(10, 5, sub));  // 5
    printf("乘法: %d\n", calculate(10, 5, mul));  // 50
    return 0;
}
💡 回调的本质

"你不直接调用我,你把我的地址给别人,让别人在合适的时候来调用我。"

这就是回调——控制反转的思想。

2.7 函数指针数组

将多个同类型函数组织成数组,用索引来选择调用哪一个:

#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 divide(int a, int b) { return b != 0 ? a / b : 0; }

int main(void) {
    // 函数指针数组
    int (*ops[])(int, int) = { add, sub, mul, divide };
    const char *names[] = { "加", "减", "乘", "除" };

    int a = 20, b = 5;
    for (int i = 0; i < 4; i++) {
        printf("%s法: %d\n", names[i], ops[i](a, b));
    }
    return 0;
}
ℹ️ 应用场景

函数指针数组在嵌入式中常用于:
  • 命令解析器:根据命令 ID 索引对应的处理函数
  • 状态机:根据状态索引对应的状态处理函数
  • 菜单系统:根据菜单项索引对应的操作

三、* 星号辨析:定义指针 vs 解引用 vs 乘法

* 在 C 语言中有 三种完全不同的含义,仅靠上下文区分:

场景 含义 示例 怎么读
类型声明 定义指针变量 int *p; "p 是一个 int 指针"
表达式中(单目) 解引用(取值) *p = 10; "取 p 指向的值"
表达式中(双目) 乘法 a * b "a 乘以 b"

判断方法:看 * 左边是什么

        ┌─ 左边是"类型名" → 定义指针
        │     int *p;       // int 是类型 → 定义指针
  * 号 ─┤
        └─ 左边不是类型名 → 表达式中使用
              *p = 10;      // 左边没有类型 → 解引用
              a * b;        // 两边都是变量 → 乘法

最容易混淆的场景

// 场景一:定义时的初始化 —— * 是声明的一部分,不是解引用
int *p = &a;    // 不是 "把 &a 赋给 *p",而是 "声明 int* 类型的 p,初始化为 &a"

// 场景二:一行定义多个变量 —— * 只跟紧挨着的变量
int *x, y;      // x 是 int*(指针),y 只是 int(不是指针!)
int *x, *y;     // x 和 y 都是 int*(每个都要加 *)

// 场景三:函数参数中
void foo(int *p) { ... }   // 声明:p 是指针参数
foo(&a);                    // 调用时传地址,这里的 & 是取地址
📋 总结速查
int *p = &a;     →  * 靠着类型(int)→ 声明指针
*p = 10;         →  * 单独跟变量     → 解引用(写入)
x = *p;          →  * 单独跟变量     → 解引用(读取)
c = a * b;       →  * 两边都是值     → 乘法

记不住就看一点:* 前面有没有类型名(int/char/float...)

  • ✅ 有 → 定义指针
  • ❌ 没有 → 解引用或乘法

四、总结

指针分类速查

指针类型 说明 关键点
普通指针 指向变量数据 * &,传址调用、数组遍历
函数指针 指向函数入口地址 回调、状态机、驱动抽象
📋 核心要点
  • 取地址用 &,解引用用 *
  • * 前有类型 → 声明;无类型 → 运算
  • 数组名、函数名在表达式中会自动退化为指针
  • 函数指针调用前必须判空if (cb) cb();

五、复习题

选择题

1. 在 32 位系统下,以下哪个表达式的值不是 4?

A. sizeof(int *)
B. sizeof(char *)
C. sizeof(int [5])
D. sizeof(void *)

查看答案

**C**。sizeof(int [5]) = 5 × 4 = 20 字节,得到的是整个数组的大小。其他选项都是指针大小,在 32 位系统下均为 4 字节。

2. 下面代码的输出是什么?

int a = 5;
int *p = &a;
*p = 20;
printf("%d\n", a);

A. 5
B. 20
C. 地址值
D. 编译错误

查看答案

**B**。p 指向 a*p = 20 通过解引用修改了 a 的值,所以输出 20

3. 以下声明中,哪一个是函数指针?

A. int *p(int, int);
B. int (*p)(int, int);
C. int *(p)(int, int);
D. int (p*)(int, int);

查看答案

**B**。int (*p)(int, int) 是函数指针,p 指向一个接收两个 int 参数、返回 int 的函数。A 是返回 int* 的函数声明,C/D 语法错误或含义不同。

4. int *x, y; 这行代码声明了什么?

A. 两个 int 指针
B. 两个 int 变量
C. xint 指针,yint 变量
D. 编译错误

查看答案

**C**。* 只作用于紧跟的变量名 x,所以 xint* 类型,y 只是普通 int。要让两者都是指针,需要写 int *x, *y;

判断题

5. 数组作为函数参数传递后,在函数内部使用 sizeof 仍能得到数组的总大小。( )

查看答案

**✗ 错误**。数组传参时会退化为指针,sizeof 得到的是指针大小(4 或 8 字节),而非数组总大小。这就是为什么数组长度需要作为额外参数传入。

6. p = add;p = &add; 效果完全相同,因为函数名会自动退化为函数指针。( )

查看答案

**✓ 正确**。与数组名退化为指针类似,函数名在表达式中也会自动退化为函数指针,所以 add&add 等价。

编程题

7. 请写出以下代码的运行结果:

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d %d %d\n", *p, *(p+2), p[4]);
查看答案

输出:10 30 50

- *p → 首元素 arr[0] = 10
- *(p+2)arr[2] = 30
- p[4]arr[4] = 50(指针下标访问等价于 *(p+4)

8. 使用函数指针和回调函数实现一个 apply 函数,接收一个整型数组、数组长度和一个函数指针,对数组的每个元素执行该函数并打印结果。

查看参考答案

```c
#include <stdio.h>

typedef int (*Transform_t)(int);

int doubleIt(int x) { return x * 2; }
int square(int x) { return x * x; }

void apply(int arr[], int len, Transform_t func) {
for (int i = 0; i < len; i++) {
printf("%d ", func(arr[i]));
}
printf("\n");
}

int main(void) {
int data[] = {1, 2, 3, 4, 5};
apply(data, 5, doubleIt); // 输出: 2 4 6 8 10
apply(data, 5, square); // 输出: 1 4 9 16 25
return 0;
}
```

关键点:使用 typedef 定义函数指针类型,将"行为"作为参数传入,体现回调思想。


延伸阅读

  • C语言指针详解(下) — 空指针、野指针、嵌入式实战用法
  • 嵌入式开发函数指针使用 — 函数指针的完整实战应用(状态机、命令解析器、驱动抽象)
  • C语言指针和结构体 — 指针与结构体的综合使用
  • C语言变量 — 变量的存储与生命周期