指针深入理解(二)
volatile关键字
防止优化指向内存地址,
typedef
指针可以指向C语言所有资源
typedef 就是起一个外号。
指针运算符加减标签操作
指针加的是地址,并且增加的是该指针类型的一个单位,指针变量的步长可以用sizeof(p[0])
这两个的p+1是不一样的,因为char是一个字节,int是四个字节,这样char是一个内存地址就可以存储,但是int需要四个内存地址存储。但是默认都是基地址,
指针数组?
地址的标签访问,一定不要理解成数组
标签是访问,p想象成地址变量,然后我们通过标签进行访问,并且这个标签(0、1、2、3)是C语言内部给我们处理好的。
P[2] 相当于是取出内存的值,P+2:指的是内存地址,
变量分配是从内存地址的高到低分配的
int a = 0x12345678;
int b = 0x99999999;
内存是先分配a,然后是b,并且a的地址是高字节,b的是地址是低字节。
取地址,一定是取最低的字节的地址,
内存是先分配a,然后是b,并且a的地址是高字节,b的是地址是低字节。
取地址,一定是取最低的字节的地址,
volatile int a = 0x12345678; int b = 0x99991199; int c = 1;c = a;int *p1 = &b;char *p2 =(char *)&b;printf("the p1+1 is %x,%x,%x\n", (p1+1), p1[1],*p1+1 );printf("the p2+1 is %x,%x,%x\n", p2[1],p2,c);printf("the p2+1 is %x,%x,%x\n", p1,p1[0],p1+1); printf("the p2+1 is %x,%x,%x,%x\n", p2,*p2,p2+1,p2[1]); printf("the p2+1 is %x,%x,%x,%x\n", p1,*p1,p1+1,p1[1]);the p1+1 is 403ffa44,12345678,9999119athe p2+1 is 11,403ffa40,12345678the p2+1 is 403ffa40,99991199,403ffa44the p2+1 is 403ffa40,ffffff99,403ffa41,11the p2+1 is 403ffa40,99991199,403ffa44,12345678
可以明显看出,在分配内存的时候,是从高字节到低字节,
在取内存的时候是从低字节到高字节
并且在p2[1],和p1[1],有明显的区别。
首先p2和p1都是指向的变量b的基地址都是一样的,都是地址6bbffcb0
但是由于指针的类型不一样,导致我们在将基地址加1的时候或者说我们在按照地址的标签访问的时候,p1[1]的步长是四个字节,也就是指向了变量a,因此p[1]指向的值是0x12345678。相反我们将p2的指针类型进行了强制转换为char,导致步长变为1,那么p2[1]指向的值是0x11。从这里我们得出结论,改变指针的类型,并不会改变存储该变量的地址,只是会改变指针标签访问对象,或者说是以基地址位基准的步长会影响,也就是可能会导致我们取地址存储的值的时候取不出来完整性。[引起数据截断]
如这篇文章的解释
指针深入理解(一)-CSDN博客
强制类型转换不改变指针存储的地址值,仅改变对内存数据的解释方式
也就是我原本这四个地址存储的是一个数0x99991199
我使用的是int类型表示的,那么我使用* p2的时候,默认他能看到全部的这四个内存地址,那么就会一口气读出来,告诉我这个值是0x99991199。
但是我强制转换以后,我相当于是眼睛被限制了,我只能看到基地址(也只仅仅是基地址),步长就是1,眼界也就是如此,被限制了。
指针的类型,会限制他看到的内存地址的宽度,(或者是理解成一种类似于宽度的概念。)
所以说定义指针的时候必须要定义是什么类型的,不然都没有眼睛了,怎么取地址,怎么看,另一个就是地址在哪里,不然有了眼睛不知道看到哪里了,不是一样的没有意义。也就是俗称野指针。
volatile int a = 0x12345678; int b = 0x99991199; int c = 1;c = a;int *p1 = &b;char *p2 =(char *)&b;printf("the p1+1 is %x,%x,%x\n", (p1+1), p1[10],*p1+1 );the p1+1 is 1ff754,73ef12ee,9999119a
这是指针的越界,从语法角度越界是允许的,粗暴一点就理解为修改游戏的一些参数,开外挂。
但是在产品中,不能越界,因为会出现一些未知的Bug,导致出现严重Bug。造成不可估量的损失。内存泄漏
有些内存段是不能读的,强行访问就会出现段错误。
越界访问:
volatile int c = 0x11223346;volatile int a = 0x12345678; int b = 0x11223344;// a = 100;// printf("the p2+1 is %x,%x,%x\n", a,p2,c);int *p = &b;// int *p1 = &a; // p[1] = 0x100;printf(" %x,%x\n", p,*p);printf(" %x\n", *(p) ); printf(" %x\n", p[1]); printf(" %x\n", p[2]); printf(" %x\n", p[3]); printf(" %x\n", p[4]);ca7ff73c,11223344112233441234567811223346ca7ff73c8int c = 0x11223346;int a = 0x12345678; int b = 0x11223344;// a = 100;// printf("the p2+1 is %x,%x,%x\n", a,p2,c);int *p = &b;// int *p1 = &a; // p[1] = 0x100;printf(" %x,%x\n", p,*p);printf(" %x\n", *(p) ); printf(" %x\n", p[1]); printf(" %x\n", p[2]); printf(" %x\n", p[3]); printf(" %x\n", p[4]);b47ffc8c,1122334411223344b47ffc8ce71234567811223346
可以看出这两个部分增加Volatile是有区别的,这是跟内存和编译器是有很大区别。
并且const并不是不能改变,只是一种建议字符,可以通过越界的方式访问
int c = 0x11223346;const int a = 0x12345678; int b = 0x11223344;// a = 100;// printf("the p2+1 is %x,%x,%x\n", a,p2,c);int *p = &b;// int *p1 = &a; p[3] = 0x100;printf(" %x,%x\n", p,*p);printf(" %x\n", *(p) ); printf(" %x\n", p[1]); printf(" %x\n", p[2]); printf(" %x\n", p[3]); printf(" %x\n", p[4]);1c5ffe0c,11223344112233441c5ffe0c8210011223346
可以看出const修饰的a还是被改变了。
所以一定不能越界,不然就会出现未知错误。
但是这里有一些疑问,例如为什么内存地址没有连续,以及增加了volatile以后就会不一样,这可能需要以后了解了编译器原理才能对此进行深入理解,暂时就先不纠结了。
指针逻辑操作符
<= >= == != &&
一般是用的是跟一个特殊值进行比较,== 或者 != 0x0. 0值进行比较 地址的无效值,结束标志,没有空间。
NULL 这个就是一个0,只不过是一个任意类型的0。
指针是同类型比较,才能编译通过。不能说char和int进行比较。
多级指针(二维指针)
int * *P
说白了就是存放的是地址,只不过这个地址指向的是一个值。
说穿了就是一级一级的套娃,
只是将一级指针的地址给存储起来了。
char ** p
二维指针。
把一些毫不相干的东西,组合成一种线性关系。描述的不是内存是什么,而是内存与内存之间的关系。
感性理解是
原本这两个字符的内存是不连续的,可以说是毫无关系,但是我通过多级指针,可以将他们的内存联系起来,构造到一个连续的内存空间,“相当于是将原本存储字符串的内存地址,在外面给包裹起来了,然后让他们的地址存出来连续的内存中,多了一层封装,从而形成了一段线性关系。”
二维指针的大小,
p[m]== NULL的时候,相当于这个内存空间结束了。二维指针结束标志。
一定要记住这个模型,
地址的结束标志是NULL,
static关键字
static uint8_t bvalid = 0;和
static uint8_t bvalid; bvalid = 0;` 这两种写法在语法和底层逻辑上存在关键区别,能否等价替换取决于代码所处的作用域和实际需求。以下是具体分析:
1. 全局作用域或文件作用域
- 原写法
static uint8_t bvalid = 0;
这是标准的静态变量初始化方式,编译器会在程序启动时自动完成初始化,且仅执行一次。静态变量的默认初始化值为0,但显式赋初值(如=0
)可以提高代码可读性 - 改写为
static uint8_t bvalid; bvalid = 0;
语法错误:在全局作用域中,bvalid = 0;
是赋值语句,无法直接出现在函数体外。C语言规定全局作用域只能进行变量声明和初始化,不能包含可执行代码
2. 函数内部(局部静态变量)
-
原写法
static uint8_t bvalid = 0;
这是局部静态变量的初始化方式,仅在首次进入函数时执行一次,后续调用时保留修改后的值void func() {static uint8_t bvalid = 0; // 仅初始化一次bvalid++;printf("%d", bvalid); // 输出会递增:1, 2, 3... }
-
改写为
static uint8_t bvalid; bvalid = 0;
虽然语法合法,但语义不同:bvalid = 0;
会在每次调用函数时执行,导致静态变量被重复重置为0。例如:void func() {static uint8_t bvalid; // 默认初始化为0(仅一次)bvalid = 0; // 每次调用时赋值bvalid++;printf("%d", bvalid); // 始终输出1 }
这破坏了静态变量的“持久性”特性
3. 总结与建议
场景 | 原写法 static uint8_t bvalid = 0; | 改写 static uint8_t bvalid; bvalid = 0; |
---|---|---|
全局/文件作用域 | 正确,初始化仅一次 | 语法错误,无法编译通过 |
函数内部 | 正确,初始化仅一次,保留持久性 | 合法但语义错误,每次调用都会重置变量 |
- 推荐做法:
若需静态变量仅初始化一次并保留其持久性,必须使用原写法。
若需每次进入作用域时重置值(如函数内需重复初始化的场景),应避免使用静态变量,改用普通局部变量。
4. 底层机制
-
初始化 (
=0
):
在编译或首次运行时完成,存储在程序的.data
或.bss
段(未显式初始化的静态变量默认置0) -
赋值 (
bvalid = 0
):
是运行时操作,每次执行时覆盖当前值,可能产生额外的机器指令
5. 特殊情况
若静态变量是复杂类型(如结构体或类对象),初始化和赋值可能涉及构造函数与赋值运算符的调用差异,但对基本类型(如uint8_t
)两者效果相同(仅语义不同)
结论:在用户提供的代码片段中,若 bvalid
需保持静态变量的持久性,不能直接改写为分开的声明和赋值形式。若在函数内部强制要求每次调用时重置值,需权衡是否真正需要静态存储特性。
【版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。】