大家好,欢迎来到IT知识分享网。
通常认为C语言的指针在C语法中是比较复杂的,特别是其与其它语法复合到一起时。应该说,C语言的声明,特别是其涉及到指针、数组、函数等三者的复合声明时,其复杂度更高。而当返回函数指针或数组指针时,其写法涉及到类型和符号”()”、”[]”,通常会出现一种“分裂”的写法,类型写在声明的最前面,符号”()”、”[]”写在最后面。
C语言的声明模型之所以如此晦涩,这里有几个原因。六十年代晚期,人们在设计C语言的这部分内容时,“类型模型(typemodel)”这个概念对于当时的编程理论而言尚属陌生。BCPL语言(C语言的祖先)几乎没有类型,它把二进制字作为惟一的数据类型,所以C语言先天有缺。然后出现了一种C语言设计哲学,要求对象的声明形式与它的使用形式尽可能相同。一个int类型的指针数组声明为int*p[3]; 并以*p[i]这样的表达式引用或使用指针所指向的int数组,所以它的声明形式和使用形式非常相似。这种做法的好处是各种不同操作符优先级在“声明”和“使用”时是一样的。它的缺点在于操作符的优先级(有15级或更多,取决于你怎么算)是C语言中另外一个设计不当、过于复杂之处。程序员需要记住特殊的规则才能推断出int*p[3]到底是一个int类型的指针数组,还是一个指向int数组的指针。
“声明的形式和使用的形式相似”这种用法可能是C语言的独创,其它语言没有采取这种方法。而且,“声明的形式和使用的形式相似”即使在当时也不像是一个特别好的主意。把两种截然不同的东西做成同一个样子真的有什么重要意义吗?贝尔实验室的学究们也承认此批评有理,但他们坚决死扛原来的决定,至今依然。
1 理解 C 语言声明的优先级规则
声明从它的名字开始读取,然后按照优先级顺序依次读取。
优先级从高到低依次如下。
1.1 声明中被括号括起来的那部分。
1.2 后缀操作符:括号 () 表示这是一个函数、方括号[]表示这是一个数组。
1.3 前缀操作符:星号*表示“指向……的指针”。
也就是所谓的“右左法则”,首先从未定义的标识符开始,然后往右看,再往左看。每当遇到圆括号时,就应该调转阅读方向。一旦解析完圆括号里面所有东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。
如果 const 和(或)volatile 关键字紧跟类型说明符(如 int、long 等),那么它作用于类型说明符。在其他情况下,const 和(或)volatile 关键字作用于它左边紧邻的指针星号。
比如用上面的规则去解读文章开头的char * const *(*next)();
1 首先,看变量名 next,并注意到它直接被括号所括住。
2.1 所以先把括号里的东西作为一个整体,得出“next 是一个指向……的指针”。
2 然后考虑括号外面的内容,在星号前缀和括号后缀之间作出选择。
2.2 规则告诉我们优先级较高的是右边的函数括号,所以得出“next 是一个函数指针,指向一个返回……的函数”。
2.3 再次,处理前缀“*”,得出指针所指的内容。
3 最后,把 char * const 解释为指向字符的常量指针。
所以一通解释完,可以知道这个声明表示“next 是一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为 char 的常量指针”
C语言的声明所存在的最大问题是,你无法以一种人们所习惯的自然方式从左向右阅读一个声明。在ANSI C引入volatile和const关键字后,情况就更糟糕了。由于这些关键字只能出现在声明中(而不是使用中),这就使得现今声明形式和使用形式能完全对得上号的例子越来越少了。那些从风格上看像是声明,但却没有标识符的东西(如形式参数声明和强制类型转换)看上去显得滑稽。如果想要把什么东西的类型强制转换为指向数组的指针,就不得不使用下面的语句来表示这个强制类型转换:
char (*j)[20]; /* j是一个指向数组的指针,数组内有20个char元素 */ j = (char (*)[20]) malloc(20);
如果把星号两边看上去明显多于的括号去掉,代码会变成非法的。
涉及指针和const的声明会出现几种不同的顺序:
const int * grape; int const * grape; int * const grape_jelly;
在最后一种情况下,指针是只读的。而在另外两种情况下,指针所指向的对象是只读的。
当然对象和指针有可能都是只读的,下面两种声明方法都能做到这一点:
const int * const grape; int const * const grape;
ANSI C提到typedef说明符之所以被称为“存储类型说明符”只是为了语法上的方便而已,它也不否认其中存在一些另外的问题。即使是经验丰富的C程序员也都觉得这里麻烦多多。
声明的核心是标识符以及与它组合在一起的任何指针、函数括号、数组下标等。
在合法的声明中存在限制条件。你不可以像下面那样做:
函数的返回值不能是一个函数,所以像foo()()这样是非法的 函数的返回值不能是一个数组,所以像foo()[]这样是非法的 数组里面不能有函数,所以像foo[]()这样是非法的
但像下面这样则是合法的:
函数的返回值允许是一个函数指针,如: int (*fun())() 函数的返回值允许是一个指向数组的指针,如:int (* foo())[] (以上就是所谓的“分裂”写法。) 数组里面允许有函数指针,如:int (* foo[])() 数组里面允许有其它数组,如:int foo[][]
2 返回函数指针的函数声明
int (*func)(int *p); /* 例1 */
首先找到未定义标识符func,它的外面有一对圆括号,而且它的左边有一个*,所以func是一个指针;
跳出这个圆括号,看右边,也是一个圆括号,说明(*func)是一个函数,而func是一个指向函数的指针,这类函数有一个int *类型的形参;
跳出这个圆括号,看左边,是一个int,说明这类函数返回的是一个int类型的值。
int (*func)(int *p, int (*f)(int *)); /* 例2 */
未定义标识符func,它外面有一对圆括号,而且它的左边有一个*,所以func是一个指针;
跳出这个圆括号,看右边,也是一个圆括号,说明(*func)是一个函数,而func是一个指向函数的指针,这个函数具有int * 和 int (*)(int *)这样的形参;
跳出这个圆括号,看左边,是一个int,说明这类函数返回的是一个int类型的值。
int (*func[5])(int *p); /* 例3 */
未定义标识符func,看右边,有一对方括号,说明它是一个具有5个元素的数组;
看左边,是一个*,说明数组的元素是指针;
跳出这个圆括号,看右边,也是一对圆括号,说明数组中的元素是函数指针,这类函数具有int *这样的形参;
跳出这个圆括号,看左边,是一个int,说明这类函数返回的是一个int类型的值。
int (*(*func)[5])(int *p); /* 例4 */
未定义标识符func,看左边,是一个*,说明它是一个指针;
跳出这个圆括号,看右边,是一对方括号,说明(*func)是一个数组,一个指向数组的指针;
看左边,是一个*,数组的元素是指针;
跳出这个括号,看右边,是一对圆括号,说明数组中的元素是函数指针,这类函数具有int * 这样的形参;
跳出这个括号,看左边,是一个int,说明这类函数返回的是一个Int类型的值。
3 返回数组指针的函数声明
int (*(*func)(int *p))[5]; /* 例1 */
未定义标识符func,看左边,是一个*,说明它是一个指针;
跳出这个圆括号,看右边,是一对圆括号,说明func是一个指向函数的指针,这类函数具有int* 这样的参数;
看左边,是一个*,说明这类函数返回的是一个指针;
跳出这个圆括号,看右边,是一对方括号,说明这个返回的指针指向一个具有5个元素的数组;
看左边,是一个int,说明数组的元素是int类型。
int func(void)[5];/* 例2 */
未定义标识符func,看右边,是一对括号,func是一个函数,没有参数;
由于右边是一对方括号,所以函数返回的是一个具有5个元素的数组;
这个数组的元素是int类型。
int (*func(int i))[10];/* 例3 */
func(int i) 意味着调用func函数时需要一个int类型的实参。
(*func(int)) 意味着对函数调用的结果是一个指针。
(*func(int i))[10] 意味着对函数调用结果的指针是一个指向数组,数组大小为10。
int (*func(int))[10] 意味着对函数调用结果的指针指向的数组中,数组元素都是整型的。
但正如前面所说,如果我们使用类型别名,声明一个返回数组指针的函数的函数就看起来要优雅多了。
using arrT=int[10]; // 或者 typedef int arrT[10];
arr是大小为10的数组的一个别名,即相当于把int[10]看成是一种类型,该类型是一个整型数组,大小为10。
有了这,现在声明一个返回数组指针的函数时就是声明一个返回这样类型的指针了,于是很优雅的如下:
arrT* func(int i);
再看一个例子:
void (*signal(int sig, void(*func)(int)))(int); typedef void(*ptr_to_func) (int); /* 它表示ptr_to_func是一个函数指针,该函数 * 接受一个int参数,返回值为void。 */ ptr_to_func signal(int, ptr_to_func); /* 它表示signal是一个函数,它接受两个参数, * 其中一个是int,另一个是ptr_to_func,返回 * 值是ptr_to_func。 */
3 关于数组和指针的声明
数组的声明就是数组,指针的声明就是指针,两者不能混淆。但在使用数组(在语句或表达式中引用)时,数组总是可以写成指针的形式,两者可以互换。
标准规定作为“类型的数组”的形参的声明应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第一个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的地址。
3.1 表达式(包括函数的形式参数)中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针(一维的数组指针,因为指针本身也相当于一维或一级)。
3.2 下标总是与指针的偏移量相同。(下标与指针的偏移量也总是出现在表达式中,所以此条规则也相当于是上一条规则的推论)
ref
Perter Van Der Linden《C和C++经典著作 C专家编程Expert C Programming Deep C Secrets》
-End-
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/159706.html