由typedef和函数指针引起的危机
由typedef和函数指针引起的危机
昨天阅读了大神强哥的代码,发现里面用到了函数指针,也用到的typedef。本来我自以为对这两个概念有一定的认识,但是突然发现这两个东西居然用到了一起!!!!(在一起了也不说一声,一点心理准备都没有):
typedef int (* fp)(void *para, void *end);
瞬间就蒙了,这是个啥东西???于是我开始看书,上网查资料,想弄明白。在这个过程中,我发现自己不仅仅是对这两个概念理解不够!!!而是,对数组、指针、变量的理解都不够。这引发了我对C语言认识的危机。想想看,一直在写C程序,突然有一天发现,你对它的认识始终是模糊的,你所做的事都是建立在这种模糊的认识之上的,可不可怕!!!!
有点类似于数学危机呀!
我希望我能够像数学的发展那样,在矛盾中发展,在危机中升华。
下面就记录一下,我在解决函理解这个东西:
typedef int (* fp)(void *para, void *end);
的过程中的一些心得体会。
心得1:由变量名到地址,可以理解为一种退化。而指针就是地址。
在我之前的一篇随笔中,我简单写了一下对地址和变量名的理解。
http://www.cnblogs.com/qingergege/p/6723509.html
这里再简单谈一下。
我觉得:
变量名可以认为代表一个结构。代表什么结构呢?学过编译原理的同学都知道,有一个概念叫做符号表。变量名就代表了这个符号表中的某一项。至于符号表是什么样,我不知道,但是大概就类似于下面这个(其中的一条记录):
变量名 |
首地址 |
类型 |
空间大小 |
值 |
a |
0x3333 |
int |
4字节 |
10 |
编译器用一个叫符号引用的东西来访问符号表中的记录。
这个符号引用就是变量名。
看上面一条记录,可以发现其中有一个字段叫首地址,而上面说了变量名可以代表整个结构,换句话说就是,地址仅仅是变量名的一部分,或者从另一个角度讲,变量名是一个受到限制的地址,因为指针就是地址,这句话也可以说成:变量名是一个受到限制的指针(受什么限制呢?就是受到上面那条记录中其他字段的限制)
学习C语言的同学都有体会,指针是一个让人又爱又恨的东西。爱她是因为她使用方便,效率高;恨她是因为她太不安全了。她的不安全在于她几乎可以不受任何限地访问内存。
比如说void * p; 你就可以给她分配你想要的大小的空间。
p = malloc(你想要的大小)。
而且通过p可能会访问到未分配的空间,比如你执行p++,也许p原来指向一片有意义的内存,但是p++之后呢?就不一定了。
但是变量名就没有这个问题,看看上面那条记录,已经限定了a能够访问的内存的首地址是0x3333,能够访问的内存大小就是4字节。这些都是受限制的,但是单单给一个指针,也就是个首地址就没有这些限制。所以可以说,变量名是个受限制的地址。学c++的时候,老师在讲引用和指针的区别时,经常会说,引用就是个受限制的指针。那引用是什么?比如 int &b = a, b和a就是一样的,上面那条记录可以用a进行访问,现在通过b也可以访问了,就是这么回事儿。
而从变量名到地址,实际上就是从整个结构退化到结构中的“首地址”字段,而这个过程是通过取地址符号&实现的!
心得2:数组名不是指针。
记得当大一学C语言那会儿,老师就说,数组名就相当于个指针。现在看来数组名也仅仅是“相当于”个指针而已。
为什么我们会认为数组名是指针呢?答案很简单,他俩太像了!为什么他俩这么像?答案:都是编译器“惹的祸”。
可能有两个用法导致了我们对“数组名就是指针”这个观点深信不疑。
第一个就是数组名可以赋值给一个指针,比如
int a[3];
int *p = a;
第二个是用指针也可以向用数组名那样,使用下标访问数组中的元素,比如:
p[2]和a[2]是等价的。
基于上面两种用法,很难让我们不相信“数组名就是指针”。
这一切都是编译器的锅!!!
当我们使用赋值符“=”,真的是仅仅执行了赋值语句这么简单吗?或者到汇编层,真的只是几个mov语句吗?一定不是的!
比如说:
int main()
{
int a = 100;
short b;
b = a;
printf("b = %d\n",b);
getchar();
return 0;
}
当将a的值赋值给b时,一定是存在一个类型转换的,应该是
b = (short)a;
但是我们并不需要显式地强制类型转换,这是因为编译器为我们做了。就像Java中即使我们不写构造方法,创建对象的时候也会调用构造方法,因为编译器会为我们生成一个默认的构造方法(编译器不容易啊,知道我们懒,活都帮我们干了)。同样,当我们将一个数组名赋值给指针时,编译器在私下也帮我们做了大量的工作。
上边那句 int *p = a。实际上编译器帮我们转换了,转换成类似
int *p = &a[0];这样的语句。看起来好像是将一个数组名赋值给了指针,实际上底层还是讲数组的首元素的地址赋值给了指针!!
再来说说通过下标访问数组元素的方法,使用的是下标运算符[]。
在我们看来就是通过下标访问的元素呀,但实际上编译器会将[]运算符转换成指针运算,比如a[2] + 1 ,在底层就是类似于*(a + 2) +1。有一个写法可以从侧面证明这一点:
我们知道 *(p + 2) + 1就是p指向的数组的第3个元素+1,这句话也可以写成p[2] +1, 当然因为a+b和b+a一样,所以也可以写成*(2 + p) + 1; 那么,神奇的事情出现了,也可以写成2[p]+1!!!!当时看到这种写法的时候,真的是颠覆了世界观!!这种写法也从侧面证明了,下标运算符[]实际上被编译器转换成了指针的运算!!
下面是程序源码和输出结果:
int tmain()
{
int a[4] = {1,2,3,4};
int *p = a;
printf("第三个元素为:%d\n",p[2] );
printf("第三个元素为:%d\n",*(p+2) );
printf("第三个元素为:%d\n",*(2+p) );
printf("第三个元素为:%d\n",2[p] );
getchar();
return 0;
}
正是由于编译器为我们做了这么多,才让数组名看是来像是一个指针!!
还有一个问题就是数组名可以赋值给指针,但是指针不能赋值给数组名,很多人给出解释是因为数组名是个指针常量,而常量的值是不能改变的。。但是上面已经解释了数组名根本就不是指针,更不用说是指针常量了!!变量名之所以能够赋值给指针变量,是因为编译器做了优化,但是这是变量名在=左边的情况呀!
根据心得1,数组名也是个变量名,那么它也可以理解为一个首限制的指针,而从变量名转换成指针是需要取地址运算&的。事实 情况也是如此:编译器将int *p = a 优化成了 int *p = &a[0]
而&a就表示的是数组a的地址!这就要谈心得3了。
心得3:定义一个类型的指针变量的方法,就是先定义出该类型的普通变量,然后在变量名前加上*即可,但是要注意运算符的优先级。
上面的话肯能不太好理解,下面就是例子:
比如说我想定义一个指向int类型的指针,那么我就可以先定义一个int类型的变量,然后再在变量名前面加上*即可。
如:
int a; ---》 int *a;
同样如果我想定义一个指向int数组的指针,我可以先定义一个int类型的数组,然后再在数组名(变量名)前加上*
int a[5] -----》int (*a)[5]
这里要加括号是因为[]的优先级比*高。
这里可能有人会疑惑,指向一位数组的指针不应该定义成int *a就好了吗?
这里需要解释的是int *p = a这种写法中p实际上指向的数组中的元素,而不是整个数组,上面心得2中提到int *p = a会被编译器优化成int *p = &a[0],所以这种定义下p指向的是数组中的元素,而不是整个数组,这也是为什么p++会指向下一个元素。
而int (*p)[5]这种定义方法,p指向的是整个数组,而p++则是指向下一个数组(如果合法的话)
发现没有:
int a;
int *p =&a;
int a[5];
int (*)p[5] =&a;
都是有套路的!!!!
同样对于函数指针也一样:
int print(char *a); --》 int (*print)(char *)
看到没有也仅仅是在函数名前面加上一个*即可(加括号是由于优先级的问题),只不过通常我们会把函数指针换个名字而已,比如
int (*p)(char *)
就是把print换成了p而已呀。
问题来了,前面都是通过取地址得到指向相应类型变量的指针,为什么我们在给函数指针赋值的时候不用&符呢?比如:
int print(char *);
int (*p)(char *);
p = print;
为什么不用 p = &print呢?
还记得将数组名赋值给指针变量吗?没错!还是编译器的“锅”,编译器帮我们做了优化,p = print会被编译器优化成p = &print,
而且我们就写p = &print也没有任何问题!!!
比如下面代码和运行结果:
int print(char *a)
{
printf("值为:%s\n",a);
return getchar();;
}
int main()
{
int (*p)(char * a);
p = &print;
p("阿星");
return 0;
}
心得4:定义某种类型的变量,在前面加上typedef就得到了该变量的类型。
比如 定义int类型的变量a,
int a;
如果前面加上typedef 那么a不再是变量,而是变量的类型!!
typedef int a; 那么a就相当于int。
同样 我们定义一个数组变量:
int a[5];
加上typedef之后 a就变成了有五个元素的数组类型。
typedef int a[5];
此时a就不再是变量了,而是类型!!升级了!!!
之后我们就可以用a定义数组变量了!!!
一个代码和运行结果
typedef int a[5];
int main()
{
a arr = {1,2,3,4,5};
printf("数组数据为:");
for(int i = 0; i < 5; i++)
{
printf("%d ",arr[i]);
}
getchar();
return 0;
}
看到了吗? 加上typedef之后小小的数组变量a就变成了类型(一步登天呀),然后他就可以定义变量啦。只不过通常情况下我们会把这个类型大写,而不是使用看是来更像是变量的a。
最后终于到了要解决引起我危机感的东西了,typedef加上函数指针变量。
函数指针变量
int (*p)(char *);
p本来是个指针变量,加上typedef这个皇冠就麻雀变凤凰了。就从一个变量名变成了能定义函数指针变量的类型名了。
看代码和运行结果
typedef int (*p)(char *s);
int print(char *s);
int main()
{
p pf;
pf = &print;
pf("阿星");
return 0;
}
int print(char *s)
{
printf("值为:%s\n",s);
return getchar();
}
也许我今天辛苦整理的心得,到了明天发现依然不够全面不够好。不过没关系,都是在不断完善中成长的!
水平有限,有纰漏之处还请指正。谢谢。。。。