在C语言程序开发中,一些比较成熟的库函数常常会被使用。毕竟,如果手边就有不错的“轮子”可以用,没有程序员愿意再花费精力凭空造一个轮子出来。
奇怪的 void* 指针
事实上,C语言标准库提供了非常丰富的成熟函数供程序员使用,不过不知道读者注意到没,有些库函数的参数是 void * 类型的,例如:
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
前面的章节在讨论C语言指针时,提到指针从某种程度上来说,无非就是一个地址,它的类型只是用于说明数据结构的。例如 int * 型指针告诉编译器该地址处紧接着的 4 字节按照 int 型数据解释,double * 型指针高速编译器接下来的 8 字节按照 double 型数据解释。
这里假定int型变量占用4字节内存空间,double 型变量占用 8 字节内存空间。
对于 void * 型指针,之前的分析似乎就不再适用了。因为 void 类型是一个特殊的类型,常被称作“空类型”,C语言中没有 void 类型的变量,所以在遇到 void * 指针时,编译器根本不知道如何解释接下来的内存,甚至编译器都不知道接下来多少内存属于它。
正因为如此,在C语言程序开发中,遇到 void * 指针时,如果需要访问它指向的内存,需要重新指定类型,否则就会报错。例如下面这段C语言代码:
#include <stdio.h>
void myprint(void *p)
{
char c = p[0];
printf("c=%c\n", c);
}
int main()
{
char buf[] = "hello world";
myprint(buf);
return 0;
}
编译这段C语言代码,得到如下输出:
可以看出,这段代码在编译阶段就出错了,原因我们已经分析过:编译器不知道该如何解释 void * 型指针 p。所以在使用 void * 型指针时,要将其先转换为 char * 型,相应C语言代码如下:
void myprint(void *p)
{
char c = ((char*)p)[0];
printf("c=%c\n", c);
}
有时候为了简便,常常使用中间变量:
void myprint(void *p)
{
char *cp = (char*)p;
char c = cp[0];
printf("c=%c\n", c);
}
这时再编译执行就一切正常了。
为什么使用 void * 指针
仅从上面的实例来看,使用 void * 指针似乎比较麻烦:至少强制类型转换操作是少不了的。那为什么还要使用 void * 指针呢?
仔细分析上面的实例,读者应注意“在使用 void * 型指针时,要将其先转换为 char * 型”,这其实要求程序员事先了解 void * 指针指向的数据结构(本例是 char * 型),否则就没法使用 void * 指针。
利用这一点,程序员可以使用 void * 将不公开的数据隐藏起来,避免外界调用者访问和修改它。例如,在实际的C语言项目开发中,操作某个对象时,常常先构建结构体 struct S 描述该对象,然后使用 init() 函数获取相应信息,因为接下来的操作函数 handle() 需知道要操作哪个对象,所以要使用 init() 函数返回的信息,C语言代码似乎可以这么写:
struct S *p = init();
handle(p);
从上面两行C语言代码可以看出,其实外界调用可以不用关心 p 的具体数据结构。不过,如果这么写了,外界又的确可以随意访问 p 指向的数据结构,这样就很危险了。事实上,我们完全可以将 p 转换为 void * 指针:
void *p = init();
handle(p);
这样一来,只有 init() 库的开发者才能访问 p 指向的内容,避免外界调用者“意外”或者“恶意”的修改,整个C语言程序就会稳定和安全很多了。
另外,void * 指针在一些教科书里被称作“万能指针”,这主要是因为任意指针都可以使用 void * 指针传递,并且编译器不会报出“类型不匹配”相关的警告。例如,要是将 myprint() 函数的参数类型修改为 int * 型,相关C语言代码如下:
void myprint(int *p)
{
char *cp = (char*)p;
char c = cp[0];
printf("c=%c\n", c);
}
再次编译时,就会发现编译器发出警告了:
应明白,myprint() 只是为了讨论主题而举出的简单例子,在实际应用中,当然没有人会闲的将指针转来转去。不过,如果不能确定,或者不限定传入的指针类型,使用 void * 指针就是首选了,例如我们前面讨论过的 pthread_create() 函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
从它的C语言原型可以看出,第4个参数为 void * 型的指针,该指针负责将参数传递给 start_routine 指向的函数。void * 指针不限定传递的参数的数据结构,“万能指针”允许程序员传递任意参数给 start_routine 指向的函数。
小结
本节抛砖引玉,主要讨论了 void * 指针在实际C语言项目开发中的特性和用途。void * 指针可以将不希望被公开的数据结构隐藏,避免外界调用“意外”或者“恶意”的修改。另外,void * 指针作为“万能指针”,可以传递任意类型的指针,而不引起“类型不匹配”相关的编译警告。