C语言复杂声明的本质与局限
操作方法
- 01
先简单回顾一下C语言的独有的变量声明方式。自诩使用C语言多年,却一直对于C的复杂的变量声明方式头皮发麻,直到看到VCZH大神前不久的大作,才恍然大悟。惭愧,因此下面的内容颇有拾人牙慧之嫌,但为了引出后面一系列关于语言的随笔,也没办法了,本文的荣誉都归于vczh大神。就从最简单的说起。 int a; // 说明表达式a的值是int型,a自己本身也是int型,这不是废话吗? int array[N]; // 于是,表达式array[n]的值为int型,array是int数组,是否废话的味道少了一点? int *pA; // 显然,*pA的值为int型,而pA的类型是指向int的指针。 int fun(int x, int y) // 毫无疑问,表达式fun(a,b)的值为int型,fun则是函数,其函数签名是…… 通过前面例子,说明一个道理,可以从另外一个角度来理解C变量的类型声明,先确定整个表达式的结果值的类型,再考察变量本身的类型。就好比以上几个例子,a(单独一个变量都是表达式), array[n], *pA, fun(a,b)这些表达式都是int型,定义变量的语句的类型,其实就是为了说明这个语句的变量的整个表达式的结果的值的类型。 好了,请深呼吸,开始重口味了,下面的注释,其实都是废话。 int *fuck[N]; // *func[n]的类型为int,因此,func[n]的结果类型为int*,因此,func的类型为数组,数组的元素为int的指针 int (*pfuck)(int x, int y) // (*pfuck)(a, b)的结果类型为int,看到(*pfuck),括号内出现一元操作符*,此物为求得指针的所指之内容,然后,此内容还能进行函数调用,因此可知,pfuck为指针,指向一函数,该函数的签名是……。当然,表达式pfuck(a, b)也可以得到相同的结果,但是,为了强调pfuck的类型,请坚持使用(*pfuck)。 int* (*pfuck)(int x, int y) // *(*pfuck)(a, b)的值为int,pfuck的类型自然是函数指针,函数签名是有两个int型的参数,其返回值是int* int (*func[5])(int *p); // 毋庸置疑,(*func[i])(int *p)的结果是int型。它表示先获取数组的一个元素,对元素解引用,进而函数调用。显然,func为长度5的数组,数组元素是函数指针,函数有一int*行的变量,返回值是int型。 int *(*func())(); // 心里发麻是不是,要淡定。不管怎么样,*(*func())()的结果始终都是int值,是不是?从最外围上看,*(...)(),此乃一函数调用,然后对返回值解引用得到的值为int。我们知道,C语言中,只有两物可进行函数调用的操作,或函数,或函数指针,两者必居其一。有以上例子分析可知,*(*func)()此乃对函数指针的函数调用结果求指针值。现在,又有*(*func())();,括号内的*func(),分明就表示func的函数调用,此函数的返回值为指针。结合最外层的函数调用,此返回值指针指向一函数,也就是说,返回值是函数指针。因此表达式*(*func())(),涉及到两个函数调用,它表示内层的函数调用返回函数指针,而此函数指针再调用一次,其结果为int*,再用上指针*运算符,整个表达式的值就为int了。因此,func是一函数,此函数返回函数指针,函数指针指向一个无参而返回值为int*的函数。曲折离奇,大功告成。 好了,该反过来想了,如何从变量的类型来构造其定义语句。好比,“fuck指向一个数组,其个数为5,数组元素为函数指针,函数签名为带一个(int *p)参数,返回结果是int”。 先考虑如何使用此变量。既然fuck是数组指针,那么,*fuck就是返回其所指向的数组,然后要得到数组的元素,自然理所当然必须用到[]操作符了,因此,就得到,(*fuck)[i]了,注意,千万切记,必须加括号,否则,*fuck[i]意味着fuck自己本身就是数组了。自己本身是数组,和指向数组,也即,数组和数组指针的差别,是相当大的,其差别之大就好像整型类型和整形指针类型。然后,必须不能忘记的是,一元操作符*就是取得指针的所指之物。 好了,总之,对于fuck,我们通过(*fuck)[i]得到数组元素。既然元素又是函数指针,进而就得到,(*(*fuck)[i])(pa),这个表达式的值为int。因此,答案就是,“int (*(*fuck)[5])(int *p);”。 代码写成这样子,真他妈的贱,尽玩文字游戏,写的人费心,读的人糊涂。这该死的C语言,shit! 文章突然长了,打住。不惜对完整的类型进行分离,以求得声明与使用的语法的高度一致性。C语言真是,真是精致得让人大倒胃口。 又:有时候,对于稍微复杂一点声明的常用类型,会经常出现重复的声明语法,特别是在函数指针的时候,为了拟补这种缺陷,或者说是痛苦,或者说是对于变量类型的重视,C语言提供了typedef的关键字。用以代表这种声明与使用的一致性的变量的类型。在前面的例子中看到,声明语句中的类型,只是说明变量采用这种表达式时,它的就是这种类型。好比,int *pArray[20],*pArray[i]的值为int型,但pArray却绝不是int型,为了取得pArray的类型,可借助typedef;具体的使用如下,typedef int* IntArray[20];,然后,IntArray pArray;以定义同样类型的变量。又好比上例,int *(*func())();这个函数声明好像让某些人难以理解,用上typedef化简一下,就可以重点很突出了: typedef int* (*FunFuck)(); // FunFuck代表无参返回值是int*的函数指针类型; FunFuck func(); // 作用相当于int *(*func())(),但含义更加鲜明。 可以看到,typedef的用法很简单,不过是在过去的表达式的前面加一个typedef而已。后话,typedef在C++的template中,扮演了非常非常重要的角色,特别是模板元编程MPL中,全部的类型演算全部压在它身上,其作用之大,简直是惊天地泣鬼神,没有了typedef,C++的template不过是普通简单的泛型编程,有了template对typedef的完善支持,其实就是在struct/class内部中支持typedef语句,就导致了tmp的横空出现,导致C++的template成为威力最恐惧,同时语法也是最恐惧的泛型语言,没有之一。 继续补充:加上const。以上对于复杂声明的理解,明眼人一看就知道仅仅是从右值的角度入手。要理解const,就必须不可不提到左值了。左值右值是C++中的基本概念,三言两语也说不清楚。最粗浅的看法,左值指可以被写入,也就是说能出现于赋值语句的左边;右值指可读,只能在赋值表达式的右边。当然,一般来说,左值往往可以当做右值来使用,可写往往意味着可读。而const的作用,就是将原本可写的东西,给整成只读的了。具体到表达式来说,就是某些表达式的值具备左右值,而const就是去掉了它的左值功能。举例说吧,还是从最简单说起。 int a; 表达式a的结果是int型,既是左值又是右值; const int a;,a返回的结果是int类型,但是此结果已不再可写了,也即是a不能放在赋值语句的左边了。 int const a; 可这样理解,先不理int const,a是一个变量,既可读又可写,const将a整成只读。int表示const a的结果类型。虽然,理解上这样,但对编译器来说,const int a;和int const a;都一样,都只是表达了同样的意思,a是一个整型常量。 const int *p;,*p结果为int型,加上const后,*p只能读了,所以,p是整形指针,其所指的内容只能读不能写,但p本身却可写。 int const *p;,则先强调*p的只读性,然后再说明*p为int型。其实,这两种写法的意思都一样。 int *const p;,const 紧挨着p,说明p本身只读。至于 int *,则表示*p类型为int,可读写。因此,p是整形指针,p本身不可写,但其所指的内容却可读又可写。说实在,不明白这样的指针变量有什么鬼用,实际的代码应该用的很少才是。 为了表达指针的只读纯洁性的概念,不仅指针本身不能写,连其指向的内容也不可修改,C++终于整出了下面这样伟大的代码。int const *const p;或者const int * const p;。C++的这种做法,俗称致力于解决臆想中的问题,因为const与指针的组合,实际上只有指针所指向的内容只能读很有意义,其他的,意义微乎其微。 可见,原本C的声明使用一致性的语法,遇上const之后,开始有点混乱了。当半路中杀出C++的引用之后,这种语法的一致性在C++中就不复存在了。C++的很多语言特性,都在不同程度不同角度,深度和广度上,形式和语义上,给C语法的精致性造成致命的各种各样的冲击。以至于,最后C++变成了有史以来很难很复杂超级变态恐怖的语言了,没有之一。