说在前面的话
不得不说,看了太多的人在各种地方讨论指针……越发看下去,越发觉得简单的事情被
搞那么复杂,真是够了,求求你们,放开那个变量,让我来!
以下讨论均以ARM环境下使用C语言进行嵌入式软件开发为背景。感谢网友的批评指正。
1、从变量的三要素开始谈起
为了把复杂的事情说简单,我们抛开指针先从变量谈起。(好吧,不知道这个笑话是
不是够冷)一个变量(Variable),或者顺便兼容下面向对象(OO)的概念,我们统一
称为对象(Object),除了保存于其中的内容以外,只有三个要素:
1)由一定宽度无符号整数(Unsigned Integer)所表示的地址“数值”(Address Value)
2)对象的大小(Size)
3)可对该对象适用的“方法”(Method)和“运算”(Operation)
其中,我们习惯于把后两者合并在一起称之为,变量的"类型"。
> 地址数值(Address Value)
地址的数值是一个无符号整数,其位宽由CPU的地址总线宽度所决定。话虽如此,其实
主要还是编译器在权衡了“用户编写代码的便利性”以及“生成机器码的效率”后为我们提供的
解决方案:例如,针对8位机,编译器普遍以等效为uint16_t的整数来保存地址信息;针对
16位机和32位机,编译器则普遍选择uint32_t的整数来保存地址信息;针对64位机,编译
器则可能会提供两种指针类型,分别用来对应uint32_t的4G地址空间和由uint64_t所代表的
恐怖地址空间……
提问,8086有20根地址线,请问用哪种整形来表示其地址呢?(uint16_t、uint32_t
还是uint20_t)——由于uint20_t并不存在,也并不适合CPU进行地址运算,所以统一用
uint32_t来表示最为方便。
总而言之,
地址的数值是一个无符号整数。知道这个有什么用呢?我们待一会再说。这
里我们需要强调一句废话:地址的数值既然是整数,那么它就可以用另外的变量(类型合适
的整形变量或者指针变量)进行保存——任何指针变量,其本质,首先是一个无符号整形变
量。任何指针常量,其本质首先是一个无符号整数。
请一定要记住(重要的事情说三遍):
变量的三要素中,仅有
地址值有可能会
占用物理存储空间。
变量的三要素中,仅有
地址值有可能会
占用物理存储空间。
变量的三要素中,仅有
地址值有可能会
占用物理存储空间。
> 大小(Size)
如果仅从变量的大小来看整个计算机世界,就好像一副彩色图片被二值化了,到处是
Memory Block,他们的尺寸通常是1个字节、2个字节、4个字节、8个字节、16个字节或者由
他们组合而成的长度各异Block。这些Block通常被编译器在代码生成的时候对其到地址的宽度
上,比如地址宽度是32bit的,就对齐到4字节,地址宽度是16bit的,就对齐到2字节……
如果你习惯于使用汇编语言来进行开发,你一定能体会我所描述的这种感觉。这些你统统
都可以忘记,但有一点绝对要记住(重要的事情说三遍):
变量的三要素中,
大小值从
不会额外占用物理存储空间。
变量的三要素中,
大小值从
不会额外占用物理存储空间。
变量的三要素中,
大小值从
不会额外占用物理存储空间。
注意:地址的大小信息描述的是这个变量占用几个字节,这里说大小信息并不占用物理存储器
空间,并不是说,变量中保存的内容不占用存储器空间。请注意区别。
C语言中,可以用sizeof( )来获取一个变量的大小。前面我们说过,指针首先是一个整形变
量,那么容易知道:
-
- uint8_t *pchObj;
- uint16_t *phwObj;
- uint32_t *pwObj;
-
复制代码
sizeof(pchObj) 、sizeof(phwObj)、sizeof(pwObj)以及sizeof任意其它指针的结果都是一样的,
都是当前系统保存地址数值的整形变量的宽度。对32位机来说,这个数值就是4——因为,
sizeof( ) 求的是括号内变量的宽度,而指针变量首先是一个整形变量!同一CPU中同一寻址能力
的指针,其宽度是一样一样一样的!
> 适用的方法(Method)和运算(Operation)
对面向对象中的对象来说,方法就是该对象类中描述的各种成员函数(Method);
对数据结构中的各类抽象数据类型(ADT,Abstract Data Type)来说,就是各类针对该数
据类型的操作函数,比如链表的添加(Add)、插入(Insert)、删除(Delete)、和查找
(Search)操作;比如队列对象的入队(enqueue)、出队(Dequeue)函数;比如栈对象的
入栈(PUSH)、出栈(POP)等等……
对普通数值类的变量来说,就是所适用的各类运算,比如针对 int的四则运算(+、-、*、
/、>、<、==、!=...)。你不能对float型的数据进行移位操作,为什么呢?因为不同的类型拥
有不同的适用方法和运算。
也许你已经猜到了,类型所适用的方法和运算也不会占用物理存储空间。由于变量的“大小
信息”和“适用的方法和运算信息”统称为“类型(Type)信息”,我们可以简化为:
变量的三要素中,类型信息
从不会额外占用物理存储空间。
变量的三要素中,类型信息
从不会额外占用物理存储空间。
变量的三要素中,类型信息
从不会额外占用物理存储空间。
2、化繁为简的威力
前面说了那么多,实际上可以简化为下面的等式:
Variable = Address Value + Type Info
变量 = 地址数值 + 类型信息
其中,地址数值的保存、表达和运算是(有可能)实实在在需要占用物理存储器空间的
(RAM和ROM);而类型信息则是编译器专用的——仅仅在编译时刻会用到,用来为编译器语
法检测和生成代码提供信息的——话句话说,你只需要知道,
类型信息是一个逻辑上的信息,
是虚的,在最终生成的程序中并不占用任何存储器空间。你也可以理解为,
类型信息最终以程
序行为的方式体现在代码中,而并不占用任何额外的数据存储器空间。
既然知道了变量的本质,我们就可以随心所欲了,比如,我们可以随意创建一个全局变量:
-
- #define s_wMyVariable (* (( uint32_t *) 0x12345678))
-
复制代码
s_wMyVariable是一个 uint32_t类型的全局变量,它的地址是0x12345678。它和我们通过
普通方式生成的全局变量使用起来没有任何区别——当然,它是个黑户,简单说就是它所占用的
空间是非法的,无证的,在编译器的户口本看来,这块空地上什么都没有,因此它仍然会将
0x12345678开始的4个字节用作其它目的。
一方面,是不是突然觉得手上拥有了神一般的权利?其实,这种方法非常常用,MCU的寄存
器就是这么定义的,例如:
-
- #define CONTROL (*(volatile uint32_t *) CONTROL_BASE_ADDR)
-
复制代码
我们可以将上述定义全局变量的方法提炼成所谓的全局变量公式:
-
- #define <全局变量的名称> ( *( <全局变量的类型> * ) <全局变量的地址> )
-
复制代码
甚至,我们干脆定义一个宏来替我们批量生产全局变量:
-
- #define __VAR( __TYPE, __ADDR ) ( *( __TYPE * ) (__ADDR) )
-
复制代码
使用起来也很方便,例如:
-
- __VAR( float, 0x20004000 ) = 3.1415926;
-
复制代码
总结来说
:只要给我一个整数,我就可以把它变成任何类型的全局变量!你可以的!我看
好你哦。
3、万能类型转换
只要你牢记了那句话:给我一个整数,我就能翘起地球,那么我们就可以用它玩出更好玩
的东西。
首先,整数从何而来呢?除了前面的直接使用常数以外,当然还可以从整形变量中来,例如,
前面的例子可以简单的改写成:
-
- uint32_t wTemp = 0x20004000;
- __VAR( float, wTemp ) = 3.1415926;
-
复制代码
毫无压力!整数还可以从指针中来,例如:
-
- //!我们定义一个全局变量 wDemo,其地址是0x20004000
- #define wDemo (*(uint32_t *) 0x20004000 )
- uint32_t *pwSrc = &wDemo; //!< 获取wDemo的地址
-
- //!< 获取指针保存的地址数值,并用普通整形变量保存下来
- uint32_t wTemp = (uint32_t) pwSrc;
-
- __VAR( float, wTemp ) = 3.1415926;
-
复制代码
是不是觉得wTemp有点多余?因此我们可以直接写成:
-
- //!我们定义一个全局变量 wDemo,其地址是0x20004000
- #define wDemo (*(uint32_t *) 0x20004000 )
- uint32_t *pwSrc = &wDemo; //!< 获取wDemo的地址
-
- __VAR( float, (uint32_t) pwSrc ) = 3.1415926;
-
复制代码
是不是pwSrc也多余了?好,我们继续来:
-
- //!我们定义一个全局变量 wDemo,其地址是0x20004000
- #define wDemo (*(uint32_t *) 0x20004000 )
- __VAR( float, (uint32_t) &wDemo ) = 3.1415926;
-
复制代码
当然,如果这个时候你说直接填 0x20004000 不就行了,要么你已经懂了,要么你还糊涂着,
仔细想想:
> 如果wDemo是任意由编译器生成的对象(变量),意味着什么呢?(前面说过,作为全
局变量,我们土法制造的和compiler原装的用起来没有任何区别)
> 如果我们有任意的指针,我们需要对指针指向的类型进行转换(转换后才好操作),应该
怎么办?
接下来,我们很容易根据前面的讨论,得出第二个万能公式,可以将任意变量(或地址)转
换成我们想要的类型:
-
- #define CONVERT( __ADDR, __TYPE ) __VAR( (__TYPE), (uint32_t) (__ADDR) )
-
复制代码
例如,我们可以直接将字节数组中某一段内容截取出来,当做某种类型的变量来访问:
-
-
- //! 某数据帧解析函数
- void command_handler( uint8_t *pchStream, uint16_t hwLength )
- {
- // offset 0, uint16_t
- uint16_t hwID = CONVERT( pchStream, uint16_t);
-
- // offset 4, float
- float fReceivedValue = CONVERT( &pchStream[ 4 ], float ) ;
- ...
- }
-
复制代码
3、请忘记指针
如果你是一个指针苦手,那么请忘记之前所学的一切。记住一句话:
指针只是一个用法
怪异的整形变量,专门用来保存变量的地址数值。指针的类型都是用来欺骗编译器的,我是
聪明的人类,我操纵类型,我不是愚蠢的编译器。
推论:因为指针变量的本质是整形变量,所以指向指针的指针,只不过是一个指向普通
整形变量的普通指针,因此指向指针的指针并不存在——
世界上只存在普通指针
——世界上只存在用法怪异的整形变量,专门用来保存目标变量的地址数值。
推论:
世界上并不存在指向指针的指针的指针的指针……
给我一个整数,我自己造自己的变量。
指针的数值运算太坑?转换成整数,加减乘除,随便整。
哈哈哈哈哈哈……
4、小结
地址:所谓地址就是一个整形的数值(常数)。地址不包含任何类型信息
指针:指针分为指针常量和指针变量,单独说指针的时候,通常指指针常量。其中:
指针常量 =
地址数值(常数)+
类型信息
指针变量 =
整形变量 +
类型信息
变量 = (*
指针)
指针 = &
变量
类型信息可以通过强制类型转换来实现,也就是大家熟悉的 (
) 用法。
地址数值的改变,则统一转化为普通整数以后再说。
指针常量 =
整数常量 +
类型信息
也就是:
指针常量 = (<
类型信息> *)
整数 常量
反过来也成立:
整数常数 =
指针常量 -
类型信息
也就是:
整数常数 = (unsigned int)
指针常量 //! 仅对32位机使用unsigned int
同理,可以获得整形变量和指针之间的转换关系,这里就不一一列举了。
怎么样,事情是不是变得简单了?哪有什么指针,哪有那么多麻烦事情?统统都是整数。
下回我们将一起来捅一个马蜂窝。哈哈哈哈哈
说在后面的话:
其实,每次看到一群人热热闹闹的谈论指针,我心里真实的想法是:这么简单的事
情被你们搞这么复杂,我要让你们见识见识,什么叫把简单的事情搞复杂,把复杂的事
情搞成课题,把课题搞成疑难杂症,把疑难杂症搞成科幻片……哈哈哈哈哈哈哈
为了您的已经吃下去的食物,请自动无视以上内容。
______________未完待续______________
如果你喜欢我的思维,除了在阿莫论坛关注我,您也可以订阅公众号
裸机思维
所有内容原创,
严禁任何形式的转载。