2013年5月13日星期一

C中指针的复杂用法及阅读技巧

在说明复杂指针定义之前,我们先补习一下基本的指针定义形式。

普通指针
定义:int *p;
这个是定义一个指向int类型数据的指针。但是这样不容易理解,我自己的理解方式如下:
其实,p是一个32位的数字(64位系统为64位,这里以32位),只不过这个数字具体的意思是个内存地址,所以不能进行数学意义上的加减乘除(地址的加减乘除,是以类型大小sizeof(int)为单位的)。
如果我们直接使用p,则是一串数字地址,如果我们想要p地址处的数据,就需要加一个*
因此,*p诞生了,意思是我要拿出地址p处的数据。
因此,p是一个单独的变量,里面存储的是地址。
因此,我们视觉上,可以这样写:
int* p;
即代表int*类型的变量p

使用:
既然p是个单独的变量,那么直接使用p,就是在操作地址。为了操作p地址处的值,我们使用*,这时候,*p作为一个整体,含义就与变量一样了。我们直接认为它是个普通的变量即可。修改p地址处的值:
*p = 1;


数组指针与指针数组
数组指针是一个指向数组的指针,指针数组是一个存储指针的数组。这仍然是个抽象的定义,我们来看看例子。

指针数组:
int *p[10];
根据上文提示的,我们可以先转为
int* p[10];
格式,这样我们就很好理解了,这是一个存储int*类型的数组。

数组指针:
int (*p)[10];
根据上文理解,由于(*p)带了括号,说明强制*p为一个整体,而*p作为一个整体,可以认为是个普通变量,那我们转换一下,将*p修改为a
int a[10];
这样就好理解了,*p=a,由于a就是个地址,所以,其实*p里保存的仍然是地址。那么,p就是个指向有十个int元素数组。
这还有一点,“int (*p)[10];”仅仅是定义了一个指针,p仅占4个字节。
因此,p必须初始化才能用,比如:
int (*p)[10];
int a[10];
p = &a;
这样p初始化后才可以用,否则p是个随机数。

扩展1
这里有一个问题,a其实是个地址,那能不能直接:
p = a;
如果这样写,gcc会有警告,类型不匹配。这其实涉及到普通数组的实质。a确实存储了一个地址,但a的类型却不是指针(在这里sizeof(a) = 12而不是4),这就类似于:
long i = 4;
int j = 4;
ij存储的都为4,但ij是不同的类型。gcc编译时,就会警告类型不匹配。因此,a&a的类型是不一样的(在这里sizeof(a) = 12, sizeof(&a) = 4)。但他们存储了相同的内容。

扩展2
我再附上上面的例子:
int (*p)[10];
int a[10];
p = &a;
然后,“*p[1]”与“(*p)[1]”一样吗?这里涉及到*的优先级,其实*的优先级很低,不如“[]”,因此,*p[1]实际为:
*(p[1])
那么,p[1]是个什么东东呢?写程序验证一下会发现,p[1]代表了另一个跟a一样结构的数组,这个数组紧跟在数组a之后,由于我们没有定义这个数组,因此,在p[1]这个数组中全部是乱码。从内存的角度来说,如果p地址为0a数组大小为12字节,那么p[1]代表地址12p[2]代表地址24。这样做有什么意义呢?在二维数组中很有意义,在二维数组中,p[N]代表第N行的首地址。

扩展3
看以下代码正确吗?
#include <stdio.h>
int main() 
{
    int a=3, b = 5;

    printf(&a["Ya!Hello! how is this? %s\n"], &b["junk/super"]);
     
    printf(&a["WHAT%c%c%c  %c%c  %c !\n"], 1["this"],
        2["beauty"],0["tool"],0["is"],3["sensitive"],4["CCCCCC"]);
         
    return 0; 
}
这段代码是正确的,输出如下:
Hello! how is this? super
That is C !
为什么呢?本例主要展示了一种另类的用法。下面的两种用法是相同的:
"hello"[2]
2["hello"]
如果你知道:a[i] 其实就是 *(a+i)也就是 *(i+a),所以如果写成 i[a] 应该也不难理解了。


函数指针
函数指针在感官上比较复杂,但实际上仍然是有规律的。首先我们要明白,函数名的本质是个指针常量,是的,你没看错,它和数组名是一样的道理。比如sizeof(function) = 1,如果用printf打印function,则是函数的入口地址。如果我们理解了数组指针,那我们同理可以推出函数指针。只不过函数指针在书写格式上有些特别,比如以下:
void (*fun)();
我们仍然需要将*fun用括号括起来,因为默认*是与void结合的,如果不用括号,则变成如下:
void* fun();
代表一个返回值为void*fun函数。

带参数的函数指针
先看例子,如下:
int (*fun)(int, int);
参数不需要名称,只需要指定类型即可。再来一个复杂点的:
int (*fun)(int, int (*)(int));
int (*)(int)是函数的一个参数,表示带一个int参数并且返回值为int的函数指针。中间括号里只有一个*可能让大家不习惯,其实我们上面说过了,我们只需要指定类型,不需要名字,本来*后面是放一个名字的,这里去掉了,就变成了*。当然,我们也可以画蛇添足,把名字加上:
int (*fun)(int a, int (*b)(int));

返回值为函数指针的函数指针
那这里有个问题,如果我们需要定义一个函数指针,该函数返回的是另一个函数的指针,该怎么定义呢?是这样吗:
(void (*)(int)) *fun(int, int)
放到gcc里,发现编译错误。
实际上,C采用了一种很难理解的方式来处理这种返回函数指针的情况,不过格式是固定的。在阐述“返回值为函数指针的函数指针”前,我们先讨论一下,如何定义一个“返回值为函数指针的函数”

根据上面的经验,以下函数定义明显不对了:
(void (*)(int)) fun(int a, int b) {
...
}
那什么格式是正确的呢?我们需要将整个“fun(int a, int b)”搬迁到“(void (*)(int))”。如下:
(void (*fun(int a, int b))(int))
做这样的修改后,我们不需要大括号了,去掉大括号:
void (*fun(int a, int b))(int)
因此,最终代码为:
void (*fun(int a, int b))(int) {
...
}
很绕吧。但是C就是这么定义的,呵呵。有几个小细节,上面是*fun而不能是(*fun),这个其实很好理解,我们以一个返回普通指针的例子做类比:
int* fun2(int c, int d) {}
fun(int a, int b)”是一个整体,对应于“fun2(int c, int d)”。“*”对应于上面的“*”,“void (*...)(int)”整体即为上面的“int”。只不过由于“void (*...)(int)”这个类型把函数名给包起来了,所以看起来很绕。如果使用“(*fun)”就把“fun(int a, int b)”拆开了,没有任何意义。对比表:
int* fun2(int c, int d) {}
void (*fun(int a, int b))(int) {}
int*
void (*...)(int)
fun2(int c, int d)
fun(int a, int b)


知道了“返回值为函数指针的函数”,那我们再来探讨“返回值为函数指针的函数指针”。我再将上文错误的写法写一遍:
(void (*)(int)) *fun(int, int)
对于“返回值为函数指针的函数指针”,我们也需要将右边“*fun(int, int)”整个放到左边“(void (*)(int))”中,如下:
(void (*(*fun)(int, int))(int))
做这样的修改后,我们不需要大括号了,去掉大括号:
void (*(*fun)(int, int))(int)
注意1右边“*fun(int, int)”中的“*fun”我们加了括号,这主要是优先级问题,*优先级比较低,如果不加括号,默认含义为“*(fun(int, int))”,引起错误。
注意2上面的写法其实还是可以与普通的指针做类比的,例如:
int * p;
*fun(int, int)”是一个整体定义,对应于*p
 void (*...)(int)”是一个整体类型,对应于上面的类型int。如下表:
int * p;
void (*(*fun)(int, int))(int);
int
void (*...)(int)
*p
(*fun)(int, int)


更复杂的函数指针
直接上个例子:
void (*(*fun2_p2)(int, void(*)(int)))(int);
此类定义已经快逆天了(对的,是“快”,还没到,坚持住啊),不过我们按照前面的方法,还是比较容易理解的。首先看到这个结构,这肯定是个被“函数指针返回值”包裹的指针,因此,我们先按照普通指针的结构进行剥离:
int * p;
void (*(*fun2_p2)(int, void(*)(int)))(int);
int
void (*...)(int)
*p
(*fun2_p2)(int, void(*)(int))
可以看出,这是一个名字叫fun2_p2的函数指针,指向一个函数,这个函数返回一个函数指针,并且参数里也有一个函数指针。这个函数的定义是:
void (*fun2(int a, void(*b)(int)))(int) {
...
}
我们把这个函数定义拆一下:
int* fun(int a, int b)
void (*fun2(int a, void(*b)(int)))(int)
int*
void (*...)(int)
fun(int a, int b)
fun2(int a, void(*b)(int))


逆天前的准备:存储((返回值为函数指针的函数)的指针)的数组
还能坚持住不?先解释一下标题吧……
这是一个数组;
这数组存储指针;
这些指针指向函数;
这些函数的返回值是函数指针。
上例子:
void (*(*fun_p[4])(int, void(*)(int)))(int);

我们以返回int型指针的函数的指针数组做类比:
int* (*p[4])()
void (*(*fun_p[4])(int, void(*)(int)))(int)
int*
void (*...)(int)
*p[4]
*fun_p[4]
()
(int, void(*)(int))


逆天!指向(存储((返回值为函数指针的函数)的指针)的数组)的指针
好吧,你到这了,我就不卖关子了,关门放狗:
void (*(*(*fun_arr_p)[10])(int, void(*)(int)))(int);
我们不得不再解释一下标题先:
这是一个名为fun_arr_p的指针;
它指向一个数组;
这数组存储指针;
这些指针指向函数;
这些函数的返回值是函数指针。

是的,这个指针可以指向上面那个准备逆天的数组!我们先从最外面剥皮:
1
void (*(*(*fun_arr_p)[10])(int, void(*)(int)))(int);
    =>
void(*...)(int);
好熟悉的格式,显然是个函数指针类型的返回值。说明里面定义了一个函数指针。
2
(*(*fun_arr_p)[10])(int, void(*)(int))
    =>
(*...[10])(int, void(*)(int))
这显然是在定义一个函数指针数组。可以跟简单点的函数指针数组做类比:
int (*fun[10])();
其中(*...[10])();就是上面的部分。
3
(*fun_arr_p)
*号,这显然是个指针定义,说明不是在定义函数指针数组,而是定义指向函数指针数组的指针。

乱了吧?实际上我在写上面内容的时候也纠结死了。我们再做一个类比吧,以下是一个普通的“指向函数指针数组的指针”:
int* (*(*p)[10])(); //写法:先写一个指针数组int* p[10],然后我们将p改为指针int* (*p)[10],这样就变成指向指针数组的指针
他只有四个字节,类比表:
int* (*(*p)[10])()
void (*(*(*fun_arr_p)[10])(int, void(*)(int)))(int)
int*
void (*...)(int)
(*...[10])()
(*...[10])(int, void(*)(int))
(*p)
(*fun_arr_p)
我花了两天才写到这里……不过我现在又晕了……
我们梳理一下:
普通的函数指针是这样的:void (*p)(int);
普通的函数指针数组是这样的:void (*p[10])(int);
普通的指向函数指针数组的指针是这样的:void (*(*p)[10])(int);
如果函数的返回值是函数指针类型int(*)(),那上面的指针是这样的:
int(*(*(*p)[10])(int))();
好吧,我只能写到这了。


C中对于逆天函数及函数指针的折中解决方法
如果C中全部是这种逆天的定义,那代码就直接没办法看了。后来C标准中出现了typedef,用来解决这种超复杂定义的问题。
typedef的作用是给类型定义别名,因此,我们可以把复杂的类型用简单的方式定义。例如:
对于函数:
void (*fun2(int a, void(*b)(int)))(int);
我们将其返回值类型定义一个别名:
typedef void (*HANDLER)(int);
这样,函数就可以定义为:
HANDLER fun2(int a, HANDLER b);
看,这样就简洁多了,而且看起来比较符合普通函数的定义。
对于函数指针:
void (*(*fun2_p2)(int, void(*)(int)))(int);
我们可以写为:
HANDLER (*fun2_p2)(int, HANDLER);
是不是简单了许多呢?注意哦,*fun2_p2的括号仍然不能去,优先级问题,不加括号会被理解为“*(fun2_p2(int, HANDLER))”,前面已经解释过了。

下面分析一下逆天的指针:
void (*(*(*fun_arr_p)[10])(int, void(*)(int)))(int);
结果:
HANDLER (*(*fun_arr_p)[10])(int, HANDLER);


强制转换为函数指针
(void (*)(int))fun”,这是强制类型转换的效果。


几个例子
1
(*(void(*)())0)();
首先,我们先判断最外面,显然是两个括号“()()”,这说明这句话应该是在调用某个无参函数。
然后我们分析“*(void(*)())0”,由于*的优先级比较低,所以代码可以理解为:
*((void(*)())0)
也就是“*(...)”的形式,这说明,整句代码是在用某个函数指针调用函数。
第三步,就是“(void(*)())0”了,这个很好理解了,将地址0强制转换为“(void(*)())”型指针。
最后,整句代码的意思就是调用地址0处的无参函数。


2
int *a;                  //指针
int **a;             //指向指针的指针
int a[10];           //数组
int *a[10];          //存储指针的数组
int (*a)[10];        //指向数组的指针
int (*a)(int);           //指向函数的指针
int (*a[10])(int);       //存储指向函数的指针的数组
int (*(*a)[10])(int);    //指向上面数组的指针


一段验证代码
上面说到的大部分内容的测试代码,可以很方便的验证结论。
#include<stdio.h>

typedef void (*HANDLER)(int);

void fun(int a) {
    printf("Hi, I'm fun! a = %d\n", a);
    return;
}

int* fun0(int* a) {
    return a;
}

//(void (*)(int)) (fun)(int a, int b) {
void (*fun1(int a, int b))(int) {
    printf("fun1 invoked. a = %d, b = %d\n", a, b);
    return fun;
}

void (*fun2(int a, void(*b)(int)))(int) {
    printf("fun2 invoked. a = %d, I will invoke your function and return the function\n", a);
    (*b)(a);
    return b;
}

int main() {


    int a[3] = {1, 2, 3};
    int (*b);
    int (*p)[3];
    printf("p = %p, sizeof(p) = %d, sizeof(a) = %d\n", p, sizeof(p), sizeof(a));
    p = a;
    printf("p = a\n");
    printf("&p = %p, p = %p, a = %p\n", &p, p, a);
    printf("p[0] = %p, *p[0] = %p, p[1] = %p, *p[1] = %d, sizeof(p[1]) = %d, (*p)[1] = %d\n", p[0], *p[0], p[1], *p[1], sizeof(p[1]), (*p)[1]);
    printf("**p = %d, *p = %p\n", **p, *p);

    p = &a;
    printf("p = &a\n");
    printf("&p = %p, p = %p, a = %p\n", &p, p, a);
    printf("p[0] = %p, *p[0] = %p, p[1] = %p, *p[1] = %d, sizeof(p[1]) = %d, (*p)[1] = %d\n", p[0], *p[0], p[1], *p[1], sizeof(p[1]), (*p)[1]);
    printf("**p = %d, *p = %p, *a = %d\n", **p, *p, *a);

    int i = a;
    printf("i = a, i = %d, sizeof(a) = %d\n", i, sizeof(a));
    i = &a;
    printf("i = &a, i = %d, sizeof(&a) = %d\n", i, sizeof(&a));

    printf("sizeof(fun) = %d, fun addr = %p\n", sizeof(fun), fun);
   
    void (*fun_p)(int);
    fun_p = &fun;
    (*fun_p)(1);
   
    int* (*fun0_p)(int*);
    fun0_p = &fun0;
    i = 0;
    printf("fun0: I'm return int point. a = %d\n", *(*fun0_p)(&i));
   
    void (*(*fun1_p1)(int, int))(int);
    fun1_p1 = &fun1;
    fun_p = (*fun1_p1)(2, 3);
    (*fun_p)(4); 
   
    void (*(*fun2_p2)(int, void(*)(int)))(int);
    fun2_p2 = &fun2;
    (*fun2_p2)(5, fun)(6);
   
    HANDLER (*fun1_p3)(int, int);
    fun1_p3 = &fun1;
    fun_p = (*fun1_p3)(7, 8);
   
    HANDLER (*fun2_p4)(int, HANDLER);
    fun2_p4 = &fun2;
    (*fun2_p4)(9, fun)(10);

    void (*(*fun2_arr[4])(int, void(*)(int)))(int);
    for (i = 0; i < sizeof(fun2_arr) / sizeof(fun2_arr[0]); i++) {
       fun2_arr[i] = &fun2;
       fun2_arr[i](i, fun)(i);
    }

    void (*(*(*fun2_arr_p)[4])(int, void(*)(int)))(int);
    fun2_arr_p = &fun2_arr;
    for (i = 0; i < sizeof(*(fun2_arr_p)) / sizeof((*fun2_arr_p)[0]); i++) {
       (*fun2_arr_p)[i] = &fun2;
       (*fun2_arr_p)[i](i, fun)(i);
    }

    void (*fun_arr[10])(int);
    void (*(*fun_arr_p)[10])(int);
    fun_arr_p = &fun_arr;
   
    int(*(*(*fun_arr_p1)[10])(int))();
    //int (*fun_p2)(int a, int (*b)(int));
    //void (*fun(int, int))(int);
   
    return 0;
}


再专业一些的内容可以参见《C陷阱与缺陷》这本书,讲的很好。

没有评论:

发表评论