C语言深处

    返回首页    发表留言
本文作者:李德强
          第三节 通用指针
 
 

一、通用指针

        通用指针变量用void *来修饰。顾名思义,通用指针可以用来存放任意型的内存地址,char、short、int、float、double等这些类型的地址都可以使用void *型变量来存放。同样char *、short *、int *、float *、double *等指针变量均可以赋值给void *型变量。例如:

#include <stdio.h>
#include <string.h>

int main(int argc, char **args)
{
        char v0;
        short v1;
        int v2;
        long v3;
        float v4;
        double v5;

        char *p0 = &v0;
        short *p1 = &v1;
        int *p2 = &v2;
        long *p3 = &v3;
        float *p4 = &v4;
        double *p5 = &v5;

        void *p;

        p = &v0;
        p = &v1;
        p = &v2;
        p = &v3;
        p = &v4;

        p = p0;
        p = p1;
        p = p2;
        p = p3;
        p = p4;
        p = p5;
}

       以上代码是合法的,没有任何问题。编译时可以顺利通过。但是通用指针与普通指针的唯一区别是:通用指针无法解引用。实际这是一个比较大的问题,正是因为通用指针可以存放任意类型的地址,所以当给通用指针解引用时编译器无法确定它所指向内存地址中的原始数据类型。例如,我们定义了一个通用指针变量,并尝试让编译器为其解引用:

#include <stdio.h>

int main(int argc, char **args)
{
        int i = 0x11223344;
        void *p = &i;
        *p = 0;
}

        这样的代码是错误的,编译器无法对其编译。因为p是一个通用的指针变量,它可以指向任意类型的变量地址,甚至是函数地址。那么当对p做解引用时编译器并不知道p所指向的地址是什么数据类型,所以在对上面代码编译时会出现错误:

main.c:7:3: error: invalid use of void expression

       实际上,想要对通用指针解引用必须要对其强制类型转换为指定类型指针,再对其解引用,例如:

#include <stdio.h>

int main(int argc, char **args)
{
        int i = 0x11223344;
        void *p = &i;
        *(char *)p = 0;
        p+=2;
        *(char *)p = 0;

        printf("0x%x\n", i);
}

        运行结果:

0x11003300

       我们来分析一下int i和void *p在内存中的结构:     


 

        首先(char *)p就是将通用指针强制转换为char *类型,也就是告诉编译器这是一个指向char型的指针变量,然后再对其解引用*(char *)p,把这个地址中的变量赋值为0。由于编译器认为这是一个char型指针,所以*(char *)p只为地址为0x2000的内存byte赋值。接下来p += 2;表示p的值加2,于是p的值为0x2002,然后同样将其强转为char型指针,再对其解引用并赋值。于是内存中0x2002处的值就被赋值成了0。最后使用printf函数来显示i的值为0x11003300,运行结果正确。

        注意:虽然void *p这样的通用指针不能解引用,但是void **p这样的二级指针却可以解引用:

#include <stdio.h>

int main(int argc, char **args)
{
        int i = 7;
        void *p1 = &i;
        void **p2 = &p1;

        printf("0x%x\n", *p2);
}

        上面代码的合法的,运行结果为:

0xffc68d28

        为什么void *p1不可以解引用,但void **p2却可以呢?这是因为void *p1是一个通用指针,编译无法知道p1所指向的地址是一个什么类型的变量,而对于void **p2而言,编译器知道p2是一个指针,这个指针变量中存放的是一个通用型指针的地址也就是&p1,因为&p1是一个地址,在32位架构下所有指针变量以及内存地址都占用4byte(32bit),所以对p2解引用*p2是合法的,它代表的是一个void *型变量的地址即&p1,它的值是0xffc68d28。

 

二、通用函数

        下面来介绍一下通用指针的实际应用。我们不希望编程工作中出现大量的功能相同、类似的代码,而希望通过一些通用的功能函数来简化编程工作,这样作也便于编写代码的工作量和后期功能修改。例如,我们定义如下3组数组变量:

char array_0[SIZE] = {'H','e','l','l','o'};
char array_1[SIZE];

int array_2[SIZE] = {1,2,3,4,5};
int array_3[SIZE];
        
double array_4[SIZE] = {12.3, 23.4, 34.5, 45.6, 56.7};
double array_5[SIZE];

        我们想要如下操作:

  • 将array_0所有的值复制到array_1中;
  • 将array_2所有的值复制到array_3中;
  • 将array_4所有的值复制到array_5中。

        实际上我们不想实现3遍数组复制的功能,重复的实现相同的代码可不是一个好习惯。我们应该实现一个通用的函数,专门用于数组的值复制,这就用到了通用指针,我们想使用一个函数使用通用型指针来接收2个数组的首地址,然后进行数组的值复制。即支持char,也支持int,也支持double,实际上这个函数可以支持所有数据类型的数组复制:

void array_copy(void *src, void *tar, int element_size, int array_size)
{
        for (int i = 0; i < element_size * array_size; i++)
        {
                *(char *) tar = *(char *) src;
                tar++;
                src++;
        }
}

        array_copy函数有4个参数:

  • void *src              存放来源数组首地址的通用指针
  • void *tar               存放目标数组首地址的通用指针
  • int element_size  数组元素大小在调用时使用sizeof函数来计算
  • int array_size       数组元素个数

        有了这个函数就可以方便的对不同类型的数组做复制了:

int main(int argc, char **args)
{
        char array_0[SIZE] =
        { 'H', 'e', 'l', 'l', 'o' };
        char array_1[SIZE];

        int array_2[SIZE] =
        { 1, 2, 3, 4, 5 };
        int array_3[SIZE];

        double array_4[SIZE] =
        { 12.3, 23.4, 34.5, 45.6, 56.7 };
        double array_5[SIZE];

        array_copy(array_0, array_1, sizeof(char), SIZE);
        array_copy(array_2, array_3, sizeof(int), SIZE);
        array_copy(array_4, array_5, sizeof(double), SIZE);

        for (int i = 0; i < SIZE; i++)
        {
                printf("%c ", array_1[i]);
        }
        printf("\n");

        for (int i = 0; i < SIZE; i++)
        {
                printf("%d ", array_3[i]);
        }
        printf("\n");

        for (int i = 0; i < SIZE; i++)
        {
                printf("%f ", array_5[i]);
        }
        printf("\n");
}

        运行结果:

H e l l o 
1 2 3 4 5 
12.300000 23.400000 34.500000 45.600000 56.700000

        可以看到运行结果非常正确。其实现代码简单又方便。

        通用指针的作用不可小觑,我们将在下一节学习多级指针并在下一章中学习通用指针的另外一个重要应用:回调函数。 

    返回首页    返回顶部
#1楼  吴  于 2017年10月18日13:12:12 发表
 
在阅读这一篇文章时,出于对通用指针的好奇,我就按着文章所说的把上面实现数组复制功能的代码敲了一下,结果确有一个错误。错误提示是ios++forbids incrementing a pointer of type ‘void*‘[fpermissive],于是我把问题反馈给强哥。强哥解释说应该是编译器的原因,他用的是gcc并指定了std 11标准。而且强哥还热心的帮我提出了解决方法。通过在array_copy这个函数开始的地方

char *p_s = (char*)tar;

char *p_t = (char*)src;

在下面的循环里用p_s和p_t就可以了

*p_s = *p_t;

p_s++;

p_t++;

让我成功的解决了这个问题。在此再次感谢强哥
#2楼  李德强  于 2017年10月18日15:49:39 发表
 
一些C语言的编译器不允许对void*的指针变量做算数运算,因为编译器不知道void*型的指针变量所指向的是一个什么类型的变量 ,所以对其做算数运算时,编译器无法确定应该加减多少个字节的地址。

但在C11标准中,为了让所有的指针都能做算数运算,所以规定了void*型指针在++或--时只做一个字节的增减,在其它算数运算时与char*型指针一样处理,这就是为什么指定了了C11标准后的程序允许void*型指针做算数运算。

但二级指针void**在不指定C11标准也是可以直接做算数运算的,因为二级指针void**它是指向了一个一级指针void*的地址,也就是说编译器知道这个二级指针所指向变量的类型,也就可以确定它所指向的地址长度,所以对二级指针++就是增加4个字节(64位系统里是8个字节)。
#3楼  开发实习生  于 2020年01月10日21:59:12 发表
 
今天发现void*的这个东西可以实现代码的复用,顺序栈的data类型可以定义成void*,这样data可以是int* ,可以是float*,取出来的时候强转。这样不用每次都写一个stack了。
#4楼  匿名  于 2021年05月11日17:10:08 发表
 
支持问渠网
#4楼  匿名  于 2021年05月11日17:10:08 发表
 
其实我一直在想C++的模板是不是通过这种方式实现的。。。
  看不清?点击刷新

 

  Copyright © 2015-2023 问渠网 辽ICP备15013245号