对于C语言多重指针的理解

Posted by 皮皮潘 on 12-28,2021

背景

最近在看《深入理解Nginx》一书,其中涉及了大量Nginx的源码,由于Nginx是使用C语言写的,因此本来就看的比较头疼了,然后又看到了Nginx的模块配置存储那一块,Nginx在具体实现中每个进程都会有一个ngx_cycle_t的数据结构去存储所有重要的东西,其中就是使用了一个conf_ctx的成员变量去存储所有模块的配置信息,然后这个成员变量的类型是void ****,一下子就懵逼了,于是和实验室做C++助教的同学讨论了一下C语言中多星号指针的意义,特地通过这篇博文记录下来,以好日后温故知新

理解

当只有一个星号的时候,代表对应的变量是一个指针,变量具体存储的是一个8个byte的数值,该数值对应了一个虚拟内存地址,对该指针解引用的时候,其实就是去获取变量对应的虚拟内存地址所存储的数据,至于拿多少数据,就根据指针类型决定了,如果是int *那么就拿从指针对应的虚拟内存地址开始的4个Byte,如果是char *那么就是从指针对应的虚拟内存地址开始的1个Byte……,这里需要注意的是一个特殊的指针void *,由于不存在void类型,因此在对于void *类型指针解引用时,一定先要把它转化成其他指针,另外只有指针之间可以随意互转,普通类型不允许转换成指针(比如:long类型就不能转换成void *类型,虽然它们都是8字节的)

除了将一个星号的变量看作一个简单指针之外,该变量也可以看作是一个数组的起始位置,因为对于指针进行加减操作的时候,会将对应的虚拟内存地址加上sizeof(type),比如如果是int *ptr那么ptr + 1得到值就是原来的虚拟内存地址+4,如果是char *ptr 那么ptr + 1得到值就是原来的虚拟内存地址+1……,那么通过简单的加减运算就能完成虚拟内存的遍历,如果对应的空间正好是一个数组空间的话,那么也就等价完成了数组的遍历

也就是说一个星号的指针既可以看作是一个简单的指针也可以看作一个数组的起始位置,这取决于如果初始化对应的空间,但是指针的本质就是使用对应变量存储了一个8个byte大小的代表了虚拟内存地址的值,视作数组只是因为指针的加减运算

当存在多个星号的时候,我们可以简单地这么理解:有几个星号就将对应变量看作一个几维数组,底层存了星号左边的具体类型,比如int ***ptr 就可以将ptr看作是一个三维数组,最底层存放了类型为int的值,又由于一个星号本质上是一个指针,所以int ***ptr 又可以看作是一个指向最底层存放了类型为int的二维数组的指针,至于该指针能否看作是数组(也即将变量看作三维数组)取决于如果初始化对应的空间,另外对于以void作为最底层类型的多重指针,比如void ***ptr往往不会把ptr看作是存放了void类型的三维数组,因为void类型没有任何的意义,只有void *类型才有意义(可以把它看作一个指向任意类型的指针,但是需要进行指针强制转换,类似Java的Object),因此void ***ptr往往看作是一个二维数组,数组底层存放了可以指向任意类型数据的指针,同理也可以看作一个指向一维数组的指针

void ****解释

所以回到一开始的问题:void ****代表什么?

从之前的思路来看void ****可以看作是一个三维数组,数组最底层存放了一个可以指向任意对象的void *指针,这样理解没有问题,但是由于Nginx通过该成员变量存储了不同核心模块的配置信息,如:Http模块和Event模块,前者的配置信息还细分为Http,Server以及Location三大类,每一类对应一个数组,而后者就一大类,所以对于前者而言是一个三维数组,后者是一个二维数组,为了兼容性,Nginx统一使用了三维数组,只是对于Envent模块而言,它的第二维的数组只有一个元素,也即Nginx中的Event模块如果要获取最底层的void *元素,需要使用如下方式:void *element = (*conf_ctx[i])[k]或者void *element = conf_ctx[i][0][k],以下是从网上截的一张解释conf_ctx四重指针的图,供参考:image.png

总结

对于C中的多重指针,在最开始可以简单地套用之前的公式——几个星号就是几重数组,从而形成一个最简单的印象,之后要多结合结合项目具体实现,考虑数组分配本身的动态性,来分辨每个星号代表的是指针还是数组,一般建议自外向内地进行解释,每解释好一个星号就去除一个星号

P.S. 在C语言中void *可以视作Java的Object,一方面它们都可以进行强制转换成其他类型,一方面C语言中都是值传递没有引用传递,因此需要使用指针来对标Java中的类型