指针的应用学习日记
Git常见的命令:
%h 简化哈希
%an 作者名字
%ar 修订日期(距今)
%ad修订日期
%s提交说明
指针简介
指针(Pointer)是C语言的一个重要知识点,其使用灵活、功能强大,是C语言的灵魂。
指针与底层硬件联系紧密,使用指针可操作数据的地址,实现数据的间接访问。
计算机的存储机制一般是以小端进行存储,数组的存储必须是连续的。
指针的加加减减一般用在数组方面。
1. 把内存想对了:地址 + 类型 = 解释方式
地址(address) 是一个无符号整数,指向内存中某个字节的“门牌号”。
类型(type) 告诉编译器“这堆字节该按什么格式解释”,并决定了指针的步长、对齐、
*
解引用后得到的对象类型等。指针(pointer) 就是“携带了类型信息的地址”。
关键观点:指针不是内存本身,指针只是“如何找到并解释内存”的方法。
2. 基础操作:取址、解引用、指针运算
int x = 42;
int *p = &x; // 取址:&x 的类型是 int*
*p = 100; // 解引用:写入 x
printf("%d\n", *p); // 读取 xint a[4] = {1,2,3,4};
int *q = a; // 数组名在表达式中大多数场合衰减为指向首元素的指针,等价于 &a[0]
q++; // 指针运算:q 跳到下一个 int 元素(步长 sizeof(int))
printf("%d\n", *q); // 打印 2
3. const
/ volatile
/ restrict
到底修饰谁?
读法建议:从标识符向右再向左读,先绑定最近的。
const int *p; // 指向“const int”的指针:不能通过 p 改值,但能让 p 指向别处
int * const p2=&x; // const 指针:p2 自身不可改,但可改它指向的 int 值
const int * const p3=&x; // 都不能改volatile uint32_t *reg; // 典型寄存器映射:每次读写都不能被优化掉
volatile:告诉编译器不要优化掉对该对象的访问(硬件寄存器/中断共享变量/内存映射IO)。
restrict
(C99):承诺同一作用域内通过该指针访问的对象不与其他指针别名,利于优化(用于 DSP/大数组运算)。必须百分百保证不别名才用。
4. 函数参数:传入大对象、输出参数、多返回值
传大对象:用指针/const
指针避免拷贝。
**输出参数:用指针作为“返回槽位”**实现多返回值。
typedef struct {uint8_t id;uint8_t payload[32];
} Frame;// 仅读入参:用 const 指针,避免拷贝
void process_frame(const Frame *f) {// 不能修改 *f,保证调用方数据安全
}// 多返回值:用指针作为输出槽位
bool parse_header(const uint8_t *buf, size_t n,uint8_t *out_id, uint16_t *out_len)
{if (n < 3) return false;*out_id = buf[0]; // 写回调用者的变量*out_len = (uint16_t)buf[1] << 8 | buf[2];return true;
}
5. 返回指针:生命周期必须搞清楚
int* bad(void) {int local = 123;return &local; // ❌ 返回了栈上临时变量的地址,离开函数后悬空
}int* ok_heap(void) {int *p = malloc(sizeof *p); // ✅ 堆上if (p) *p = 123;return p; // 记得由调用方 free(p)
}int* ok_static(void) {static int s = 123; // ✅ 静态存储期(程序结束才回收),但非线程安全return &s;
}
6. 数组与指针的那些“坑”
int a[4];
printf("%zu\n", sizeof(a)); // 16(整块数组大小)int *p = a;
// 注意:在函数形参里写 int a[] 和 int *a 是一样的(数组会衰减为指针)
// 所以在函数里对 a 用 sizeof 得到的是指针大小,而不是数组总大小// 区分“指向数组的指针”与“指针数组”
int (*pa)[4] = &a; // pa 的类型:指向“含 4 个 int 的数组”
int *ap[4]; // ap 的类型:含 4 个“int*”的数组
字符串常量:
char *s = "abc"; // 旧式写法,"abc" 在只读区,修改 *s 未定义行为
const char *s2 = "abc"; // 推荐:只读
char s3[] = "abc"; // 拷贝到栈上,可以改 s3[0] = 'A';
7. 指针算术、对齐与别名
- p + k 跳过 k * sizeof(*p) 字节。
- 对齐:把 uint8_t* 强转为 uint32_t* 读写前,要保证地址按 4 字节对齐,否则可能硬件 Fault(很多 MCU 如 ARM Cortex-M 严格对齐)。
- 别名:通过不同类型指针访问同一内存可能违反 严格别名规则,优化后会出现玄学 Bug。跨类型拷贝请用 memcpy。
// 不要直接用强转跨类型暴力读写
uint32_t get_u32_unaligned(const uint8_t *p) {uint32_t v;memcpy(&v, p, sizeof v); // 安全、无对齐和别名风险return v;
}
8. 二级指针(指向指针):动态分配/修改实参的指针值
bool make_buffer(uint8_t **out_buf, size_t n) {uint8_t *p = malloc(n);if (!p) return false;memset(p, 0, n);*out_buf = p; // 修改调用者手里的“指针变量”return true;
}int main(void){uint8_t *buf = NULL;if (make_buffer(&buf, 128)) {// 使用 buffree(buf);}
}
9. 函数指针与回调(状态机/驱动表必备)
// 1) 普通函数指针
typedef int (*cmp_fn)(const void*, const void*);// 2) 嵌入式驱动“虚表”——把不同实现装入同一接口
typedef struct {void (*init)(void);void (*write)(const uint8_t *data, size_t n);size_t (*read)(uint8_t *buf, size_t n);
} UartOps;// 某个具体 UART 的实现
static void usart1_init(void) { /* 硬件初始化 */ }
static void usart1_write(const uint8_t *d, size_t n) { /* 发送 */ }
static size_t usart1_read(uint8_t *b, size_t n) { /* 接收 */ return 0; }static const UartOps USART1_Ops = {.init = usart1_init,.write = usart1_write,.read = usart1_read,
};// 使用时只依赖接口指针
void app_run(const UartOps *ops) {ops->init();const uint8_t msg[] = "Hello";ops->write(msg, sizeof msg);
}
10. 嵌入式必修:寄存器内存映射(volatile
+ 结构体)
以 STM32F103 为例(示意,寄存器偏移按参考手册调整):
#include <stdint.h>typedef struct {volatile uint32_t CRL; // 配置低 8 个 IOvolatile uint32_t CRH; // 配置高 8 个 IOvolatile uint32_t IDR; // 输入数据寄存器volatile uint32_t ODR; // 输出数据寄存器volatile uint32_t BSRR; // 置位/复位volatile uint32_t BRR;volatile uint32_t LCKR;
} GPIO_TypeDef;#define PERIPH_BASE (0x40000000UL)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000UL)
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800UL)#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)static inline void gpioa_set_pin(uint32_t pin) {GPIOA->BSRR = (1U << pin); // 写 BSRR 置位
}static inline void gpioa_reset_pin(uint32_t pin) {GPIOA->BRR = (1U << pin); // 写 BRR 复位
}void blink(void) {// 假设 PA5 已配置为推挽输出gpioa_set_pin(5);// delay...gpioa_reset_pin(5);
}
要点:
所有寄存器字段必须是
volatile
,防止编译器优化掉访问。把基地址强转为“寄存器布局结构体指针”,代码更可读、可维护。
11. 环形缓冲区(UART/传感器常用)——指针版
typedef struct {uint8_t *buf; // 数据区size_t cap; // 容量(必须为 2 的幂以便快速取模,可选)size_t head; // 写指针size_t tail; // 读指针
} RingBuf;// 初始化:外部提供存储,避免 malloc
void ring_init(RingBuf *rb, uint8_t *storage, size_t cap) {rb->buf = storage;rb->cap = cap;rb->head = rb->tail = 0;
}bool ring_put(RingBuf *rb, uint8_t v) {size_t next = (rb->head + 1) % rb->cap;if (next == rb->tail) return false; // 满rb->buf[rb->head] = v;rb->head = next;return true;
}bool ring_get(RingBuf *rb, uint8_t *out) {if (rb->head == rb->tail) return false; // 空*out = rb->buf[rb->tail];rb->tail = (rb->tail + 1) % rb->cap;return true;
}
这里 buf
就是一块“外部管理的内存”的指针,分离存储与逻辑,是指针设计的经典范式。
12. 内存区域与生命周期
栈(auto):函数内局部变量,离开作用域即失效。
静态/全局(static):程序全程有效,初始化一次。
堆(heap):
malloc/free
管理,灵活但需负责释放(嵌入式尽量少用或自建内存池)。
13. 常见 Bug 快查表(以及怎么防)
悬空指针:返回局部变量地址;释放后继续用。→ 规则:谁分配谁释放,置
p=NULL
。越界:指针运算超出对象边界。→ 规则:所有索引做边界检查。
未对齐访问:
uint32_t*
指向非 4 字节对齐地址。→ 规则:跨类型用memcpy
。严格别名违规:不同类型指针访问同一对象。→ 规则:用
memcpy
或通过unsigned char*
。多线程/中断竞态:ISR 与主循环共享变量未
volatile
/未屏蔽中断。→ 规则:volatile
+ 临界区。格式化打印错误:
printf("%d", p)
。→ 规则:指针打印用%p
,强制转换(void*)p
更稳。
调试建议(PC 端):开启最高警告级别、静态分析、ASan/UBSan(嵌入式可在宿主机先验证算法)。
Keil/MDK:用“Watch/Memory”窗口观察地址,查看 Map 文件确认符号地址;优化等级过高时留意 volatile
与 -O
的交互。
14. 速记清单(工程实践)
读右左法:从变量名向右再向左解析
const/volatile/*/()
。函数只读入参:
const T *p
;输出参:T *out
。访问寄存器:
volatile
是底线;结构体映射可读性最好。跨类型读写一律
memcpy
;避免未定义行为。需要修改“指针变量本身”:传
T **
。大对象传参用指针,别拷贝;必要时配合
restrict
。数组参数在函数里就是指针;
sizeof
不再是整块大小。区分
T (*p)[N]
(指向数组)与T *p[N]
(指针数组)。回调/状态机/驱动表用“函数指针 + 结构体”。
生命周期先于技术:你指到的东西还活着吗?
15. 小练习
练习 A: 写 swap_int(int *a, int *b)
;写 split_u16(uint16_t v, uint8_t *hi, uint8_t *lo)
。
练习 B: 实现一个 hex_dump(const void *buf, size_t n)
,每 16 字节一行打印地址和内容。
练习 C: 定义 GPIO_TypeDef
并把某个端口第 7 位反转(BSRR/BRR 或 ODR ^= 1<<7)。
练习 D: 用“函数指针表”实现 I2C
与 SPI
的同一 SensorOps
接口,主逻辑只依赖接口。
练习 E: 写一个安全的 read_u32_be(const uint8_t *p)
(大端),注意对齐问题。
// A
void swap_int(int *a, int *b){ int t=*a; *a=*b; *b=t; }
void split_u16(uint16_t v, uint8_t *hi, uint8_t *lo){*hi = (uint8_t)(v >> 8); *lo = (uint8_t)(v & 0xFF);
}// B
void hex_dump(const void *buf, size_t n){const unsigned char *p = (const unsigned char*)buf;for(size_t i=0;i<n;i+=16){printf("%p: ", (void*)(p+i));for(size_t j=0;j<16 && i+j<n; ++j) printf("%02X ", p[i+j]);printf("\n");}
}// C(示意)
static inline void gpio_toggle_bit(GPIO_TypeDef *port, uint32_t bit){if (port->ODR & (1U<<bit)) port->BRR = (1U<<bit);else port->BSRR = (1U<<bit);
}// E
uint32_t read_u32_be(const uint8_t *p){uint32_t v; // 不直接强转,避免未对齐uint8_t t[4] = {p[0],p[1],p[2],p[3]};// 若目标是小端 MCU(如 Cortex-M),需要字节翻转v = ((uint32_t)t[0]<<24)|((uint32_t)t[1]<<16)|((uint32_t)t[2]<<8)|t[3];return v;
}
16. 下一步进阶建议
指针 + DMA:零拷贝采样缓冲、双缓冲(ping-pong)技巧。
container_of
/偏移:驱动里常见的“由成员指针反推外层结构体”(注意可移植性与别名规则)。内存池:在 MCU 上用固定块内存池替代
malloc/free
,指针是句柄。接口抽象:把“操作集合”收拢成结构体的函数指针表,组件可插拔(驱动/协议/UI 控制器都适用)。