2013年5月27日星期一

Word2013撤销功能无法使用的解决方法


最近Word2013出现了一个奇怪的问题,撤销按钮一直是灰色。做编辑时闪一下,又变为灰色。按Ctrl-Z也不管用。网上查询,得知解决方法(但是未找到确切原因):
关闭Word2013主程序,然后Win+R调出运行窗口,执行Winword.exe /safe,然后关闭窗口,再正常打开文档,发现撤销功能又回来了。
参考:

2013年5月20日星期一

gdb常用调试命令


第一步:如果用gdb调试,需要在gcc编译时添加调试符号,要用-g参数。例如:gcc test.c -o test -g
第二步:使用gdb ./test载入程序。这时GDB会载入test但不会执行。
第三步:使用以下命令进行调试,刚开始一定要先设置断点,否则直接运行完毕了。


直接回车 执行上次命令(单步调试时很有用)
l 10 查看第十行上面五行和下面五行代码
b 6 第六行设置断点(行号可以用l查看到)
b main 在main函数设置断点
info b 查看断点列表
r 运行代码
p n 查看变量n的值
n 单步调试next(单步跳过)
s 单步调试step(单步进入)
c 恢复运行
finish 执行到返回
help [command] 查看命令帮助
q 退出gdb
set args 指定运行时参数(如:set args 10 20 30 40 50)
show args 查看设置好的运行参数

2013年5月18日星期六

深入研究C语言中.h和.c文件的关系



一些基本概念
在进入主题之前,我们先了解一些基本内容。
C语言是面向过程的编程语言,C语言没有包(命名空间),没有明确的私有和公有函数,一旦你定义了函数,理论上可以被整个程序调用(这里先不讨论折中的static类型的函数)。不论你编辑了几万个还是几十万个.c源程序,最终经过编译,会变成单个二进制可执行文件。在这个文件里所有函数都是平等的,没有公私之分。如果反汇编,可以看到这些函数按顺序一个接一个的排在代码段,并且入口地址是固定的。Linux内核也不例外,Linux内核其实就是一个巨大的二进制可执行文件。
由上可以得出,.c文件的多少,只是根据人的“管理难易程度”决定的。如果你不介意,你可以将所有的程序都写在一个.c文件中,这样反而提高了gcc的效率,gcc不用再去拼接.c文件了。
对于特殊的static类型的函数,它的特殊之处在于,它并不是保存在代码段,而是保存在静态区,因此造成了它的特殊性,这里不再展开讨论。


函数定义是有顺序的
这个概念比较简单,但也是万恶之源。对于标准C来说,函数有先后顺序。如果B函数定义在A函数之后,那么在A中不能直接调用B(对于编译器来说,可以成功编译运行,但会有警告),因此需要在A之前先声明B函数(关于声明与定义的区别见后文)。例子:
test.c
#include <stdio.h>

int main() {
        A();
        return 0;
}


void A() {
        printf("I'm function A.\n");
        B();
}

void B() {
        printf("I'm function B.\n");
}

编译器警告:
test.c:9:6: 警告:与‘A’类型冲突
test.c:4:2: 附注:‘A’的上一个隐式声明在此
test.c:14:6: 警告:与‘B’类型冲突
test.c:11:2: 附注:‘B’的上一个隐式声明在此
换句话说,标准C不会自动寻找函数,默认仅仅会查找已处理过的函数,没查到就认为违反标准了。还是上面的例子,编译器处理到A时,B还没有处理到,结果A中有对B的调用,就警告了。由于标准C在这方面的规定,就有了这一篇文章。
声明与定义的区别:声明仅仅是告诉编译器有这个东西,是一种描述,不会占用内存。定义则会根据声明的结构,产生实际的内存占用。换句话说,定义是对声明的一个实现。这就类似于面向对象中类与对象的关系。


解决函数定义的顺序问题
既然函数定义是有顺序的,那我们写代码就非常麻烦了,写一个A函数还得提心吊胆的看看将要调用的B函数是不是在A前面,一旦函数增多,程序员必然崩溃,这比goto语句都来的爽。当然,你也可以放任自流,结果会是换来编译器的一大堆警告。
为了解决这个问题,我们可以将所有的函数生命在.c文件的头部。这样我们在写代码时,想怎么调用就怎么调用了。刚才的程序我们可以这么写:
test.c
#include <stdio.h>
void A();
void B();

int main() {
        A();
        return 0;
}


void A() {
        printf("I'm function A.\n");
        B();
}

void B() {
        printf("I'm function B.\n");
}
因此,我们写完函数后,顺便在头部加个声明。这样在整个.c文件中都可以调用了。

显然我们不满足于在单个文件中无忧无虑的调用,我们还会在其它文件中调用。但是如果直接在其它文件中调用B,仍然有警告(GCC虽然可以编译通过并运行,但这种隐式声明比出错更隐蔽,更不容易发现问题)。

当然,我们不止这么一个问题。还有,如果我们在A中声明了一个结构体,那么在B中无法使用。如果想要在B中使用,需要在B中重新声明。如果我们定义了一个全局的结构体,那我们需要在每个.c文件中声明一遍。


.h文件的由来
上面提了两个问题,这正是.h文件要解决的。.h文件中主要包含的就是函数声明、结构体声明、宏定义。那.h文件是如何解决上文两个问题的呢?实际上,编译器在处理.h文件时,仅仅是将.h文件复制到.c文件的头部,这样我们就不难理解了。一旦将.h文件复制到.c中,那在.c中使用函数、结构体、宏定义都自由了,因为已经声明过了。
这也是为什么在.c文件中会加入对自己.h文件的调用,例如:
test.h
void A();
void B();
test.c
#include <stdio.h>
#include "test.h"

int main() {
        A();
        return 0;
}


void A() {
        printf("I'm function A.\n");
        B();
}

void B() {
        printf("I'm function B.\n");
}
加粗部分会被test.h的内容覆盖,这样可以自由调用test.c中的函数而不用担心顺序问题。
我们也可以很方便的在其它.c文件中使用test.c的函数,我们只需要包含test.h即可。例如:
test2.c
#include "test.h"

void C() {
        B();
}
这样,编译器会把test.h中的内容复制到test2.c的头部,然后再编译。这相当于在test2.c中声明了test.c中的函数,这样再使用test.c中的函数就不会出现“隐式声明”的警告了。如果test.h中还有结构体的声明,那么在test2.c中也可以自由使用test.h中的结构体。可以看出,.h文件的存在,就是为了解决函数定义顺序问题以及不同.c文件间的函数调用问题。下面还会证明,在编译器看来.h文件与.c文件没有区别。


#include "..."#include <...>的区别
他俩区别主要在于编译器搜索路径不一样。引号一般是引用用户自己的头文件,编译器会搜索用户目录。尖括号一般是引用系统提供的头文件,编译器会搜索系统头文件目录。
还有一点,对于大型工程来说,用户的头文件可能会分别放在不同的子目录下,引号可以加相对路径或者绝对路径来指定头文件的位置,例如:
#include "arch/test.h"
这样制定的是源码根目录下的arch目录下的test.h头文件。


.h.c必须在一起吗?
.h文件不一定非要和.c文件放在一起。上文我们说过,编译器只是将.h文件内容拷贝到.c文件中,我们大可以将他们分开。很多程序都是.h一伙,.c放到另一伙的。只要我们include时指定的位置正确即可。


后缀名必须是.h吗?
这是约定俗成的东西,实际上编译器不会根据后缀名区分代码文件,在编译器里,这些文件都是文本文件,性质都是一样的。你甚至可以把头文件命名为.c,代码文件命名为.h,然后在.h文件中“#include "xxx.c"”。
这样,我们就对.h文件有了更进一步的了解。实际上,.h文件只不过是程序员们约定的东西,用来方便大家写代码和集成代码。解决我们前面提到的那些不方便(不用在每个.c中重复声明某个函数、结构体或宏定义)。


为什么.h里尽量不要放函数的定义?
这点主要是因为,大家约定好.h用来声明函数,并且知道头文件可能会被很多不同的.c代码文件包含。如果.h文件包含函数定义,那么两个及以上的地方引用了这个头文件,就会出现在两个及以上的地方都包含了相同的函数定义,这属于重复定义了。例如:
test.h
void C() {
    return;
}
test1.c
#include "test.h"
void A() {
...
}
test2.c
#include "test.h"
void B() {
...
}
编译器合并这些.h.c文件,会产生如下的效果:
void C() {
    return;
}

void A() {
...
}

void C() {
    return;
}

void B() {
...
}
显然C重复定义了。

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陷阱与缺陷》这本书,讲的很好。