目录
指针
1. 地址、&、*
2.容易混淆的点:
3. 指针类型
4.指针和数组
5. 字符串指针
6. 指针传参
7.指针指向
8.const 与指针
①:锁内容
②:锁指针
③:双锁
④:总结记忆点
9.volatile与const
①:硬件寄存器映射
②:中断服务程序(ISR)与主循环通信
10.野指针/空指针/悬垂指针
①:野指针
②:空指针
③:悬垂指针
④总结:
11:void * 是什么
①:可以接收任何类型的地址(不用强转)
②:不能直接解引用
③:为什么要给 %p 强制转 (void *)
指针
1. 地址、&、*
每个变量都住在内存里,有一个门牌号——地址。
int a = 10; int *p = &a; // & 取地址:把 a 的门牌号存进 p int val = *p; // * 解引用:按门牌号找到房间,取出里面的值 → 10&x→ 问"x 住哪"→ 拿到地址*p→ 按地址 p 去敲门 → 读写那个位置的值int *p里*是类型的一部分,意思是 p 是指针变量,不是解引用
记一句话:声明时*表示"这是一个指针",使用时*表示"去那看看"。
2.容易混淆的点:
int *p 和 int* p 对编译器来说完全没区别,生成的代码一模一样,但它们的“写法含义”和“易踩的坑”不一样。
int* p 的写法; // 把 *p 看成一个整体,*p 的类型是 int
int *p; 的写法 传统的、最符合语言设计的写法
int* p; // 看上去像:p 的类型是 int* ,这种是最直观,也是最容易引起混淆的写法
比如int* p, q; 很多人会以为p和q都是指针,但其实:p 是 int* q 是普通的 int ,
因为*只绑在p上,等价于:int *p, q;
因此我们声明指针的时候,写成int *p,不要写成int* p
3. 指针类型
指针不光存地址,还"知道"它指向什么类型。这对嵌入式很重要,内存移位频繁用到。
char *pc; // 指向 char,pc+1 跳 1 字节int *pi; // 指向 int,pi+1 跳 4 字节short *ps; // 指向 short,ps+1 跳 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开始
此处假设i=3,a[3]就是第四个元素, arr为首元素地址,(arr+3)指向第四个元素地址,随后
解引用,*(arr+i)即得到第四个元素。
指针可以++,而数组名不可以:
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 原来指向 a,p++ 后 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 *arr=&arr[0];
它是右值(常量),不能放在赋值号左边,也不能自增/自减。
arr++; // ❌ 编译错误:左值 required(需要可修改的左值)
就好比你不能写:100++; // ❌
因为100是常量,arr在表达式中也是个地址常量——数组在内存中的起始位置是固死的,不能让它“挪动”。
5. 字符串指针
char *s1 = "hello"; // 指向只读常量区 char s2[] = "hello"; // 在栈上复制了一份,可修改// s1[0] = 'H'; ❌ 段错误/未定义行为! s2[0] = 'H'; // ✅ 合法,"Hello"6. 指针传参
C 语言全是值传递。想在一个函数里改外部变量,必须传地址:
例1:
int x=5 ,y=10; // 错误示范 void bad_swap(int a, int b) { int t = a; a = b; b = t; // 改的是副本,外面没变 }一句话总结:实参x, y把自己的值复制了一份交给形参a, b。a, b有自己独立的地址,怎么折腾都跟x, y没关系——C 语言里函数调用就是复印机,永远复印值,不传原件。唯一的例外:想改原件,传原件的地址,让被调函数顺着地址摸回原件去改。
例2:
void good_swap(int *a, int *b) { int t = *a; *a = *b; *b = t; // 通过地址改原值 } int x = 5, y = 10; good_swap(&x, &y); // x=10, y=5调用 good_swap(&x, &y) — 参数传递瞬间:把x 和y 的地址传入 a 和b,a 和b 记住了x和y 对应的地址。
然后*不是在声明,而是在解引用,因此*a,是指向x 的地址取出x对应的值,因此int t =5;
随后*a =*b ,解引用,找到y 对应的地址下对应的值10,把 它复制给x ,
*b=t ,把y改成5
最后得到x = 10, y=5
7.指针指向
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去访问该int
printf("%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 *p(const打头),后来又允许int const *p(const放类型后面也能修饰类型)。两条路到达同一个终点:
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 p
int a = 10, b = 20; int *const p = &a; *p = 100; // ✅ 允许:可以通过 p 改数据 // p = &b; // ❌ 报错:指针不能改指向因为p一旦定下来就不能再换,所以声明时必须初始化
③:双锁
内容和指针都锁
两种写法:const int * const p / int const * const p
const 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 不占 RAM
9.volatile与const
volatile 是 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
*p=10;往一个随机的内存地址写了 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分配堆内存,或者让调用者把缓冲区传进来。
④总结:
11:void *是什么
一句话:void *是 C 里的"万能指针"——可以存放任何类型的地址,但不能直接解引用。
可以把它理解成一个只有门牌号、没有户型信息的指针:
int *p_int → 不仅知道"地址是 0x1000",还知道"里面住的是 int(4 字节)" double *p_dbl → 不仅知道"地址是 0x2000",还知道"里面住的是 double(8 字节)" 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 字节(char)?4 字节(int)?8 字节(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保证按平台的地址格式来。