回想一下,在C中指针变量拥有与其他变量一样的类型。之所以指针变量会有类型是因为当我们想获取指针变量的值时,编译器已经知道指针所指向的数据的类型,从而可以访问相应的数据。但是,有些时候我们并不关心指针所指向的变量的类型。在这种情况下,就可以使用泛型指针,泛型指针并不指定具体的数据类型。
泛型指针
通常情况下,C只允许相同类型的指针之间进行转换。例如:一个字符型指针sptr(一个字符串)和一个整型指针iptr,我们不允许把sptr转换为iptr或把iptr转换为sptr。但是,泛型指针能够转换为任何类型的指针,反之亦然。因此,如果有一个泛型指针gptr,就可以把sptr转换为gptr或者把gptr转换为sptr。在C语言中,通常声明一个void指针来表示泛型指针。
很多情况下void指针都是非常有用的。例如:C标准函数库中的memcpy函数,它将一段数据从内存中的一个地方复制到另一个地方。由于memcpy可能用来复制任何类型的数据,因此将它的指针参数设定为void指针是非常合理的。void指针同样也可以用到其他普通的函数中。例如:之前提到过的交换函数swap2,可以把函数参数改为void指针,这样swap2就变成一个可以交换任何类型数据的通用交换函数,代码如下:
- #include <stdlib.h>
- #include <string.h>
- int swap2(void *x, void *y, int size)
- {
- void *tmp;
- if ((tmp = malloc(size)) == NULL)
- return -1;
- memcpy(tmp, x, size); memcpy(x, y, size); memcpy(y, tmp, size);
- free(tmp);
- return 0;
- }
void指针在用来实现数据结构时是非常有用的,因为可以通过void指针存储和检索任何类型的数据。我们再来看一下之前提到过的链表结构ListElmt,回想一下,这个结构包含两个成员:data和next。如果data被声明为一个void指针,那么data就可以指向任何类型的数据。从而,我们就可以使用ListElmt结构来建立各种不同数据类型的链表。
在第5章里,定义了一个链表的操作函数list_ins_next,它的功能是将一个指向data的指针元素插入链表中:
- int list_ins_next(List *list, ListElmt *element, void *data);
要将指针iptr引用的整数插入名为list的整型链表中,element引用的元素后面,使用以下调用。C语言允许将整型指针iptr赋值给参数data,因为data是一个void指针。
- retval = list_ins_next(&list, element, iptr);
当然,当从一个链表中删除数据时,必须使用正确的指针类型来检索要删除的数据。这样做是为了保证当我们想对数据进行操作时数据的类型是正确的。如前所述,从一个链表中删除元素的函数list_rem_next,它的第三个参数是一个指向void指针的指针:
- int list_rem_next(List *list, ListElmt *element, void **data);
想要从list中element引用的元素后面删除一个整型变量,用如下调用方式。当函数返回时,iptr指向已删除的数据。这是由于此操作改变了指针本身,使其指向已删除的数据,因此传递iptr指针的地址。
- retval = list_rem_next(&list, element, (void **)&iptr);
同时,此函数调用包含一个将iptr临时转换为一个指向void指针的指针的过程。正如我们将在下一节所看到的,类型转换是C语言中一种特殊的转换机制,它允许我们临时把一种类型的变量转换为另一种类型的变量。在这里,类型转换是必要的,因为C语言中虽然一个void指针与其他类型的指针相兼容,但一个指向void指针的指针并不一定与其他类型的指针兼容。
类型转换
要将类型为T的变量t转换成S类型,只需要在t前加上用圆括号括上的S。例如:要将一个整型指针iptr转换为一个浮点型指针fptr,在整型指针前面加上一个用圆括号括起来的浮点指针即可,如下所示:
- fptr = (float *)iptr;
通常来说将一个整型指针转换成一个浮点指针是一种危险的做法,但在这里我们仅仅用这个例子做一个类型转换的示例而已。)在类型转换之后,iptr与fptr都指向同一块内存地址。但是,从这个地址取到什么类型的值是由我们用什么类型的指针访问它所决定的。
对于泛型指针来说类型转换非常重要,因为只有告诉泛型指针是通过何种类型来访问地址时,泛型指针才能正确取到值。这是由于泛型指针不会告诉编译器它指向的是何种类型数据,因此编译器既不知道多少个字节要被访问,也不知道应该如何解析字节。当将泛型指针赋值给其他类型的指针时,使用类型转换也是一种很好的代码自注释方法。尽管这里的转换并不是必需的,但这样做能大大提高程序的可读性。
转换指针时,我们对内存中的数据对齐方式必须特别注意。具体来说,我们需要知道,指针的类型转换会破坏计算机本身的对齐方式。很多计算机对对齐的方式有要求,以便某些硬件的优化可以使访问内存更有效率。例如,一个系统可能要求所有整数按字边界对齐。所以,如果有一个非按字对齐的void指针,当将它转换为一个整型指针并试图获取它的值时,程序可能在运行时出现异常。



