C语言指针详解(上):指针变量、函数指针
- C语言进阶
- 9小时前
- 24热度
- 0评论
C语言指针详解(上)— 指针变量、函数指针基础与语法
本篇讲解指针的核心概念与使用方法。关于指针安全(空指针、野指针)和嵌入式实战用法,请看《C语言指针详解(下)》。
一、指针基础
1.1 定义
指针变量是一个特殊的变量,它存储的值是另一个变量的内存地址(即地址编号),而不是数据本身。我们平时说的"指针",通常指的就是这个地址值本身。
- 指针:一个内存地址(一个数值),代表某块内存的编号
- 指针变量:一个变量,专门用于存放内存地址
类比:门牌号是"指针",写着门牌号的纸条是"指针变量"。
变量存数据,指针存地址。指针变量的值有特殊意义——它代表一个地址编号,通过这个地址可以间接访问和操作对应内存中的数据。
语法格式:
数据类型 *指针变量名;
数据类型:指针所指向的变量的类型*:声明这是一个指针变量指针变量名:指针本身的名字
&(取地址):获取变量的内存地址*(解引用):获取指针指向地址上的值
指针变量的大小是固定的,不取决于它指向什么类型,而取决于系统的地址总线宽度:
| 系统 | 指针大小 | 原因 |
|---|---|---|
| 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"; |
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;
}
与数组名退化为指针类似,函数名在表达式中也会自动退化为函数指针:
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. x 是 int 指针,y 是 int 变量
D. 编译错误
查看答案
**C**。* 只作用于紧跟的变量名 x,所以 x 是 int* 类型,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语言变量 — 变量的存储与生命周期