工作数年还是被指针坑了—谈谈C语言指针运算

C指针:避不开的话题

指针,表征内存地址的一个变量。
与其他编程语言比较,由于指针使用的灵活性和高效性,常常和C的数组/结构体一同使用,算是C语言的一个重要特点了。

一个和指针类型有关的陷阱

笔者在从stack中取数时,发现这样一个问题:
栈底的位置位于0xFFC2_0000,栈顶的位置位于0xFFC0_0000,假设栈底48个字节的数据如下排放:

addressdata
0xFFC2_00000x00
0xFFC1_FFFF0xFF
0xFFC1_FFFE0xFE
0xFFC1_FFFD0xFD
0xFFC1_FFF50xF5
0xFFC1_FFF40xF4
0xFFC1_FFF00xF0
0xFFC1_FFE00xE0
0xFFC1_FFD00xD0

指针pBStack指向0xFFC2_0000,如果pBStack为UINT32类型,即无符号4字节整型,那么通过减法运算,永远无法取出以0xFFC1_FFF5为起始地址的32位数据,即用以下code永远无法访问到0xF8F7F6F5的数据:

pBStack = (UINT8*)(UINTN)STACK_BOTTOM;
for(index = 0; index < 48; index++){
  printf("Addr = 0x%x, Data = 0x%x\n", pBStack-index, 
  *((volatile UINT32*)(pBStack-index)));
}

回溯指针的算数运算

指针加上或减去一个整型数据,还是一个指针,问题在于这个指针究竟指向哪个数据单元呢?很显然,下面这些运算结果都是不一样的:
1.(UINT8 *)p + 1
2. (UINT16 *) p + 1
3. (UINT32 *) p + 1
4. (UINT64 *) p + 1
因为当一个指针和一个整数进行加减运算的时候,整数会在执行运算前根据指针的类型进行一定的调整;因此上述1的结果指向下一个字节,2的结果指向+2个字节处,3的结果指向+4个字节处,4的结构指向+8个字节处。
以C语言中常见的数据类型为例,下面列出指针运算的实际指向结果:
指针加法运算结果
还有一种算数运算,即两个指针之间做减法的运算,其结果也是会根据指针的类型进行调整,计算结果并不代表两个地址之间的字节数,而是单元个数。假设存在两个指针p和q,下面这些计算结果也是不一样的:

指针类型p-q的结果
UINT *8p和q之间地址的差值
UINT *16p和q之间地址的差值/2
UINT *32p和q之间地址的差值/4
UINT *64p和q之间地址的差值/8

那么这里更加清楚的看到,指针是指向数据单元的,并不能简单的说其指向一个数据/内容。数据单元的维度是这个指针关键的一个特性。当然还有一个容易理解,容易被初学者接受的方式:假设地址p和地址q分别指向数组里的第x+1个元素和第x个元素,而且p和q与数组的数据类型相同(数组a[]为float型,指针p和指针q也为float型),那么p-q的结果就是中间相隔的元素的个数,结果一定为1!

回溯指针的比较运算

学过汇编的朋友们都知道,比较运算cmp的本质是两个操作数a, b做减法,如果ZF/SF等标志,就可以确定a, b两个数谁大谁小;
因此,可以说指针的比较运算本质上是指针的减法运算,上述p-q的运算也分析过了,下面仅给出一个容易被忽略的指针和数组元素地址比较的case:
假设有数组UINT32 array[],其维度为3,使用UINT32 *pa对其元素进行索引:

#define NUM	3
UINT32 array[NUM];
UINT32 *pa;

那么下面这两种给数组元素赋值0的方式,其结果也是不同的:

//方式1
for(pa = &array[0]; pa < &array[NUM];)
	*pa++ = 0;
//方式2
for(pa = &array[NUM]; pa >= &array[0];)
	*--pa = 0;

各位可以看出方式1和方式2的差异在哪里吗?同样都是数组元素的遍历和赋值,区别在哪里呢?下面就根据执行过程和可能发生的问题做一个描述:
方式1从数组的第一个元素开始,清零,第二个元素,清零,第3个元素也相同,到pa自加第四遍的时候,进行指针的比较运算,会发现pa = &array[3],那么停止清零操作;
方式2从数组的最后一个元素(第3个元素)开始,清零,再第2个元素,第1个元素,到pa自减第4遍的时候,此时的pa已经小于array[0]的地址了,这个时候就有可能发生指针跑飞的现象!
方式2正确的改发是这样,让pa = &array[0]的时候就终止:

//方式2
for(pa = &array[NUM]; pa > &array[0];)
	*--pa = 0;

再看开篇问题

明确了指针的算数运算和比较运算之后,再回过头来看看开篇提到的栈上数据访问的问题。

addressdata
0xFFC2_00000x00
0xFFC1_FFFF0xFF
0xFFC1_FFFE0xFE
0xFFC1_FFFD0xFD
0xFFC1_FFF50xF5
0xFFC1_FFF40xF4
0xFFC1_FFF00xF0
0xFFC1_FFE00xE0
0xFFC1_FFD00xD0

pBStack的指针如果是UINT32 *类型,从0xFFC2_0000 stack bottom开始,通过减法只能访问到0xFFC1_FFFC/0xFFC1_FFF8/0xFFC1_FFF4/0xFFC1_FFF0这样地址的数据,因为这里每4个字节组成一个数据单元,每一个指针指向这4个字节的数据单元,可以称之为32Bit对齐。

如果要想访问到0xFFC1_FFF5为起始地址的单元,又不该栈的栈底地址,那么pBStack的类型就得声明为UINT8 *类型;

如果想配合sizeof()对stack中的数据访问,那我们也是统一用UINT8 *类型,就可以轻松避免指针算数运算的陷阱了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cerman

你的鼓励是探索和创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值