一些声明:
char ch = 'a'; char *cp = &ch;
现在有了两个变量,一个ch(字符类型变量),一个cp(指向字符类型的指针变量)。
它们的初始化如下:
图中还显示出了ch后面的那个内存位置,由于并不知道它的初始值,所以用一个问号来代替。
下面来讨论一些表达式的意义。
ch &ch cp &cp *cp *cp+1 *(cp+1) ++cp cp++ *++cp *cp++ ++*cp (*cp)++ ++*++cp ++*cp++
左值(L-value) 和 右值(R-value): 通俗的讲,左值就是能够出现在赋值符号左边的东西,而右值就是那些可以出现在赋值符号右边的东西了。较为准确的定义:左值指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以做为一个左值。右值指的是引用了一个存储在某个内存地址里的数据。从上面的两个定义可以看出,左值其实要引用一个对象,而一个对象在我们的程序中又肯定有一个名字或者可以通过一个名字访问到,所以左值又可以归纳为:左值表示程序中必须有一个特定的名字引用到这个值。而右值引用的是地址里的内容,所以相反右值又可以归纳为:右值表示程序中没有一个特定的名字引用到这个值除了用地址。
ch
当它作为右值使用时,表达式的值为'a',如下图所示:
那个粗椭圆提示变量ch的值就是表达式的值,即字符a。
但是,当这个表达式作为左值使用时,它是指这个内存的地址,而不是这个该地址所包含的值。
所以它的图示方式有所不同:
此时该位置用粗方框标记,提示这个位置就是表达式的结果。另外,它的值并未显示,因为它并不重要。事实上,这个值将被某个新值取代。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; printf("%c\n",ch); //作为右值 char name = ch; //右值引用的是地址里的内容,即字符a。把name变量初始化为字符a。 //作为左值 ch = 'b'; //左值其实要引用一个对象,指ch这个变量在内存中的地址。把这个地址里面的内容修改为'b'。 printf("%c\n",name); printf("%c\n",ch); return 0; }
&ch
作为右值,这个表达式的值就是变量ch的地址。注意这个值同变量cp中所存储的值一样,但这个表达式并未提及cp,所以这个结果值并不是因为它产生的。这样,途中椭圆并不画于cp的箭头周围。
为什么这个表达式不是一个合法的左值?当表达式&ch进行求值时,它的结果应该存储于计算机的什么地方呢?它肯定会位于某个地方,但你无法知道它位于何处。所以说它不是一个合法的左值。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //ch的地址和变量cp中所存储的值是一样的。 printf("%p\n",cp); printf("%p\n",&ch); //作为右值 char *name = &ch; printf("%p\n",name); return 0; }
cp
它的右值就是cp的值,也就是ch的地址。它的左值就是cp所处的内存位置。
#includeint main() { char ch = 'a'; char *cp = &ch; //作为右值 char *cp1 = cp; printf("%p\n",cp); printf("%p\n",cp1); //作为左值 cp = NULL; //把指针变量cp设为NULL指针,现在cp就不再指向ch了。 printf("%p\n",cp); return 0; }
&cp
这个与&ch类似。不过这次所取的是指针变量的地址。作为右值时,这个结果的类型是指向字符的指针的指针。同样,这个值的存储位置并未清晰定义,所以这个表达式不是一个合法的左值。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //打印指针变量cp在内存中的地址。 printf("%p\n",&cp); //作为右值 char **cp1 = &cp; //cp1是一个指向“指向字符类型的指针”的指针。 printf("%p\n",cp1); //打印出cp1所存储的值,也就是cp的地址。 return 0; }
*cp
这个是间接访问操作,也叫解引用指针。注意:*cp的类型是char。
作为右值和左值时表达式的结果和ch的一样。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; printf("%c\n",*cp); printf("%c\n",ch); //作为右值 char name = *cp; printf("%c\n",name); //作为左值 *cp = 'b'; printf("%c\n",*cp); printf("%c\n",ch); return 0; }
接下来的几个表达式会比较有趣。
*cp+1
这里有两个操作符。作为右值时,*的优先级高于+,所以首先执行间接访问操作(如图中cp到ch的实线箭头所示),可以得到它的值(如虚线椭圆所示)。取得这个值的一份拷贝并把它与1相加,表达式的最终结果为字符'b'。图中虚线表示表达式求值时数据的移动过程。这个表达式的最终结果的存储位置并未清晰定义,所以它不是一个合法的左值。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //作为右值 char name = *cp+1; printf("%c\n",name); // 将会输出字符'b' printf("%c\n",*cp); //还是字符'a' return 0; }
*(cp+1)
在前面的表达式中增加了一个括号。这个括号使表达式先执行加法运算,就是把1和cp中所存储的地址相加。此时的结果值是图中虚线椭圆所示的指针。接下来的间接访问操作随着箭头访问紧随ch之后的内存位置。这样,这个表达式的右值就是这个位置的值,而它的左值就是这个位置本身。
注意指针加法运算的结果是个右值,因为它的存储位置并未清晰定义。如果没有间接访问操作,这个表达式将不是一个合法的左值。然而,间接访问紧随指针访问一个特定的位置。这样,*(cp+1)就可以作为左值使用,尽管cp+1本身并不是左值。间接访问操作符是少数几个其结果为左值的操作符之一。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //作为右值 char name = *(cp+1); printf("%c\n",name); // 以字符的形式输出,不知道会输出什么。 printf("%p\n",name); // 以十六进制的形式输出 //作为左值 *(cp+1) = 'b'; name = *(cp+1); printf("%c\n",name); // 以字符的形式输出,输出‘b’。 printf("%p\n",name); // 以十六进制的形式输出 return 0; }
注意:这个表达式所访问的是ch后面的那个内存位置,我们如何知道原先存储于那个地方的是什么东西?一般而言,我们无法得知,所以像这样的表达式是非法的。
一般来说,经常是在数组使用*(cp+1)。数组的储存是一片连续的内存区域。
++cp
在这个表达式中,增加了指针变量cp的值。(为了让图更清楚,我们省略了加法)。表达式的结果是增值后的指针的一份拷贝,因为前缀++先增加它的操作数的值再返回这个结果。这份拷贝的存储位置并没有清晰定义,所以它不是一个合法的左值。(前一个例子也说到指针的加法不是一个左值)。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //作为右值 char *cp1 = ++cp; printf("%c\n",*cp1); //不知道会输出什么。 return 0; }
cp++
后缀++操作符同样增加cp的值,但它先返回cp值的一份拷贝然后再增加cp的值。这样,这个表达式的值就是cp原来的值的一份拷贝。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //作为右值 char *cp1 = cp++; printf("%c\n",*cp1); //输出字符'a'。 return 0; }
前面两个表达式的值都不是合法的左值。但如果我们在表达式中增加了间接访问操作符,它们就可以成为合法的左值,如下面的两个表达式所示。
*++cp
间接操作符作用于增值后的指针的拷贝上,所以它的右值时ch后面那个内存地址的值,而它的左值就是那个位置本身。
验证:(左值和右值的用法和*cp类似,由于这是非法访问,就不举例了。)
*cp++
使用后缀++操作符所产生的结果不同:它的右值和左值分别是变量ch的值和ch的内存位置,也就是cp原先所指。同样,后缀++操作符在周围的表达式中使用其原先操作数的值。后缀++操作符的优先级高于*操作符。事实上,这里涉及3个步骤:(1)++操作符产生cp的一份拷贝,(2)然后++操作符增加cp的值,(3)最后,在cp的靠拷贝上执行间接访问操作。
验证:(右值和左值的用法也和*cp类似,只是这个表达式还有一个效果就是增加cp的值,使得cp不知指向ch,而是指向ch的下一个内存地址。)
这个表达式常常在循环中出现,首先用一个数组的地址初始化指针,然后使用这种表达式就可以依次访问该数组的内容了。
++*cp
在这个表达式中,由于这两个操作符的结合性都是从右向左,所以首先执行的是间接访问操作。然后,cp所指向的位置的值增加1,表达式的结果就是这个增值后的值的一份拷贝。
验证:
#includeint main() { char ch = 'a'; char *cp = &ch; //作为右值 char ch1 = ++*cp; printf("%c\n",ch1); //打印出字符'b' printf("%c\n",*cp); //同时,ch存储的内容也修改为了字符'b' return 0; }
最后3个表达式在实际中使用得很少。
(*cp)++
加上括号,使它首先执行间接访问操作。这个表达式的计算过程与前一个表达式相似,但它的结果值是ch增值前的原先值,即'a'。
++*++cp
这个表达式共有3个操作符。我们先前已经计算过*++cp了,所以现在我们需要做的只是增加它的结果值。但还是从头开始分析吧,记住这些操作符的结合性都是从右向左的,所以首先执行的是++cp。cp下面的虚椭圆表示第1个中间结果。接着,我们对这个拷贝值进行间接访问,它使我们访问ch后面的那个内存位置。第2个中间结果用虚线方框表示,因为下一个操作符把它当作一个左值使用。最后,我们在这个位置执行++操作,也就是增加它的值。
++*cp++
这个表达式和前一个表达式的区别在于这次第1个++操作符是后缀形式而不是前缀形式,由于它的优先级较高,所以先执行cp++。间接访问操作所访问的是cp所指的位置而不是cp所指向的位置后面的那一个位置。