c语言中cnthe普通变量,不得不说,关于 *(unsigned long *) 和 (unsigned long)

@辛昕:

然而我很悲伤地再次郑重声明:我木有错!具体请看我回复 水果君 那难得一见的长回帖的回帖!!205687023_1_20201028091322962_wm这事情居然还可以从遥远的 2月2号 刚下班放假那天开始。那天,水群里,水果君弄了个帖子,说让我去回,我看了一下,看到一个挺新鲜的东西,觉得有点意思,但看到下面的讨论我就觉得很晕。当时稀里糊涂在群里和他们胡说八道了一番,废了很多劲才发现问题的关键。

帖子很短,基本都是代码,我贴过来........void GPIO_DeInit(GPIO_TypeDef* GPIOx)

{

/* Check the parameters */

assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

switch (*(uint32_t*)&GPIOx)

{

case GPIOA_BASE:

RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, ENABLE);

RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, DISABLE);

break;.....

//不明白switch行中GPIOx为什么要取址,GPIOx本来不就是地址么?

#define GPIOA    ((GPIO_TypeDef*)GPIOA_BASE)

//求解释

对于水果君和我,尽管我们的意见不一致,但其实可以说,我和他,都知道是怎么回事。只不过,他的逻辑是,反正是一回事,能写多简单是多简单,而我的观点是:不要为了偷懒少敲那几个字母,让别人误会。尽管我们都很清楚他其实是怎么回事。没想到这事余孽未消,刚刚突然水群里又来一个类似的问题,这次问得更加没头没脑,直接把我这容易犯懵的大脑弄糊涂了。绕了一圈睡了一觉后,我想了想,决定好好写个帖子,好好说说这事情。

首先,从 GPIOx 说起这是ST库里的经典映射手法。具体就是和PC上的程序不一样,在STM32上,可以通过*(int *)0x08001000 = 34;在地址为 0x80010000上写入 34 这个内容。——这里不考虑什么 FLASH RAM之类的问题。所以,ST库映射寄存器的典型手法就是比如说 GPIOA的寄存器地址,如果是从 0x20001000开始存放什么 ODR IDR之类的。因为 结构体成员在内存上也是按序排放的,所以,它就把ODR IDR等等 寄存器 按顺序 定义成 GPIO 这个结构体。形如 typedef GPIO{    odr;    idr;};这一部分具体可以去看 stm32fxxx.h,我就不多说了。最后,GPIOA GPIOB GPIOC都会有一个 GPIOA_Base GPIOB_Base这个基地址,指的就是 每个port端口 寄存器的 起始地址。ABCDEFGI口各自按顺序排好。所以只要找到头,再借助这个结构体,就可以直接通过GPIOA->ODR这样的写法非常简单直观的 寻知道 GPIOA的ODR地址。非常形象,非常生动。而且很简洁,完全的利用了C语法本身的特性。是以,从我个人的角度看,这是一个非常不错的 映射手法。这基本也是那天晚上我语无伦次 发语音说的重点。

现在,先来回答原来那个帖子的问题。*(uint32_t*)&GPIOx这句话到底是什么意思?其实问题还是在上一个帖子里提到的 GPIOx的定义里#define GPIOA    ((GPIO_TypeDef*)GPIOA_BASE)其实我为什么第一反应会觉得这个东西写的挺新鲜,因为以前我曾小小纠结过如何通过一个函数,让函数自己区分 GPIOA GPIOB这个问题。而这里提供了一个 非常直接简单的方法:*(uint32_t*)&GPIOx对这种较复杂的表达式或者宏,解决的思路很简单,就是一步一步展开。但这个过于简单,而且这个话题也太口水了,我就直接带过去不罗嗦了,罗嗦了你们还以为是我无知大惊小怪......(多怨啊我,我只是一个喜欢 详细解释的好版主)GPIOx 是传递进来的形参,它的可能值就是 GPIOA GPIOB之类的那也就是((GPIO_TypeDef*)GPIOA_BASE)GPIOA_BASE是一个数值,代表的是 GPIOA的寄存器的起始地址。*(uint32_t*)&GPIOx这个操作,等于,把 GPIOA_BASE 这个最初宏定义的数值,就是说,这是一个常数。所以这个时候,就可以很方便的使用 switch-case结构了因为case后面跟的只能是常数,而不能是变量或者其他任何数值。这就是这个问题的所有答案

水果君在那个帖子里认为,其实这个地方是多此一举,可以直接写成(uint32)GPIOx说实在的,这个,和 那个显得很麻烦的写法*(uint32_t*)&GPIOx效果确实是一样的。只是,我强调 后面这种写法,我的理由在于:可读性。看到前者,你不会联想到 GPIOx是一个地址,而看到后者,稍微有点经验的C程序员都马上会领悟到这一点。这就很重要。为什么,因为,在类似的环境下,我就会搞懵。比如说,最开始主楼贴 的那个图。那是 人民币君发的。我一开始因为直接联想到 这个放假前的讨论,因此我想都没想,就直接说,这两个效果是一样的。然而,果真是一样吗?呵呵,那还真不是。比如说205687023_2_2020102809132356_wm写到这里,我就懵了,看出问题了没?一个是那个数其实是地址值,要去操作那个地址上的内容另一个,压根就只是一个常数。这两个操作的出来的结果和影响完全不一样。虽然这可以解释为看走眼,糊涂,当然你也可以认为我经常犯懵,但是我个人的经验是——不要盲目自信你不会犯懵。因为一旦犯懵的时候,你可能要付出两三天或者两三个加班的夜晚去解决问题。试问值得不值得?而这也是我和水果君争论的核心所在:虽然我的写法复杂一点,但是,我觉得我可以很明确的告诉别人这就是一个地址。正如我发帖子或者在q群里讨论,总是不厌其烦,说的大白菜一样。而不是假设你和我一样知道一些东西。这样虽然我可以省事很多,少说很多话,打少很多字,但是却极有可能产生沟通的误解,因为双方很可能不是建立在相同的已知上。这也是我为什么非常厌恶和吐槽一些半导体厂商提供的文档的理由。因为他们说的根本不是人话,不是站在开发者使用者的角度去说事。似乎他们很希望和你分享他们设计芯片和外设原理一样,却不知道这样把我们搞的是一塌糊涂。我们只不过想学会如何配置和使用,它们却罗罗嗦嗦说另一个方面的问题。好了,就此结束。

我差点被你绕晕了。

(uint32) GPIOx  还是写  *(uint32_t*)&GPIOx我倾向于前者,或者C++写 static_cast(GPIOx)因为 GPIOx 已经是一个地址了,地址值的比较么,直接当作32位整型(这里隐含32-bit平台前提)比较就是了。写成后面那种形式,这是何苦呢,呃,还要取一个地址……如果参数是寄存器传递的话,地址是哪儿呢?就好比说有有符号的数要转换成无符号的

int x=-1;unsigned int y;是写成 y=x; 呢,还是 y=(unsigned int)x; 呢,还是 y=*(unsigned int*)&x; 呢?除了第一个写法会有编译警告,都是一样的效果。

感谢版主非常详细的解释了这个问题,以前看32的时候还真没有认真注意这个细节的地方,有时候人真的会“自以为是”,很多东西都觉得懂了,其实还是似懂非懂,在往里问一点就搞不清楚。也确实说明我们应该认真细心的学,脚踏实地一个一个搞清楚才是正确的学习方法。

205687023_3_20201028091323165

我来对号入座了,我就是文中提到的水果君。首先来讨论一个我认为很有意思的问题,就是这两个强制类型转换:*(uint32_t*)&AAA 是否等价于 (uint32_t)AAA ?(假设我们不知道AAA的类型,变量还是常量)为了便于讨论,我们假设变量uint32_t X=*(uint32_t*)&AAA, Y=(uint32_t)AAA;乍一看,好像是有点等价的意思,但是仔细想想,又不是那么回事,这还取决于AAA的类型。(1).现在假设AAA是2字节short型=0x1234;那么X的结果是强制从AAA的地址中取走4字节,其中2字节未知:X=0xXXXX1234 (小端情况下)Y的结果就比较确定,编译器帮他把高位填0, Y=0x00001234;(2).假设AAA是64位long long类型=0x1234;前面的0我就不写了。那么X还是取走4个字节,根据大小端而异,可能是高4字节,也可能是低四字节。Y只是简单的舍弃了高四字节,结果比较确定。(3). 假设AAA是个数组,这个情况比较特殊,数组名本身就是数组的首地址,取地址后还是相同的值:因此X会取出数组中的元素而Y却还是一个地址值。(4). 一个不靠谱的假设AAA是个结构体那么X可以取到结构体的成员而Y的写法就直接报错了,不允许强制类型转换。(5). AAA是uint32_t类型,那么没什么问题,X与Y等价。由此可见:X写法确实是无条件强制转换,基本是无所不能转,但是如果类型不匹配就很容出错了。Y写法是有限制的编译器参与的半智能转换,因为编译器知道源类型与目标类型,会帮忙参与转换,或者类型转换不太靠谱的话,直接报错。由此可以得出结论,在使用强制类型转换的时候,必须具有可行性,同时也必须清楚转换后的结果,也就是说,程序员(写程序的人)必须清清楚楚地知道源类型和目标类型到底是什么,否则还是去读书深造的好。现在我来说说为什么觉得uint32_t X=*(uint32_t*)&GPIOx有脱裤子放屁的嫌疑(从阅读程序的角度来说)。首先有个变量GPIOx,是个指针,也就是个地址1,此地址上面存放的类型是GPIO_TypeDef。然后对这个地址1取了个地址2,那么这个地址2的类型是GPIO_TypeDef **现在对地址2进行强制类型转换,转成uint32_t*,也就是间接说明,不再把地址1当做地址(指针)看待,而是作为一个uint32_t类型。然后在地址2中的uint32_t数据取出来,完毕。牛逼的程序员也看出来了,这就是拐外抹角的把GPIOx转成uint32_t类型。一般刚入门的程序员看了会不会很懵?好,说的有点乱,我们按教主的思路重新捋一下:我们假设不知道GPIOx到底是个什么东西,就当做是一个不知道类型的普通变量。引用:“—————水果君在那个帖子里认为,其实这个地方是多此一举,可以直接写成(uint32)GPIOx说实在的,这个,和 那个显得很麻烦的写法*(uint32_t*)&GPIOx效果确实是一样的。只是,我强调 后面这种写法,我的理由在于:可读性。看到前者,你不会联想到 GPIOx是一个地址,而看到后者,稍微有点经验的C程序员都马上会领悟到这一点。—————引用结束”看看我对脱裤子的理解:首先一个变量GPIOx,不知道其类型然后对此变量取了个地址,此地址类型也未知。然后对这个地址进行强制转换成uint32_t类型的一个地址(此处影射GPIOx是个uint32_t类型)最后,从这个地址中取出了一个uint32_t类型的变量,完成了最终这个语句的使命。这样理解,也根本看不出GPIOx有地址的意思(只是明确了变量的地址是个具体类型的地址)。只是拐了个弯,把GPIOx强制转成uint32_t类型而已。然而,把一个地址(指针)转成一个整形数,就很常见不过了。uint32_t Y=(uint32_t)GPIOx。由此,可以看到两个转换的最终区别:脱裤子那种,是强制转换的变量地址的类型,间接对数据类型进行转换,而直接转换就是直接对变量进行转换。

@辛昕:

看到你回这么长的贴,我也是挺感动的,然而很不好意思的告诉你。虽然你没错,但我还是要比你更正确......很简单,看好了。在32位机器下,你是对的,你还是更简洁的。然而如果这种写法,在16位机或者64位机 等非32位机下就会出错。why?很简单,,非32位机的 地址非4字节,而是 16位机的2字节,64位机的8字节。这种情况下,对应的指针字长也就成了 2字节 8字节。于是结果已经很明显。你每次都uint32去强转地址,问题是,你强转的是一个地址.......那也就是说。对16位机,你多转了后面未知的2字节,对64位机,你少转了后面需要的4字节。所以,必然是错的。而原来那种看起来复杂的写法呢?木有错,为神马?因为,,uint32_t * 也是一个指针,或者地址(指针或地址随你叫吧)因为在同一机器下,任何类型指针的字长都是一样的。所以这种情况下,我读到的地址值永远不会少或者多。这个问题意味着。在你的写法里,你需要去假设指针字长,比如uint32,但这永远只能对一种机器字长适应。而那个复杂的写法,则无此需求,不需要作任何假设,也就不会受限于任何机器字长的限制。事实上,我写成*(uint8_t *)*(uint16_t *).....都木有任何关系

(uint32) GPIOx  还是  *(uint32_t*)&GPIOx 如果GPIOx是形参,无论哪种写法,得出的汇编都一样,都是直接取R0,如果GPIOx 是全局变量,汇编结果也是一样,都是取GPIOx变量的内容如果GPIOx变量存放到0x10001000,直接取0x10001000里面的内容编译器非常聪明,认为对指针变量X取地址A再取A里面的内容和直接取X里面的内容是一样的!

虽然效果一样,但是明显给阅读程序带来了困难,一般人看到这个表达式都要思考一下才敢下决定。如我在7楼所说,如果换一种类型,例如:(uint16_t) GPIOx  和  *(uint16_t*)&GPIOx 未必一样,或许在小端机器上同样被优化,但是大端机器上肯定就不同了。

很显然,@辛昕(别名教主)对我的反驳是很无力的在@freebsder叔叔的蛊惑下,一直在找我的漏洞,我也觉得代码这东西没有绝对的对与错,虽然那种写法看起来很复杂并且高大上,一个语句展现了好几个C语言的知识点,而且结果正确,一点毛病没有,只是让人读起来需要有个停顿思考的时间。C语言在程序移植这里确实存在许多诟病,在不同硬件平台上,数据类型的长度并不统一。例如通常int在16位机为2字节,32位机为4字节,指针也一样,根据机型有2字节,4字节或者8字节的长度,记得还有3字节的……因为硬件平台种类实在是太多了,五花八门。为了应付这类问题,C99标准出台了更具体的类型,如int16_t,uint32_t这样的具体长度类型,使程序在不同硬件平台更容易移植。但是……。遗憾的是,这些类型中没有指针,我觉得因为指针也没法具体化。因此,教主提出了他的问题:在不同平台上如何用整型来表示指针?其实这问题的根源完全来自于switch语句,因为在switch中只能使用整型数据类型,不可以是指针,如果非要去比较指针,那么只能转成整型。因此,如果把指针转为整型成了讨论的重点,但是很明显,教主的结论是错误的,他的写法并不能实现他要的结果。大家都应该知道,指针是有类型的,解引用的时候会得到相应的类型:*(uint32_t*)xxx  结果将是uint32_t*(int16_t*)xxx 结果就是int16_t根本得不到他所想要的与平台相关的数据类型。因此在C语言中,想要得到指针所对应的整型类型,只能通过手动指定,例如微软就是这么干的#ifdef _WIN64    typedef __int64          intptr_t;#else    typedef int              intptr_t;#endif这样intptr_t就可以确保能保存指针类型。但是可惜这只是某些厂家这么干,ST的库中并没有这样的类型,否则这事就好办了(当然了,ST目前可能也没考虑推出64位的单片机)我不知道*(uint32_t*)&GPIOx这样的代码是否是出自ST的标准库,我没有去考证,如果真这样写的话他们自己可能也看着别扭,所以我看到st的某个版本库里面看到的是直接比较指针,当然了,就不能使用switch语句了,而是if语句,像这样:if (GPIOx == GPIOA) xxx_statement 。反正我觉得这样写是最直观最易读的了,给他们点个赞!c语言的争议太多了,就像#define与typedef之争,#define与const之争,程序员的理论就是运行结果没错那就都不是大问题,不说了

忘了总结了:虽然看似老婆都是别人家的好,但是代码并不是别人写的就一定好,即使你是一个新手,也不要随便拿来主义随便借鉴,包括我写的代码,很多都比较烂,写的并不严谨,只是为了应付能用就行(能保证在我的应用环境中不出错就行),因为把代码写完美确实是一件很不容易的事,很费时间和精力。

有以下代码:char xx;

void GPIO_DeInit1(GPIO4_TypeDef* GPIOx) {

xx = (char)GPIOx;

}

void GPIO_DeInit2(GPIO4_TypeDef* GPIOx) {

xx = *(char*)&GPIOx;

}

在32位机测试结果如下:(ARMCC编译器,优化级别-O0,-O1,-O2,-O3都一样)xx = (char)GPIOx;

0x10000558 4919      LDR      r1,[pc,#100]  ; @0x100005C0

0x1000055A 7008      STRB     r0,[r1,#0x00]

前者虽然效果非常好,但编译器给出警告: warning:  #767-D: conversion from pointer to smaller integerxx = *(char*)&GPIOx;

0x1000055E B501      PUSH     {r0,lr}

0x10000560 4668      MOV      r0,sp

0x10000562 4917      LDR      r1,[pc,#92]  ; @0x100005C0

0x10000564 7800      LDRB     r0,[r0,#0x00]

0x10000566 7008      STRB     r0,[r1,#0x00]

0x10000568 BD08      POP      {r3,pc}

最终结果还是一样当把char修改为int型,编译器才能对这句xx = *(char*)&GPIOx优化xx = (int)GPIOx;

0x10000516 4919      LDR      r1,[pc,#100]  ; @0x1000057C

0x10000518 6048      STR      r0,[r1,#0x04]xx = *(int*)&GPIOx;

0x1000051C 4917      LDR      r1,[pc,#92]  ; @0x1000057C

0x1000051E 6048      STR      r0,[r1,#0x04]

对于这个编译器警告,我猜想,编译器认为不应该把指针变量(即内存地址)强制转换为小于这个地址宽度的值,地址就应该当地址用,不要破坏地址通过你们的争论,让我对这两个写法产生了好奇,至于用那种,没有警告没有错误就行!

我们都不是研究编译器的,也没有在那个编译器手册中看到这方面的规定在32位机下 xx = *(long long*)&GPIOx; 同样给出警告  warning: C4487E: read from variable 'GPIOx' with offset out of bounds也许ARMCC默认了,只是是将指针变量转换为整型,就默认允许,否则就是非常规应用,就给出警告!

我原来其实对这个“去指针”运算没有想的很清楚,那会儿看了各位哥哥们的说法,自己参考了下别人的,在Codeblocks写了个小程序验证了下。205687023_4_20201028091323275_wm根据C语言语法switch()的括号里面必须是常量表达式。这样以来就让我更加理解了。(我的小程序后面的b = *(int *)p,其实就相当于“赋值”运算)。感谢各位哥哥啦。

再次总结一下昨天的测试结果,有以下代码,ARMCC编译器short x;

// p无论是什么类型的指针或是不是指针都不影响汇编代码

void f1(short* p) {

x = (short)p;   // 前者

}

void f2(short* p) {

x = *(short*)&p;  // 后者

}

前者当p为指针时只有在类型为int时编译器才不会警告 当 (short)p   (char)p  都会警告后者都没有警告,虽然汇编的最终结果一样,但如果非要这样用,且不能接受编译器警告,为了消除警告,恐怕要写成这样: (short)(int)p; 当p为形参时汇编结果非常高效:0x10000516 4919      LDR      r1,[pc,#100]  ; @0x1000057C

0x10000518 8048      STRH     r0,[r1,#0x02]

(short)p同样也是上面的汇编结果,只不过编译器警告!后者写法虽然复杂,但没有任何错误,对一个变量取地址,再通知编译器按新的方式读取里面的内容,无论这个变量是什么类型,是不是指针,得出的汇编代码也都一样!由于取地址是个伪操作,意思可以简化为,(编译器:请将这个内存地址的内容按新的方式对待,并读取来),只不过汇编代码显得有些复杂.可能是编译器优化得不够狠!前提条件是,编程者要知道这个地址里面的东西是什么!基于这个前提,再看前者写法,意思为(编译器,将这个变量的内容读出来,按新的内容对待,)于是,编译器发现变量是指针,里面的内容是地址,而你却明显不当地址用,直接警告你,后者写法则绕过了编译器的这个判断.论性能,我喜欢前者,但为了哄编译器,不得不写成这样(short)(int)p

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值