13

C语言陷阱与技巧第31节,都说void*指针是“万能指针”,它有什么用?为什么要用void*指...

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC31%E8%8A%82-%E9%83%BD%E8%AF%B4void%E6%8C%87%E9%92%88%E6%98%AF%E4%B8%87%E8%83%BD%E6%8C%87%E9%92%88/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在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;
}
22de7cea14cf4c22ff67e87b516919f5.png

编译这段C语言代码,得到如下输出:
988027260b6212d79084b3e791eab014.png

可以看出,这段代码在编译阶段就出错了,原因我们已经分析过:编译器不知道该如何解释 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);
}

再次编译时,就会发现编译器发出警告了:

1a089f5d38e43351b59cc3223ba914d2.png

应明白,myprint() 只是为了讨论主题而举出的简单例子,在实际应用中,当然没有人会闲的将指针转来转去。不过,如果不能确定,或者不限定传入的指针类型,使用 void * 指针就是首选了,例如我们前面讨论过的 pthread_create() 函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

5c55a21d46e2be35878d697455af39de.png
从它的C语言原型可以看出,第4个参数为 void * 型的指针,该指针负责将参数传递给 start_routine 指向的函数。void * 指针不限定传递的参数的数据结构,“万能指针”允许程序员传递任意参数给 start_routine 指向的函数。

本节抛砖引玉,主要讨论了 void * 指针在实际C语言项目开发中的特性和用途。void * 指针可以将不希望被公开的数据结构隐藏,避免外界调用“意外”或者“恶意”的修改。另外,void * 指针作为“万能指针”,可以传递任意类型的指针,而不引起“类型不匹配”相关的编译警告。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK