4.1 认识一元&和一元*运算符
虽然一个变量就是存储器里的一个存储区,但是,只有当程序真正运行时,这个存储区才实打实地确定下来。在我们写程序的时候,还只是假装它已经存在,还要用语句来模拟那些运行时才会执行的操作。
因为写程序的时候变量还不存在,自然需要用标识符来代表它,因此,标识符就是变量的化身,代表着那个变量,就好比我们每个人都有一个名字。在下面的程序里,我们声明了一个变量,标识符m是那个变量的名字,代表那个变量;语句S1中的表达式m = 1是直接操作这个变量,为它塞一个数值。
/*************c0401.c************/ int main(void) { int m, w; m = 1; //S1 * & m = 2; //S2,等价于m = 2 w = ++ * & m; //S3,等价于++ m }
这种操作是很直接的,并不涉及变量的地址,因为你“正在操作变量本身”,就像你正在面对面地塞给朋友一个苹果,并不需要知道他住在哪里。标识符m可以看成那个朋友的名字,代表那位朋友本身,数值1可以看成苹果。那么,如果我想通过变量的地址来操作它,怎么办呢?
在C语言里有哼哈二将,一个是一元&运算符,另一个是一元*运算符,它们被称为一元运算符,是因为它们只需要一个右操作数。一元&运算符用于取得变量或者函数的地址,而一元*运算符则根据一个地址来得到那个变量或者函数本身。作为例子,上面那个程序就演示了如何得到一个变量的地址,以及如何通过地址得到一个变量本身。
以上,在main函数内声明了变量m和w。语句S1我们并不陌生,是将数值1保存到变量m。如图4-1所示,这是针对变量m本身,是非常直接的操作,就像面对面给别人一个苹果:“来,缪晓红同学,给你一个苹果”。
图4-1 直接操作变量和通过地址操作变量
一元*运算符和一元&运算符的优先级相同,且都高于赋值运算符,但它们是从右往左结合的,所以在语句S2中,表达式* & m = 2等价于(*(& m))= 2。表达式& m得到变量m的地址,然后,一元*运算符则通过地址得到该地址上的那个变量——实际上就是变量m。
“变量”不是用于描述表达式的术语,“左值”才是。指示或者说代表变量的表达式称为左值,既然表达式* & m代表一个变量,那么它就是一个左值;表达式* & m = 2是将数值2赋给这个左值。又因为这个左值实际上是代表变量m的,所以语句S2等价于m = 2。
如图4-1所示,通过地址来找到变量,或者换句话说,通过一元运算符*来得到一个左值的过程相对迂回了些。就像通过快递把苹果发给缪晓红,这需要通过地址进行。
既然表达式* & m是一个左值,代表一个变量,那么,它自然也可以作为前缀递增运算符的操作数,因为该运算符要求它的操作数必须是左值。
在语句S3中,表达式++ * & m将递增左值* & m所代表的那个变量的存储值,因为该左值实际上代表变量m,故变量m的存储值现在是3,该表达式等价于++ m。
为了加深理解,最好的办法是在调试器内观察语句的执行和变量值的变化。现在,我们将上述程序保存为源文件c0401.c并翻译为可执行文件,启动GDB,将断点设置在源程序的第6行。接着,用r命令运行程序到断点位置(以Windows平台为例):
D:\exampls>gcc c0401.c -o c0401.exe -g D:\exampls>gdb c0401.exe -silent Reading symbols from c0401.exe...done. (gdb)l 1 int main(void) 2 { 3 int m, w; 4 5 m = 1; 6 * & m = 2; 7 w = ++ * & m; 8 } (gdb)b 6 Breakpoint 1 at 0x401564: file c0401.c, line 6. (gdb)r Starting program: D:\exampls\c0401.exe [New Thread 12564.0x2d10] [New Thread 12564.0xb34] Thread 1 hit Breakpoint 1, main()at c0401.c:6 6 * & m = 2;
现在,程序中的第5行已经执行,变量m被赋值为1, GDB显示下一次将要执行第6行。既然表达式&m用于取得变量m的地址,那为何不看看它到底是啥呢:
(gdb)p &m
$1 =(int *)0x61fe48
(gdb)p *&m
$2 = 1
以上,我们用命令p &m来显示这个地址,显示的内容是0x61fe48,这个十六进制的数字就是变量m的内存地址。既然表达式*&m是一个左值,代表一个变量(在这里实际上是变量m),我们也用命令p *&m显示了该变量的值,其值为1,是我们刚才赋的值。
接下来,我们用n命令执行第6行,其功能是把数值2保存到左值*&m所代表的变量里。然后,用命令p {m, *&m}来显示变量m的值,以及左值*&m所代表的变量的值:
(gdb)n 7 w = ++ * & m; (gdb)p {m, *&m} $3 = {2, 2}
如上所示,我们知道,左值*&m所代表的变量实际上就是变量m,而变量m的当前值是2,所以显示的结果应该是两个2, GDB显示的结果证实了我们的猜测。
下面继续用n命令往下执行第7行,这将求值表达式++ * & m并把结果保存到变量w。执行后GDB停留在第8行的右花括号处。此时,我们再用命令p {m, *&m, w}来显示变量m、表达式*&m所代表的变量,以及变量w的值:
(gdb)n 8 } (gdb)p {m, *&m, w} $4 = {3, 3, 3}
因为表达式++ * & m递增左值*&m所代表的那个变量的存储值,故如上所示,变量m的值和左值*&m所代表的那个变量的值都是3。运算符++的结果是其操作数递增之后的值,所以表达式++ * & m的值是3。这个结果赋给了变量w,所以变量w的值也是3。
最后,我们再用c命令执行程序直至它正常退出,然后用q命令退出GDB,本次调试过程结束:
(gdb)c Continuing. [Inferior 1(process 12564)exited normally] (gdb)q D:\exampls>
在第1章里引入“左值”这个概念的时候,很多人可能还不理解它的必要性。现在应该很清楚了,表达式*&m代表一个变量,但我们不能说“变量*&m”,这通常是不恰当的。
练习4.1
判断题:
(1)通过变量的地址可以得到该地址上的变量( )。
(2)若var是变量,则表达式* & var等价于左值var。( )