介绍了指针的基本概念和用法后,指针表达式,运算和特殊的指针是下一步的需要阐述的问题。

指针表达式

指针的表达形式多样,甚至可以非常复杂,但是如果理解了指针的意思,再复杂的表达也能轻易看懂。

左值和右值

这里先说一下左值和右值的概念,方便后面的理解。还是使用之前的例子

int a = 112;
int *d = &a;

*d就是左值,&a就是右值。有些变量或者常量可以作为左值或者右值,有些就不能。

表达式例子

表达式 左值 右值
&a 非法 表示取a这个变量所在的地址

为什么这个表达式不能成为左值呢,很简单,当对表达式求值时,它的结果该存储与计算机的什么地方呢?

表达式 左值 右值
d d所处的内存位置 d这个变量的值
&d 非法 取指针变量d的值对应的地址
*d + 1 非法 对于同一数组内,表示指针指向的地址向后挪1位
*(d + 1) 取d的值+1作为地址的变量的值 表示a后面1位的内存位置,由于不可得知,所以很容易出错

++和–操作符在指针运算中出现比较频繁。

表达式 左值 右值
++d 非法 先指针变量d的值+1,然后返回结果
d++ 非法 先返回结果,然后对指针变量的值+1
*++d 非法 对+1之后的指针变量取对应的地址的值
*d++ 非法 对d指针变量取对应的地址的值,因为++是后缀,先返回d,然后再+1

指针运算

指针可不可以运算?答案是肯定的,但并不是所有运算都合法。主要有两种运算:算术运算和关系运算。

算术运算

指针 $\pm$ 整数

标准定义这种形式只能用于指向数组中的某个元素的指针,并且这类表达式的结果类型也是指针。这种形式也适用于使用malloc函数动态分配获得的内存。总的来说,这种形式要求连续的内存位置。那么,对一个指针加1,则是向右移动5个元素的位置。一个指针减去3,则是向左移动3个元素位置。(如果一个不是连续的内存位置,那么进行算术运算,就不能确定产生的指针指向的东西是什么)

指针加上一个整数的结果是另一个指针,但是这个新指针指向哪里? 如果将一个字符指针加1,那么产生的指针指向的是内存中的下一个字符。float占据的内存空间不止1个字节,如果你将一个指向float的指针加1,产生的新指针不会向后挪移,即不会指向float值内部的某个字。它会根据合适的大小进行调整。这个”合适的大小”就是指针所指向类型的大小,“调整”就是把整数值和“合适的大小”相乘。说白了,就是以指针指向类型的大小为单位进行后移。假设,一个机器上float占据4个字节,当float型指针加3的时候,这个3将会乘以4来进行调整,也就是说实际上的加到指针的是12个字节,减法亦然。

指针 - 指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。两个指针相减得结果是两个指针在内存的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。所以指针减法的运算结果与数据的类型无关,得到的结果总是单位距离。

如果两个指针所指的不是同一个数组中的元素,那么它们的相减结果是未定义的。就像如果你把两个位于不同接到的房子的门牌号码相减不可能得出两所房子之间的房子数一样,因为它们的编号根本不是同一标准。

关系运算

对指针执行关系运算的前提也是它们是指向同一个数组中的元素。否者指针的关系运算也是无意义的(不同的编译器会有不同的处理,但是都不会得到有意义的结果)。

指针的关系运算符有:

<;
<=;
>;   
>=;

从本质上讲,指针的比较也是对地址的比较,也就说在一个数组中,或者说一个连续的内存地址中,后面的元素地址值一定大于前面的元素地址,正是这种关系使指针关系运算变得有意义。比如在一个遍历操作中,通过指针的比较来判断是否完成遍历

特殊的指针

除了正常的指针声明,初始化和他们对应的表达式运算外,C语言的指针通常还会涉及一些不常见的指针用法。

指针常量

*100 = 25;

假设a的地址是100,这条语句看似是将25赋值给a,因为a是位置100所存储的变量,但是这行代码是非法的。因为字面值100得类型是整型,而间接访问操作符*只能作用于指针类型表达式。如果非要这么写,那应该是

* (int *) 100 = 25;

能使用这种方式的场景基本就是,需要通过地址访问内存中某个特定的位置,并且这个访问需要通过地址来访问(不是用于访问某个变量,而是访问硬件本身)。

指针的指针

还是使用之前一直使用的例子:

int a = 112;
int *d = &a;

现在我们又有了第三个变量c,

c = &b;

那么b是一个指向整型的指针,而任何指向b的类型都必须是指向“指向整型指针”的指针,所以c是一个指向指针的指针。那要如何声明?

int **c;

这个表达式等同于*(*b),只要由里向外逐层求值就可以获得结果。