嵌入式 C 语言高级编程之一级指针
本文档专为嵌入式开发者设计,从基础概念到实际应用,配合完整代码示例和GDB调试实战,帮助你真正掌握一级指针的核心用法。
目录
一、指针基础概念
1.1 什么是指针?
指针 = 存储变量内存地址的变量
简单来说:
- 普通变量存储数据值
- 指针变量存储内存地址
内存示意图:
┌─────────────┐
│ a = 100 │ ← 变量a,值是100,地址是0x1000
│ 地址0x1000 │
└─────────────┘
↑
│ 存储的地址
┌─────────────┐
│ p = 0x1000│ ← 指针p,存储a的地址
│ 地址0x2000 │
└─────────────┘
1.2 为什么嵌入式必须掌握指针?
- 访问硬件寄存器 - 直接操作外设寄存器地址
- 高效传参 - 避免大结构体拷贝,节省内存
- 函数返回多值 - 通过指针参数带回多个结果
- 动态数据处理 - 操作数组、缓冲区、串口数据
- 驱动开发必备 - HAL库、设备驱动大量使用指针
二、指针定义与基本操作
2.1 指针的定义
int *p; // 定义一个指向int类型的指针
char *str; // 指向char类型的指针
float *pf; // 指向float类型的指针
关键点:
*在定义时表示"这是一个指针"- 指针类型必须与指向的变量类型匹配
2.2 两个核心操作符
| 操作符 | 名称 | 作用 | 示例 |
|---|---|---|---|
& | 取地址符 | 获取变量的内存地址 | &a → 得到a的地址 |
* | 解引用符 | 通过地址访问变量的值 | *p → 访问p指向的值 |
2.3 完整示例:指针基础
#include <stdio.h>
int main(void)
{
// 1. 定义普通变量
int a = 100;
// 2. 定义指针并初始化
int *p = &a; // p存储a的地址
// 3. 输出查看
printf("变量a的值: %d\n", a);
printf("变量a的地址: %p\n", &a);
printf("指针p存储的地址: %p\n", p);
printf("通过指针访问a的值: %d\n", *p);
// 4. 通过指针修改a的值
*p = 200;
printf("修改后a的值: %d\n", a);
return 0;
}
编译运行:
gcc pointer_basic.c -o pointer_basic
./pointer_basic
三、指针与函数
3.1 为什么需要指针传参?
问题: 普通传参无法修改原变量
// ❌ 错误示例:无法修改原值
void swap_wrong(int x, int y)
{
int temp = x;
x = y;
y = temp;
// 这里只交换了副本,原变量不变!
}
解决: 使用指针传参
// ✅ 正确示例:通过指针修改原值
void swap(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
// 通过地址直接修改原变量
}
3.2 完整示例:函数返回多值
#include <stdio.h>
// 函数同时返回平均值和最大值
void calculate(int *arr, int size, int *avg, int *max)
{
int sum = 0;
*max = arr[0];
for(int i = 0; i < size; i++)
{
sum += arr[i];
if(arr[i] > *max)
*max = arr[i];
}
*avg = sum / size; // 通过指针带回结果
}
int main(void)
{
int data[] = {10, 20, 30, 40, 50};
int average, maximum;
calculate(data, 5, &average, &maximum);
printf("平均值: %d\n", average);
printf("最大值: %d\n", maximum);
return 0;
}
嵌入式应用场景:
- 传感器数据读取函数,同时返回状态和数据
- 串口通信函数,返回接收到的数据和长度
- ADC采样函数,返回原始值和转换后的电压
四、指针与数组
4.1 数组名与指针的关系
核心原理: 数组名本质就是指向首元素的指针
int arr[5] = {10, 20, 30, 40, 50};
// 以下三种方式完全等价:
int *p1 = arr; // 数组名退化为指针
int *p2 = &arr[0]; // 取首元素地址
4.2 指针遍历数组
#include <stdio.h>
int main(void)
{
int buf[5] = {10, 20, 30, 40, 50};
int *p = buf; // 指向数组首元素
// 方法1: 使用指针算术
printf("方法1 - 指针算术:\n");
for(int i = 0; i < 5; i++)
{
printf("buf[%d] = %d\n", i, *(p + i));
}
// 方法2: 直接移动指针
printf("\n方法2 - 移动指针:\n");
for(int i = 0; i < 5; i++)
{
printf("*p = %d\n", *p);
p++; // 指针后移一个int的大小(4字节)
}
return 0;
}
4.3 指针与数组的对应关系
| 数组写法 | 指针写法 | 说明 |
|---|---|---|
arr[i] | *(arr + i) | 访问第i个元素 |
&arr[i] | arr + i | 获取第i个元素的地址 |
arr[0] | *arr | 访问首元素 |
4.4 嵌入式实战:串口数据处理
#include <stdio.h>
#include <string.h>
// 模拟串口接收缓冲区处理
void process_uart_data(unsigned char *buffer, int len)
{
unsigned char *p = buffer;
printf("接收到的数据(长度%d): ", len);
// 遍历缓冲区
for(int i = 0; i < len; i++)
{
printf("%02X ", *p); // 十六进制显示
p++;
}
printf("\n");
}
int main(void)
{
unsigned char uart_rx_buf[] = {0xAA, 0x55, 0x01, 0x02, 0x03};
process_uart_data(uart_rx_buf, 5);
return 0;
}
五、嵌入式实际应用
5.1 访问硬件寄存器
#include <stdio.h>
// 模拟GPIO寄存器地址
#define GPIO_BASE 0x40020000
#define GPIO_ODR (*(volatile unsigned int *)(GPIO_BASE + 0x14))
int main(void)
{
// 通过指针操作硬件寄存器
GPIO_ODR = 0xFFFF; // 所有引脚输出高电平
printf("GPIO输出寄存器值: 0x%08X\n", GPIO_ODR);
GPIO_ODR &= ~(1 << 5); // 清除第5位,对应引脚输出低电平
printf("修改后GPIO输出寄存器值: 0x%08X\n", GPIO_ODR);
return 0;
}
关键字解释:
volatile: 告诉编译器不要优化,每次都从内存读取unsigned int *: 转换为指针类型(*): 解引用,访问地址对应的寄存器
5.2 结构体指针应用
#include <stdio.h>
#include <string.h>
// 传感器数据结构
typedef struct {
int temperature; // 温度
int humidity; // 湿度
int pressure; // 气压
int valid; // 数据有效标志
} SensorData;
// 读取传感器数据(通过指针返回)
int read_sensor(SensorData *data)
{
// 模拟读取传感器
data->temperature = 25;
data->humidity = 60;
data->pressure = 1013;
data->valid = 1;
return 0; // 返回状态码
}
int main(void)
{
SensorData sensor;
if(read_sensor(&sensor) == 0)
{
printf("温度: %d°C\n", sensor.temperature);
printf("湿度: %d%%\n", sensor.humidity);
printf("气压: %d hPa\n", sensor.pressure);
}
return 0;
}
结构体指针访问:
data->member等价于(*data).member- 指针访问比拷贝整个结构体更高效
5.3 函数指针应用
#include <stdio.h>
// 定义函数指针类型
typedef int (*OperationFunc)(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, OperationFunc op)
{
return op(a, b);
}
int main(void)
{
int x = 10, y = 5;
printf("%d + %d = %d\n", x, y, calculate(x, y, add));
printf("%d - %d = %d\n", x, y, calculate(x, y, sub));
printf("%d * %d = %d\n", x, y, calculate(x, y, mul));
return 0;
}
嵌入式应用:
- 中断服务函数注册
- 回调函数机制
- 状态机实现
六、GDB调试实战
6.1 编译调试版本
gcc -g pointer_debug.c -o pointer_debug
关键: -g 参数添加调试信息,必须加上!
6.2 启动GDB
gdb ./pointer_debug
6.3 GDB核心命令速查
| 命令 | 缩写 | 作用 |
|---|---|---|
break 函数名 | b | 设置断点 |
run | r | 运行程序 |
next | n | 单步执行(不进入函数) |
step | s | 单步执行(进入函数) |
continue | c | 继续运行 |
print 变量 | p | 打印变量值 |
print &变量 | p & | 打印变量地址 |
x /格式 地址 | - | 查看内存 |
6.4 完整调试示例
测试代码:
#include <stdio.h>
int main(void)
{
int a = 100;
int *p = &a;
printf("a = %d, *p = %d\n", a, *p);
*p = 200;
printf("修改后: a = %d\n", a);
return 0;
}
调试步骤:
# 1. 启动GDB
$ gdb ./pointer_debug
# 2. 设置断点
(gdb) break main
# 3. 运行程序
(gdb) run
# 4. 单步执行
(gdb) next # 执行 int a = 100;
(gdb) next # 执行 int *p = &a;
# 5. 查看变量地址
(gdb) print &a
$1 = (int *) 0x7fffffffe04c
(gdb) print p
$2 = (int *) 0x7fffffffe04c
# 6. 查看内存内容(十进制)
(gdb) x /d &a
0x7fffffffe04c: 100
# 7. 查看内存内容(十六进制)
(gdb) x /x &a
0x7fffffffe04c: 0x00000064
# 8. 通过指针查看
(gdb) print *p
$3 = 100
# 9. 修改值后查看
(gdb) next # 执行 *p = 200;
(gdb) x /d &a
0x7fffffffe04c: 200 # 值已改变!
6.5 内存查看格式
| 格式 | 说明 | 示例 |
|---|---|---|
/d | 十进制 | x /d &a |
/x | 十六进制 | x /x &a |
/c | 字符 | x /c &a |
/s | 字符串 | x /s str |
/4d | 4个十进制 | x /4d &a |
七、常见错误与陷阱
7.1 未初始化指针
// ❌ 危险!野指针
int *p;
*p = 100; // p指向随机地址,可能崩溃!
// ✅ 正确做法
int a;
int *p = &a; // 初始化为有效地址
7.2 空指针解引用
// ❌ 错误
int *p = NULL;
*p = 100; // 段错误!
// ✅ 正确做法
int *p = NULL;
if(p != NULL) {
*p = 100;
}
7.3 指针越界
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
// ❌ 危险!越界访问
for(int i = 0; i < 10; i++) {
printf("%d\n", *(p + i)); // i>=5时越界!
}
// ✅ 正确做法
for(int i = 0; i < 5; i++) {
printf("%d\n", *(p + i));
}
7.4 返回局部变量的地址
// ❌ 错误!返回的地址无效
int* get_value(void)
{
int a = 100;
return &a; // a是局部变量,函数返回后销毁!
}
// ✅ 正确做法
int* get_value(void)
{
static int a = 100; // 静态变量,生命周期是整个程序
return &a;
}
7.5 指针类型不匹配
int a = 0x12345678;
int *p = &a;
// ❌ 类型不匹配
char *pc = (char*)p;
printf("%X\n", *pc); // 只读取1个字节(0x78)
// ✅ 正确做法
printf("%X\n", *p); // 读取4个字节
八、实战练习
练习1: 指针基础
任务: 编写程序,实现以下功能
- 定义一个int变量和一个指向它的指针
- 通过指针修改变量值
- 打印变量和指针的地址及值
练习2: 数组反转
任务: 使用指针实现数组元素反转
void reverse_array(int *arr, int size);
练习3: 字符串长度
任务: 不使用strlen,用指针计算字符串长度
int my_strlen(const char *str);
练习4: 查找最大值
任务: 使用指针查找数组中的最大值并返回其地址
int* find_max(int *arr, int size);
练习5: 结构体操作
任务: 定义学生结构体,编写函数通过指针修改学生信息
附录: 快速参考
A.1 指针声明速查
| 声明 | 含义 |
|---|---|
int *p | 指向int的指针 |
const int *p | 指向常量的指针(值不能改) |
int *const p | 指针本身是常量(地址不能改) |
int **p | 指向指针的指针 |
int (*p)[5] | 指向包含5个int的数组的指针 |
int *p[5] | 包含5个指针的数组 |
A.2 嵌入式常用宏定义
// 访问寄存器
#define REG32(addr) (*(volatile unsigned int *)(addr))
#define REG16(addr) (*(volatile unsigned short *)(addr))
#define REG8(addr) (*(volatile unsigned char *)(addr))
// 位操作
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
#define CLR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
#define GET_BIT(reg, bit) (((reg) >> (bit)) & 1)
A.3 调试宏
// 打印指针地址和值
#define DEBUG_PTR(ptr) \
printf(#ptr " = %p, *"#ptr" = %d\n", ptr, *ptr)
// 打印内存
#define DEBUG_MEM(addr, len) \
do { \
printf(#addr ":\n"); \
for(int i = 0; i < (len); i++) \
printf("%02X ", *(unsigned char*)((addr) + i)); \
printf("\n"); \
} while(0)
总结
一级指针核心要点:
- 本质: 指针是存储地址的变量
- 两个符号:
&取地址,*解引用 - 三大应用:
- 函数传参修改原值
- 高效遍历数组
- 直接访问硬件寄存器
- 安全第一: 初始化、检查NULL、防止越界
- 调试工具: GDB的
x命令查看内存
推荐学习路径:
- ✅ 掌握本教程所有内容
- ✅ 完成所有练习题
- ✅ 用GDB单步调试每个示例
- ✅ 在实际项目中应用指针
- ✅ 学习更高级的指针用法
记住: 指针是C语言的灵魂,也是嵌入式开发的核心技能,多练多用才能真正掌握!
4万+

被折叠的 条评论
为什么被折叠?



