数组的某个成员可以用数组的基地址加上一个偏移量来表示。我们可以声明一个指针double *p;,把它作为基地址,然后就可以像数组一样在这个基地址上使用偏移量。在基地址上,我们可以找到第1个成员p[0]的内容,在基地址上前进一步可以找到第2个成员p[1]的内容,接下来以此类推。因此,只要提供一个指针以及两个相邻成员之间的距离,就可以把它作为数组使用了。
我们可以直接采用基地址加偏移量的书面形式,类似(p+1)。正如教科书所描述的那样,p[1]等同于 *(p+1),这就解释了为什么数组的第1个成员是p[0] == *(p+0)。
这个理论提示了一些规则,用于在实际应用中表述数组和它们的成员。
例1展示了这些规则的一些实际应用。
例1 一些简单的指针运算(arithmetic.c)
❶ 使用特殊形式*evens写入到evens[0]。
❷ 成员1的地址,赋值给一个新指针。
❸ 引用数组第1个成员的通常方式。
下面我再送你一个很好的技巧,这个技巧建立在指针运算规则“p+1表示数组中下一个成员的地址(&p[1])”的基础上。根据这个规则,我们不需要在遍历数组的循环中使用下标。在例2中我们就使用了一个备用指针来指向list的头部,然后用p++在数组中向前遍历,直到数组尾部的NULL标记,从而获得了整个数组值。如果你查看了接下来的指针声明的提示,会更容易理解这种用法。
例2我们可以利用p++表示“前进到下一个指针”实现循环的流水化
自己动手
如果不了解p++,你打算怎样实现这个目标?
如果目标是为了实现简洁的语法表示形式,基地址加偏移量这个技巧并不能提供太多的帮助,但它确实解释了C的许多工作原理。事实上,我们可以考虑一下使用结构,例如:
作为一种智力模型来分析,我们可以把list看成是基地址,list[0].b与基地址的距离正好用来表示b。也就是说,假设list的位置是整数(size_t)&list,b位于(size_t)&list + sizeof(int);,这样list[2].d的位置将是(size_t)&list + 6*sizeof(int) + 5*sizeof(double)。根据这种思路,结构就与数组非常相似了,区别是结构的成员是用名称而不是序号表示的,并且它们具有不同的类型和长度。
这个思路并不是非常正确,因为存在对齐这个因素,系统可能会决定数据需要位于某个特定长度的内存块中,因此字段尾部可能会填充一些额外的空间,使下一个字符从正确的位置开始,并且结构的尾部可能也会进行填充,使结构列表中的每个结构能够大致对齐[C99和C11,§6.7.2.1(15)和(17)]。stddef.h头文件定义了offsetof宏,它精确地描述了基地址加领偏移量的思路:list[2].d的实际地址是(size_t)&list + 2*sizeof(abcd_s) + offsetof(abcd_s, d)。
顺便说一下,在结构的起始处不可能出现填充,因此list[2].a肯定等于(size_t)&list+ 2*sizeof(abcd_s)。
下面是个笨拙的函数,它以递归的方式对列表中的成员进行计数,直到遇到值为0的成员。假设我们想把这个函数用于零值为合理数据的任何类型的列表,因此我们让它接受一个void指针(当然这不是一种好的思路)。
基地址加偏移量的规则解释了为什么这种做法是不行的。为了表示a_list[1],编译器需要知道a_list[0]的准确长度,这样才能知道应该从基地址偏移多少。但是,由于没有与之相关联的类型,它无法计算这个长度。
任何时候当我们遇到一种复杂的类型时,类似于指向某种类型的指针的指针的指针等情况,可以考虑用typedef进行简化。
例如,下面这个常见的定义:
有效地减少了字符串数组的视觉混乱,使它们的意图变得清晰。
在前面的指针运算p++例子中,char *list[]这样的声明是否很清楚地告诉你它表示一个字符串列表而*p是一个字符串?
例3对例2的for循环进行了重写,用string替换了char *。
例3 添加一个typedef声明使笨拙的代码稍稍变得清晰
list的声明行现在变得简单,很清晰地表示它是个字符串列表,并且string *p也很清晰地表示p是个指向字符串的指针。因此,*p表示一个字符串。
最后,我们仍然需要记住字符串是个指向字符的指针。例如,NULL是个合法的字符串值。
我们甚至可以更进一步,例如使用上面的typedef加上typedef stringlist string*,声明一个字符串的二维数组。这种方法有时候非常实用,但有时候只会增加记忆的负担。
从概念上讲,函数类型的语法实际上是指向一个特定类型的函数的指针。如果我们有一个头部类似下面这样的函数:
然后只要添加一个星号(并加上括号以保证优先级),就可以描述一个指向这种类型的函数的指针:
然后在前面加上typedef来定义一种类型:
现在我们可以把它当作一种类型使用,例如声明一个接受另一个函数作为其输入参数的函数,可以这样:
通过对函数指针类型的重新定义,那些接受其他函数作为输入的函数的表达—其中连环星号的书写曾是令人生畏的考验变得不再可怕。
最后需要说明的是,指针实际上要比教科书所描述的简单得多,因为它实际上只是一个位置或别名,根本不需要涉及不同类型的内存管理。像指向字符串的指针的指针这样的复杂构造总是会让人感到迷惑,但这只不过是因为我们以狩猎为生的祖先从来没有见到过这玩意而已。至少,C提供了typedef这个工具来处理它们。
本文节选自《C程序设计新思维》