【随笔】关于取地址运算符&以及指针10要点

A_Zhao 2012-12-28 08:40:45
引用
至此,我想应该就是我没弄明白表达式这个概念。或者,是因为翻译的问题,产生的很多困惑。

这恐怕不是翻译的问题。如果在你所看的那本书里,出现了“取地址操作符即&,不能施加于表达式”这种说法的话,那么,这种说法是错误的。不过,考虑到这本书的特殊的背景,这种“错误”在某种程度上是可以被原谅的 —— 毕竟,如果那本书能将一切问题都讲细致的话,它就远远不能止于那个篇幅了。

首先,在排除其他意义的情况下,作为操作符的&,叫做“取地址操作符”(Address Operator)。然而,这种称呼,其实有相当多的弊端,比如,会令读者认为,由其得到的指针就是地址。而实际情况是,指针与地址是两个不同的范畴,无论如何都不能视同一致。所以,依照薛非大虾的思路,并且遵循„Zu den Sachen selbst!“的精神,我们不妨这样来描述操作符&:

它用来帮助我们获取一个指针,这个指针仅指向它所作用的操作数。

理解这句话,有这样几个要点:

(1)&是操作符。既然是操作符,就有操作数,即操作符所施加的对象。

(2)或许,对于初学者,准确地理解“操作数”这个词,是有些障碍的。因为用汉字“数”来指称这个对象,难免让人望文生义地用初等数学里的“数”或代数表达式来理解它。所以,我们有必要以更加确切的境况下的事实,揭示:这里的“操作数”究竟指的是什么?

(3)&的操作数,非但可以是表达式,而且必须是表达式。不过,这种表达式,并不是C语言里的任意一个表达式,而是受制于某种规则的表达式。

(4)现在,我们引入了“表达式”这个范畴。这个概念,是问题的核心,也是许多问题胶着难解的所在。可以这麽说,一门编程语言在表达式这个范畴上的实际含义,体现了该门语言的特质。显然,能解释清楚这类事情,并非易事。我们还是以„Zu den Sachen selbst!“的思路继续发掘。

(5)在C语言中,为了平滑而自然地接受它在“表达式”这件事情上的准确含义,我们还得事先引入“对象”这个概念(跟“面向对象编程”里的“对象”不同)。粗略地说,在C语言的视角下,一切存在于内存空间上的区域,皆是对象。这里的内存空间,一般专指主存储器上可以承载数据的空间分布。详细讨论“主存储器”,恐怕要花上几本书的篇幅,现在我们只抓住几个重点:

(i) 主存储器上用来承载数据的空间分布,以“地址”这种机制来把握。—— 我们理解“地址”,不应该仅仅将其理解为一个数值范畴,而是应该用它的作用(为什么要用到它)来理解。从硬件视角看,连接在CPU与主存储器之间的“血脉”分别有地址总线、数据总线与控制总线等。从运作逻辑接口来看,主存储器在CPU这端,以用来放置前者上的地址的寄存器与用来放置前者上的数据的寄存器来呈现。

(ii)“地址”这种机制,在实际运用中,具有“连续”的特质(这种固有禀性,是关于指针的一些关键运算的基本前提)。而同是用来承载数据的寄存器(专指CPU的某个组成部分,不指其他的寄存器),则没有这种特质。从这个角度上,我们可以将主存储器与寄存器很清晰地分别开来。所以,用存储类别register限定的被声明的对象,不能成为&的操作数。但是,请特别注意:前述规定的理由,并不是在于register跟寄存器有一一对应的联系!而是在于:由register限定的变量,有可能被编译器分配到寄存器。register这个限定符的真确含义是:可以用它,来提示编译器在为变量分配存储空间的时候,[color=blue]编译器允许将变量分配到寄存器,从而弃绝可能发生的与该变量有关的“地址”与“指针”机制。[/color]

(6)回到“对象”(即存在于内存空间上的区域)这件事情。在C语言中,如何读取与刷写对象上的数据?C语言采取的策略,是左值表达式。不难理解,只有左值表达式可以出现在赋值等号(赋值操作符)的左边,有如:
int a, b, c, d, e, *ptr;
/* 上述部分变量须初始化,此略 */

a = b;
ptr = &a;
ptr[c] = 123;
* ptr = 456 + d;
*(ptr + e + 1) = 789;
在这上面代码中的后面五行里,
a、ptr、ptr[c]、*ptr、*(ptr+e+1)
都是左值表达式。

我们需要注意:上面所说的“只有左值表达式可以出现在赋值等号的左边”,并不意味着“左值表达式只可以出现在赋值等号的左边”。比如,在上列第一行代码中,b其实也是一个左值表达式,但它可以出现在赋值等号的右边。通过这样的比对,我们不难理解:在C语言中,采用左值表达式这种策略,来handle该表达式所引用(这专指一种更为一般的机制,与C++等语言专属术语“引用”不同)的对象,从而实现对对象上的数据进行读取或刷写的企图。比如:在赋值表达式a=b中,b首先是一个左值表达式,C语言用它来读取这个表达式即b所引用的内存区域上的数值,然后将这个数值,刷写到左值表达式a所引用的内存区域上。此外,需要提到的是:由于a这个“东东”,程序令它向程序自己开放了某种在任意(事先无法预知)时刻或时机被刷写进任意数据内容的权限(但须是合法的),所以,我们也以“变量”来描摹a。

与之形成对比的是,那些无法帮助C语言施行这种策略与企图的表达式,一般称作右值表达式,比如上列代码中的 456 + d,它是一个右值表达式,因为它无法帮助C语言handle任何它自己所引用的内存区域(这种对象根本不存在)。

(7)&的操作数,仅能是一个左值表达式。我们可以从“取地址操作符”这个称呼上,理解这件事情:&操作符帮助我们获取一个地址,即用以定位内存空间上的区域的一种特殊数据,那么,当&无法完成这个任务的时候,比如其操作数是一个不具备被获取地址的特性的某种东东(如register所限定而声明出来的变量、一个右值表达式),那么这样的操作数就是非法的。

(8)对于&操作符帮助我们所获得的数据(即返回值),许多人认为是一个地址,但在真确而实际的语义中,我们不如单纯而朴素地认为:该返回值,应该是一个指向其操作数的指针,而不是一个所谓的“地址”。为什么这麽说呢?

我们之前提到了,C语言采用了利用左值表达式来handle该表达式所引用的对象,比如:表达式a的存在,是为了引用某块“属于(对应于)”a的内存区域。C语言程序的宗旨,当然就是读取或刷写这块内存区域上的数据。所以,C语言程序实现这个宗旨的层次细节,大抵是这麽一种关系:

表达式a ----> 一块对应于a 的内存区域(一个内存对象) ----> 这块内存区域上的数据(值)

C语言采用“语句(断言)”这样的策略,围绕着左值表达式施行一种当时性的对内存区域上的数据的读取或刷写。这构成了C语言在命令式语言上的基本特征。

犹如一个*简单的*左值表达式可以引用一个内存对象,指针也可以引用一个内存对象。(而地址,只是用以在连续地铺张开来的内存空间中,定位内存对象的一种机制。所以,这也可以说明:指针跟地址,并不应该视同一回事。)不过,指针相对于一个*简单的*左值表达式来说,其引用机制是间接的。所以,为了能利用指针实行一个间接的引用(企图等效于直接的引用),我们就必须借助于间接访问操作符Indirection Operator),即让初学者恐惧而忧烦的那个星号。

这种含有间接访问操作符的表达式,称作间接访问表达式,有如:
*ptr
一般我们可以认为:间接访问操作符(即星号)作用在一个指针上面,从而使得整个间接访问表达式犹如一个*简单的*左值表达式那样,去handle某个内存对象。

这里,我们可以再一次将指针与地址两下撇开。在指针被用来实现引用机制的过程中,至少我们从寻常的代码及其运行的直观中,丝毫觉察不到任何的地址这种特殊数据的形态。换句话说,地址只是指针机制的某种幕后支撑,而这种支撑策略,并非是惟一的。比如,技术性地,我们也可以选择Hash的key-value对来实现指针的引用机制。

至此,我们不难理解,间接访问表达式,也是左值表达式。
因此,诸如
&*ptr
&*&i
这样的表达式,都是完全合法的。

(9)&和*操作符的使用,尤其是配对使用,极容易给初学者带来“C语言的指针是‘脱裤子放屁’”的印象。为了破除这种误解,必须将C语言的其他特质纳入我们的视野。在这些特质中,最具有代表性的,就是C语言的函数策略。

我们观察并思考如下代码:

void foo(int x) {
x++;
}
我们的期望:以foo这个函数,为被传至其中的参数本身,施行自增操作。

但是,不论我们调用多少次函数foo(a),我们在调用完成之后,变量a不会有任何变化。这是为什么?原因出于C语言固有的函数策略。具体地说:当我们以变量a为函数foo的参数的时候,函数foo将变量a的数据值刷写到该函数治下的变量x,也就是说,函数foo内部的变量x只是拥有了作为调用该函数的参数即变量a的数据值的私有副本。换句话说,函数foo内部的变量x,只是被初始化了一个值,至于这个值是从哪里来的,站在变量x的角度,它自己是永远无法知道的。这犹如:
x = a ;
作为左值的x,只是被赋值以另一个左值a所能handle的内存区域上的数据,除此之外,有关左值a自己的其他一切信息,都不会传达给x。所以,在执行了上述语句之后,分别对左值x和a的任何更改性操作,都不会影响到对方。

除此之外,当函数完成任务之后,该函数治下的一切私有变量,都将消亡 —— 这是导致所谓的“内存泄露”现象的根本原因,后面我们会讲到。

那么,我们为了利用一个函数,完成对被传入该函数变量的本身的某种更改性操作,我们应当这麽做:

void bar(int *x) {
(*x)++;
}
我们调用函数bar的写法,也应当有所改变,应当这样:
bar(&a);
此时,被传入函数bar的参数,是一个指向变量a的指针。函数bar治下的变量x(这是一个指针变量)所拥有的,依然仅仅是前者的的一个私有副本。

函数bar在其内部,通过把间接访问(星号)操作符作用在这个私有副本上,handle到了变量a所对应的那块内存区域,从而,间接地在那块区域上刷写了新的数据。当然,变量a自已对此是一无所知的,只不过,以后当其他地方又“召唤”a的时候,a若自有记性,会心说:“唉?这个值跟原来的不一样了嘛,一定是哪个臭小子,在背地里拿了一个指向我的指针去间接地修改了这个值……”

(10)最后,我们考虑一种情形:

void qux(void){
int *ptr = malloc(sizeof(int)*15);
}
函数qux内部调用了函数malloc,后者负责在当前内存中的空闲空间中分配出一块由其参数所确定大小的存储空间(那么,这新分配出来的存储空间将不再是空闲的,直到有函数free来释放之),并将指向这块被分配出来存储空间的指针,赋值给变量ptr。

那么,只要在变量ptr生存期所覆盖的范围内,程序都可以通过ptr来handle这块存储空间。问题在于,变量ptr单单属于函数qux治下。一旦函数qux例程完结,变量ptr即消亡。此时,若程序里没有任何变量ptr的遗子,程序将永远无法再handle那块存储空间,甚至无法释放它。那块存储空间将一直处于被占用(非空闲)状态,且是完全是个孤岛。这就造成了存储空间的浪费,是为“内存泄露”。

以上,仅供参考,呵呵 —— :)

P.S.: 这种技术概念性比较强的文章,还是请薛非大虾审订一下才是妥当。一旦通过了薛非大虾的审核,楼主再和盘接受就没有什么问题了,呵呵 ……



...全文
249 4 打赏 收藏 转发到动态 举报
写回复
用AI写文章
4 条回复
切换为时间正序
请发表友善的回复…
发表回复
FrankHB1989 2012-12-29
  • 打赏
  • 举报
回复
引用 3 楼 A_Zhao 的回复:
引用 2 楼 FrankHB1989 的回复:LZ发错版了。 应该发“C”版的,对吧? P.S.:很高兴在这里见到您大虾,呵呵 ——:)
嗯。既然有独立的C和C++版,发到C版比较好。
A_Zhao 2012-12-28
  • 打赏
  • 举报
回复
引用 2 楼 FrankHB1989 的回复:
LZ发错版了。
应该发“C”版的,对吧? P.S.:很高兴在这里见到您大虾,呵呵 ——:)
FrankHB1989 2012-12-28
  • 打赏
  • 举报
回复
引用 1 楼 taodm 的回复:
简单问题复杂化了,而且在C++里要修正太多细节了。
LZ发错版了。 C++其实不用修正多少细节,基本和C一丘之貉,排除重载什么的罢了。倒是真要被深究,ISO C就有麻烦了(虽然可以绕过去)——比如变量是不是表达式之类。
taodm 2012-12-28
  • 打赏
  • 举报
回复
简单问题复杂化了,而且在C++里要修正太多细节了。

64,646

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧