C语言基础回顾(2026/7/18) 目录指针1. 地址、、*2.容易混淆的点3. 指针类型4.指针和数组5. 字符串指针6. 指针传参7.指针指向8.const 与指针①锁内容②锁指针③双锁④总结记忆点9.volatile与const①硬件寄存器映射②中断服务程序ISR与主循环通信10.野指针/空指针/悬垂指针①野指针②空指针③悬垂指针④总结11void * 是什么①可以接收任何类型的地址不用强转②不能直接解引用③为什么要给 %p 强制转 (void *)指针1. 地址、、*每个变量都住在内存里有一个门牌号——地址。int a 10; int *p a; // 取地址把 a 的门牌号存进 p int val *p; // * 解引用按门牌号找到房间取出里面的值 → 10x→ 问x 住哪→ 拿到地址*p→ 按地址 p 去敲门 → 读写那个位置的值int *p里*是类型的一部分意思是 p 是指针变量不是解引用记一句话声明时*表示这是一个指针使用时*表示去那看看。2.容易混淆的点int *p 和 int* p 对编译器来说完全没区别生成的代码一模一样但它们的“写法含义”和“易踩的坑”不一样。int* p 的写法; // 把 *p 看成一个整体*p 的类型是 intint *p; 的写法 传统的、最符合语言设计的写法int* p; // 看上去像p 的类型是 int* 这种是最直观也是最容易引起混淆的写法比如int* p, q; 很多人会以为p和q都是指针但其实p 是 int* q 是普通的 int 因为*只绑在p上等价于int *p, q;因此我们声明指针的时候写成int *p不要写成int* p3. 指针类型指针不光存地址还知道它指向什么类型。这对嵌入式很重要内存移位频繁用到。char *pc; // 指向 charpc1 跳 1 字节int *pi; // 指向 intpi1 跳 4 字节short *ps; // 指向 shortps1 跳 2 字节指针 ±1 跳的步长 sizeof(指向的类型)。#include stdio.h int main() { int arr[] {10, 20, 30}; int *p arr; printf(%d\n, *p); // *p指向数组的第一个元素 printf(%d\n, *(p 1)); // *(p 1)指向数组的第二个元素 printf(%d\n, *(p 2)); // *(p 2)指向数组的第三个元素 return 0; }4.指针和数组int arr[5] {1, 2, 3, 4, 5}; int *p arr; // 等价于 int *p arr[0];数值名首元素地址arr[i]本质就是*(arr i)因为数组首元素下标从0开始此处假设i3a[3]就是第四个元素 arr为首元素地址arr3指向第四个元素地址随后解引用*arri即得到第四个元素。指针可以而数组名不可以int *p;p 是一个指针变量——它里面存的是一个地址而且这个地址是可以被改变的。p;等价于p p 1;因为p是int*这里的1不是加 1 个字节而是加sizeof(int)个字节通常 4 字节。也就是让p指向下一个 int 元素。例子int a 10, b 20; int *p a; p; // 现在 p 不再指向 a // 如果 p 原来指向 ap 后 p 存的地址是 a 4p 后变成野指针了所以实际中我们常把 p 指向数组int arr[3] {1, 2, 3}; int *p arr; // p 指向 arr[0] p; // p 指向 arr[1] printf(%d\n, *p); // 输出 2因为p是变量所以可以自增、自减、重新赋值。为什么arr不行定义int arr[3] {1, 2, 3};arr 是数组名。在绝大多数表达式里它会自动转换成第一个元素的地址但这个“转换来的指针”有个关键性质它是右值常量也就是int *arrarr[0]它是右值常量不能放在赋值号左边也不能自增/自减。arr; // ❌ 编译错误左值 required需要可修改的左值就好比你不能写100; // ❌因为100是常量arr在表达式中也是个地址常量——数组在内存中的起始位置是固死的不能让它“挪动”。5. 字符串指针char *s1 hello; // 指向只读常量区 char s2[] hello; // 在栈上复制了一份可修改// s1[0] H; ❌ 段错误/未定义行为 s2[0] H; // ✅ 合法Hello6. 指针传参C 语言全是值传递。想在一个函数里改外部变量必须传地址例1int x5 y10 // 错误示范 void bad_swap(int a, int b) { int t a; a b; b t; // 改的是副本外面没变 }一句话总结实参x, y把自己的值复制了一份交给形参a, b。a, b有自己独立的地址怎么折腾都跟x, y没关系——C 语言里函数调用就是复印机永远复印值不传原件。唯一的例外想改原件传原件的地址让被调函数顺着地址摸回原件去改。例2void good_swap(int *a, int *b) { int t *a; *a *b; *b t; // 通过地址改原值 } int x 5, y 10; good_swap(x, y); // x10, y5调用 good_swap(x, y) — 参数传递瞬间把x 和y 的地址传入 a 和ba 和b 记住了x和y 对应的地址。然后*不是在声明而是在解引用因此*a是指向x 的地址取出x对应的值因此int t 5随后*a *b 解引用找到y 对应的地址下对应的值10把 它复制给x *bt 把y改成5最后得到x 10 y57.指针指向int *p; 这句话的意思是p 是一个指针变量。它的类型是 int *它指向 int。定义int *p; 中“指向 int”的意思是p 这个指针变量里存的是一个地址而这个地址对应的那块内存里放的是一个 int 类型的数据。int a 10; int *p a; // p 指向 aa 的类型是 int整型值是 10住在 0x1000。p 的类型是 int *整型指针它里面填的是 0x1000。所以我们说p指向a且因为a是int所以p是“指向 int 的指针”我们可以通过p去访问该intprintf(%d\n, *p); // 输出 10*p 就是 p 指向的那个 int也就是变量a。8.const与指针第一层容易混淆的写法int *p; // * 贴着变量名 int* p; // * 贴着类型 int * p; // * 两边都空格 int*p; // 挤成一团这四个对编译器来说是同一个东西。空格在 C 里不改变语义。所以你看到的int *p和int* p只是不同程序员的审美偏好可以当它们不存在。但这造成了一个视觉陷阱int* p, q; // 看起来像声明了两个指针 // 错p 是指针q 是普通 int // 因为 * 只跟 p 走不跟 int 走这就是为什么很多人坚持写int *p——时刻提醒自己*是绑在变量名上的不是绑在int上的。第二层const怎么看——只看*和const的相对位置原则const 修饰它左边的东西如果左边没有就修饰右边这是 C 的语法规则。所有混乱都来自一个事编译器不管空格但你眼睛要管的只有*和const谁在左谁在右分清这个就够了。把噪声删掉只留骨架①锁内容这里的锁意思是——你不能用p这个通道去改数据有两种写法int const *p / const int *p这是历史的锅。C 最初用const int *pconst打头后来又允许int const *pconst放类型后面也能修饰类型。两条路到达同一个终点int a 10, b 20; const int *p a; // *p 100; // ❌ 报错不能通过这个指针改数据 p b; // ✅ 允许指针可以改指向关键点被指的那个变量本身不一定真的是const。比如上面a就是普通int你直接写a 100;完全合法只是不能通过p这条通道改它所以内容锁更准确的说法是这条路是单行道只能读不能写。②锁指针锁的是p指针本身写法是const在*右边int *const p a; // ⚠️ 必须初始化因为之后不能改了这里的锁意思是——p这个变量里存的地址不能再变写法int * const pint a 10, b 20; int *const p a; *p 100; // ✅ 允许可以通过 p 改数据 // p b; // ❌ 报错指针不能改指向因为p一旦定下来就不能再换所以声明时必须初始化③双锁内容和指针都锁两种写法const int * const p / int const * const pconst int *const p a;你不能通过p改数据❌*p 100;也不能通过p 指向别处❌ p b;两个方向都被堵死限制最严④总结记忆点不要想const修饰int *而是想const修饰的是数据还是指针嵌入式里const指针大量用在只读寄存器映射—const volatile uint32_t *硬件状态寄存器的经典声明接口契约— 函数参数加const告诉调用者我不会改你的数据查表/常量数组—const uint16_t sin_table[]放 Flash 不占 RAM9.volatile与constvolatile 是 C/C 里的一个类型限定符和 const 平级用来告诉编译器“这个变量随时可能被意想不到的因素改变你每次用到它都必须老老实实从内存里重新读不准偷懒用缓存”一句话记volatile “易变的别优化我”。禁止优化缓存每次直读直写为什么需要volatile编译器的小聪明int flag 1; while (flag) { // 什么都不干空转 }如果没有volatile编译器发现循环里根本没人改flag就会把它当成恒定值1直接把循环优化成死循环——甚至干脆删掉读取flag的代码因为你“明明没改它”。但如果flag是被中断服务程序、硬件、另一个线程悄悄改掉的那就出大事了程序永远停不下来。volatile int flag 1; while (flag) { // 每次都会从内存读 flag 的真实值 }编译器就不敢优化了每次循环都去内存查flag有没有变成0。三大应用场景①硬件寄存器映射硬件状态寄存器会被外设自动改写// 假设 0x40021000 是一个硬件状态寄存器 #define STATUS_REG (*(volatile unsigned int *)0x40021000) while (STATUS_REG 0) { ; // 等待硬件把寄存器置 1 }如果不加volatile编译器可能只读取一次就完事导致永远等不到硬件变化。②中断服务程序ISR与主循环通信volatile int timer_tick 0; // 中断里 void TIM_IRQHandler(void) { timer_tick 1; } // 主循环 while (1) { if (timer_tick) { timer_tick 0; do_task(); } }timer_tick 在中断里改主循环读必须 volatile。10.野指针/空指针/悬垂指针这三种指针是 C 语言里最常见的指针炸了场景本质区别在于指针指向的地址是什么状态①野指针int *p声明了但是没有给p赋值。p里面可能有栈上残留的随机值比如0x7f456461456*p10往一个随机的内存地址写了 10可能造成段错误 / 改坏别人的数据 / 程序莫名奔溃修复声明时立刻初始化。②空指针在 C 语言中我们通常用NULL来表示空指针int *p NULL; // C 语言风格空指针不是未初始化的指针野指针。空指针是故意赋成的一个安全状态用来标记“这个指针现在什么也没指”。#include stdio.h int main() { int *p NULL; printf(%x\n, (void *)p); }如果随便用一个乱七八糟的数字比如-1或0xCCCCCCCC来表示“空”不仅不直观还容易和合法的内存地址混淆。因此编程界达成了一个共识干脆统一用数字0来代表“空”的状态。所以当你打印p的值时它显示出来的就是0或者十六进制的0x00000000。空指针的意义不是让你去用它而是让你能检查它空指针是 C 里的哨兵值——崩溃是可预测的比野指针的随机破坏好排查一万倍。让你去访问p对应地址下的值直接系统出手了。③悬垂指针指向已释放的内存这是最阴险的一种指针里存的地址曾经是合法的但现在那块内存已经被回收了。修复永远不要返回局部变量的地址。如果需要用malloc分配堆内存或者让调用者把缓冲区传进来。④总结11void *是什么一句话void *是 C 里的万能指针——可以存放任何类型的地址但不能直接解引用。可以把它理解成一个只有门牌号、没有户型信息的指针int *p_int → 不仅知道地址是 0x1000还知道里面住的是 int4 字节 double *p_dbl → 不仅知道地址是 0x2000还知道里面住的是 double8 字节 void *p_void → 只知道地址是 0x1000不知道里面住的是什么类型①可以接收任何类型的地址不用强转int x 42; double y 3.14; char z A; void *p; p x; // ✅ 隐式转换int* → void* p y; // ✅ 隐式转换double* → void* p z; // ✅ 隐式转换char* → void*C 语言里任何指针类型都可以隐式转成void*不需要写强制转换。这就是它万能的含义。②不能直接解引用void *p x; // *p 100; // ❌ 报错编译器不知道要读几个字节 // 是 1 字节char4 字节int8 字节double // 必须先强制转回具体类型才能用 *(int *)p 100; // ✅ 告诉编译器按 int 的规则读就像一个快递员拿着门牌号但是不知道里面寄的是什么——void*丢了类型信息要你自己先认定它是什么类型再操作③为什么要给%p强制转(void *)%p的格式说明符要求参数必须是void *类型。因为你传的可能是int*、char*、double*……但printf是变参函数编译器不做隐式转换所以必须手动统一成 void*int *p x; printf(%p\n, (void *)p); // 显式转成 void*满足 %p 的要求它就是把你指针里存的那个地址值用十六进制打印出来不转的话行为是未定义的虽然大部分平台也能跑但规范上不对。%p里的p就是pointer指针的首字母因为%x不保证位数、不保证前缀而%p保证按平台的地址格式来。