5.4 地址算术运算
如果 p
是一个指向数组中某个元素的指针,那么 p++
将对 p
进行自增运算并指向下一个元素
而 p += i
将对 p
进行加 i
的增量运算,使其指向指针 p
当前所指向的元素之后的第 i
个元素
这类运算是指针或地址算术运算中最简单的形式
C 语言中的地址算术运算方法是一致且有规律的,将指针、数组和地址的算术运算集成在一起是该语言的一大优点
为了说明这一点,我们来看一个不完善的存储分配程序,它由两个函数组成
第一个函数 alloc(n)
返回一个指向 n
个连续字符存储单元的指针,alloc
函数的调用者可利用该指针存储字符序列
第二个函数 afree(p)
释放已分配的存储空间,以便以后重用
之所以说这两个函数是 “ 不完善的 ”,是因为对 afree
函数的调用次序必须与调用 alloc
函数的次序相反
换句话说,alloc
与 afree
以栈的方式(即后进先出的列表)进行存储空间的管理
标准库中提供了具有类似功能的函数 malloc
和 free
,它们没有上述限制,我们将在 8.7 节中说明如何实现这些函数
最容易的实现方法是让 alloc
函数对一个大字符数组 allocbuf
中的空间进行分配
该数组是 alloc
和 afree
两个函数私有的数组
由于函数 alloc
和 afree
处理的对象是指针而不是数组下标,因此,其它函数无需知道该数组的名字
这样,可以在包含 alloc
和 afree
的源文件中将该数组声明为 static
类型,使得它对外不可见
实际实现时,该数组甚至可以没有名字,它可以通过调用 malloc
函数或向操作系统申请一个指向无名存储块的指针获得
allocbuf
中的空间使用状况也是我们需要了解的信息
我们使用指针 allocp
指向 allocbuf
中的下一个空闲单元
当调用 alloc
申请 n
个字符的空间时,alloc
检查 allocbuf
数组中有没有足够的剩余空间
如果有足够的空闲空间,则 alloc
返回 allocp
的当前值(即空闲块的开始位置),然后将 allocp
加 n
以使它指向下一个空闲区域
如果空闲空间不够,则 alloc
返回 0
如果 p
在 allocbuf
的边界之内,则 afree(p)
仅仅只是将 allocp
的值设置为 p
#define ALLOCSIZE 10000 /* size of available space */
static char allocbuf[ALLOCSIZE]; /* storage for alloc */
static char *allocp = allocbuf; /* next free position */
char *alloc(int n) /* return pointer to n characters */
{
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
allocp += n;
return allocp - n; /* old p */
} else /* not enough room */
return 0;
}
void afree(char *p) /* free storage pointed to by p */
{
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
allocp = p;
}
一般情况下,同其它类型的变量一样,指针也可以初始化
通常,对指针有意义的初始化值只能是 0
或者是表示地址的表达式
对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址
例如,声明 static char *allocp = allocbuf;
将 allocp
定义为字符类型指针,并将它初始化为 allocbuf
的起始地址
该起始地址是程序执行时的下一个空闲位置
上述语句也可以写成 static char *allocp = &allocbuf[0];
这是因为该数组名实际上就是数组第 0
个元素的地址
if
测试语句 if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
检查是否有足够的空闲空间以满足 n
个字符的存储空间请求
如果空闲空间足够,则分配存储空间后 allocp
的新值最多比 allocbuf
的尾端地址大 1
如果存储空间的申请可以满足,alloc
将返回一个指向所需大小的字符块首地址的指针(注意函数的声明)
如果申请无法满足,alloc
必须返回某种形式的信号以说明没有足够的空闲空间可供分配
C 语言保证,0
永远不是有效的数据地址,因此,返回值 0
可用来表示发生了异常事件
在本例中,返回值 0
表示没有足够的空闲空间可供分配
指针与整数之间不能相互转换,但 0
是惟一的例外:常量 0
可以赋值给指针,指针也可以和常量 0
进行比较
程序中经常用符号常量 NULL
代替常量 0
,这样便于更清晰地说明常量 0
是指针的一个特殊值
符号常量 NULL
定义在标准头文件 <stddef.h>
中
类似于 if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
以及 if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
的条件测试语句表明指针算术运算有以下几个重要特点
首先,在某些情况下对指针可以进行比较运算
例如,如果指针 p
和 q
指向同一个数组的成员,那么它们之间就可以进行类似于 ==
、!=
、<
、>=
的关系比较运算
如果 p
指向的数组元素的位置在 q
指向的数组元素位置之前,那么关系表达式 p < q
的值为真
任何指针与 0
进行相等或不等的比较运算都有意义
但是,指向不同数组的元素的指针之间的算术或比较运算没有定义
这里有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址
指针可以和整数进行相加或相减运算
p + n
表示指针 p
当前指向的对象之后第 n
个对象的地址
无论指针 p
指向的对象是何种类型,上述结论都成立
在计算 p + n
时,n
将根据 p
指向的对象的长度按比例缩放,而 p
指向的对象的长度则取决于 p
的声明
例如,如果 int
类型占 4 个字节的存储空间,那么在 int
类型的计算中,对应的 n
将按 4 的倍数来计算
指针的减法运算也是有意义的
如果 p
和 q
指向相同数组中的元素,且 p < q
那么 q - p + 1
就是从 p
到 q
指向元素间的元素的数目(包括 p
和 q
)
我们由此可以编写出函数 strlen
的另一个版本,如下所示:
/* strlen: return length of string s */
int strlen(char *s)
{
char *p = s;
while (*p != '\0')
p++;
return p - s; /* 这里没加 1 是因为字符串长度没有包括最后的 '\0' */
}
在上述程序段的声明中,指针 p
被初始化为指向 s
,即指向该字符串的第一个字符
while
循环语句将依次检查字符串中的每个字符,直到遇到标识字符数组结尾的字符 '\0'
由于 p
是指向字符的指针,所以每执行一次 p++
,p
就将指向下一个字符的地址
p - s
则表示已经检查过的字符数,即字符串的长度
字符串中的字符数有可能超过 int
类型所能表示的最大范围
头文件 <stddef.h>
中定义的类型 ptrdiff_t
足以表示两个指针之间的带符号差值
但是,我们在这里使用 size_t
作为函数 strlen
的返回值类型,这样可以与标准库中的函数版本相匹配
size_t
是由运算符 sizeof
返回的无符号整型
指针的算术运算具有一致性:如果处理的数据类型是比字符型占据更多存储空间的浮点类型
并且 p
是一个指向浮点类型的指针,那么在执行 p++
后,p
将指向下一个浮点数的地址
因此,只需要将 alloc
和 afree
函数中所有的 char
类型替换为 float
类型
就可以得到一个适用于浮点类型而非字符型的内存分配函数
所有的指针运算都会自动考虑它所指向的对象的长度
有效的指针运算包括:
- 相同类型指针之间的赋值运算
- 指针同整数之间的加法或减法运算
- 指向相同数组中元素的两个指针间的减法或比较运算
- 将指针赋值为
0
或指针与0
之间的比较运算
其它所有形式的指针运算都是非法的,如:
两个指针间的加法、乘法、除法、移位或 mask
运算
指针同 float
或 double
类型之间的加法运算
不经强制类型转换而直接将指向一种类型对象的指针赋值给指向另一种类型对象的指针的运算(两个指针之一是 void *
类型的情况除外)