当前位置: 首页 > ds >正文

指针的应用学习日记

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)。

restrictC99):承诺同一作用域内通过该指针访问的对象不与其他指针别名,利于优化(用于 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 快查表(以及怎么防)

  1. 悬空指针:返回局部变量地址;释放后继续用。→ 规则:谁分配谁释放,置 p=NULL

  2. 越界:指针运算超出对象边界。→ 规则:所有索引做边界检查。

  3. 未对齐访问uint32_t* 指向非 4 字节对齐地址。→ 规则:跨类型用 memcpy

  4. 严格别名违规:不同类型指针访问同一对象。→ 规则:用 memcpy 或通过 unsigned char*

  5. 多线程/中断竞态:ISR 与主循环共享变量未 volatile/未屏蔽中断。→ 规则:volatile + 临界区。

  6. 格式化打印错误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: 用“函数指针表”实现 I2CSPI 的同一 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 控制器都适用)。

http://www.xdnf.cn/news/18401.html

相关文章:

  • 算法训练营day55 图论⑤ 并查集理论基础、107. 寻找存在的路径
  • 信号和共享内存
  • Linux------《零基础到联网:CentOS 7 在 VMware Workstation 中的全流程安装与 NAT 网络配置实战》
  • Visual Studio 2022+OpenCV-Python安装及配置方法
  • 涡流-信号完整性分析
  • pytest高级用法之插件开发
  • 1A AMOLED显示屏电源芯片BCT1838
  • 01-Docker-简介、安装与使用
  • Day09 Go语言深入学习(1)
  • 进程与线程
  • langchain的简单应用案例---(1)使用langchain构建本地知识库
  • K近邻算法(knn)
  • 基于 RxJava 构建强大的 Android 文件下载管理器
  • Android SystemServer 中 Service 的创建和启动方式
  • AI与大数据驱动下的食堂采购系统源码:供应链管理平台的未来发展
  • Git#cherry-pick
  • QT示例 基于Subdiv2D的Voronoi图实现鼠标点击屏幕碎裂掉落特效
  • Day22 顺序表与链表的实现及应用(含字典功能与操作对比)
  • 服务器无公网ip如何对外提供服务?本地网络只有内网IP,如何能被外网访问?
  • Vue.prototype 的作用
  • JUC之CompletableFuture【中】
  • Redis Reactor 模型详解【基本架构、事件循环机制、结合源码详细追踪读写请求从客户端连接到命令执行的完整流程】
  • FPGA 在情绪识别领域的护理应用(一)
  • 论文阅读系列(一)Qwen-Image Technical Report
  • 中和农信如何打通农业科技普惠“最后一百米”
  • 企业架构是什么?解读
  • 通过分布式系统的视角看Kafka
  • python黑盒包装
  • Matplotlib数据可视化实战:Matplotlib图表注释与美化入门
  • 抓取手机游戏相关数据