C语言非常道
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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。( )