这篇文章要探讨的是关于C/C++中复杂声明的解读。涉及到以下四个板块:
1.解读复杂数据类型的基本技巧
2.const与*的那些爱恨情仇
3.函数与函数指针的那些事
4.挑战几个复杂的函数声明
文章内容属于C语言基础篇,适合被C语言的声明折磨到凌乱的小伙伴阅读。
1.解读复杂数据类型的基本技巧
如果你想知道一个变量的数据类型,找到这个变量的定义处,然后把变量名去掉,剩下的就是这个变量的数据类型。
例如:数组a的定义是int a[5],把变量名“a”去掉,剩下的int [5]便是变量a的数据类型。
值得注意的是,以上技巧中的做法是不可逆的,即只对符合C/C++语法的语句有效。
例如:给出Java的数组定义int[5] a,把变量名“a”去掉,剩下的部分也是int [5],但这并不能说明前面的语句是正确的语句。值得注意的是,正是由于C语言规定“要将类型相关的描述放在变量名的左边,数组相关的描述放在变量名的右边”才带来了一些变量声明理解的痛苦。
下面来看一组代码:
#include <stdio.h>
int main(void)
{
int a[5] = {0};
printf("sizeof(a)..[%lu]\n", sizeof(a));
printf("sizeof(int[5])..[%lu]\n", sizeof(int[5]));
return 0;
}
这是代码运行的结果:
在C语言中,我们虽然不能直接用int[5]作为一个类型定义变量,但是sizeof运算符却能够识别int[5]这个数据类型,这就间接说明int[5]其实就是数组变量a的数据类型。
事实上,如果你不知道一长串关键字拼接在一起的东西是不是一个合法的C/C++数据类型,那么你完全可以把它们扔到sizeof运算符里面,如果编译不报错的话,那么多半就是一个数据类型。
2.const与*的那些爱恨情仇
const int *a
- 此时a为常量指针,即常指针,指向的是常量,即a自身可以改变,但其指向的内容不可改变。
- 虽然指向的内容不可改写,但是delete则是可以的。
int *const a
- 类似于C++中的绑定,即a自身不可改变,但其指向的内容可以改变。
const int * cosnt a
- 该指针变量自身的内容不可改变,同时其指向的内容亦不可改变。
const int &a
- 在引用前面加上const,代表该引用为常引用,即被引用的对象不可改变。若是在形参中使用,则不可达到在函数里面修改变量值的目的。
3.函数与函数指针的那些事
在ANSI C中,标准对语法做了如下的规定:
1.函数调用运算符“()”的操作数不是“函数”,而是“函数指针”。
2.表达式中的“函数”将被自动转换成“指向函数的指针”。但是,当函数是取地址运算符&或者sizeof运算符的操作数时,不起作用。
也就是说,如果对函数指针使用解引用*,它会暂时成为函数,但是因为是在表达式中,所以它会被瞬间变回函数指针。
因此,下面的语句也是能够顺利执行的:
(**********printf)("hello world\n"); //无论如何,*就是什么都没有做。
4.挑战几个复杂的函数声明
小试牛刀
在ANSI C的标准库中,有一个atexit()函数。如果使用这个还能输,当程序正常结束的时候,可以回调一个指定的函数。
atexit()的原型定义如下:
int atexit(void (*func)(void))
首先,我们需要确定函数的名字,很明显为“atexit”,根据技巧,我们将其去除,剩下的语句即是函数的类型。
int (void (*func)(void))
函数的类型由“返回值类型”和“参量表”组成,可以清晰地看出返回值类型为int,参量表为(void (*func)(void)),只有一个参数,是一个指向返回值为void,无参数函数的指针。
因此对于以上声明的正确解读如下:
atexit是一个返回int的函数(参数是,指向 返回void、没有参数的函数 的指针)。
更加复杂的例子
经过上面的例子,下面我们进入正题,看一个复杂的函数声明。
void (*signal(int sig,void(*func)(int)))(int)
相信很多小伙伴看到这个例子以后整个人都晕了,不过没关系,我们一层一层拨开来解读这个声明。
首先可以很轻松地确定函数的名字为“signal”,去除名字,得到剩下的语句。
void (*(int sig,void(*func)(int)))(int)
对于解读函数的类型,最重要的事情就是分离“返回值类型”和“参量表”。在该声明中,函数的返回值类型被落在函数名两端(这其实是C/C++的一个语法弊端),但参量表依然可以轻松地分离出来,如下:
void (* )(int)
↓
↓
(int sig,void(*func)(int))
我们将“返回值类型”和“参量表”分割开来后,似乎整个声明又变得清晰起来了。
返回值类型为“void (*)(int)”,即 指向 返回void、参数为int的函数 的指针。
参数有两个,一个为int,另一个也是 指向 返回void、参数为int的函数 的指针。
因此对于以上声明的正确解读如下:
signal是一个返回“指向 返回void、参数为int的函数 的指针”的函数,它有两个参数,一个是int,另一个是“指向 返回void、参数为int的函数”的指针。
如此复杂的例子都能通过这样的分解被成功理解,相信不会再有什么让你恐惧的C声明了。
最后一个例子
在阅读这个例子的分析之前,我强烈地建议你先自己尝试解读这个例子,看看是否与我的解析一致,如果不一致,请仔细思考差错出在了哪里。
double (*polylines[5])[2];
这依旧是由于C语言中喜欢把变量声明的不同相关描述丢在变量名两端而造成的困惑声明,不过没关系,我们继续慢慢看。
从整体上来看,这次并不是函数的声明,而是声明了一个数组,而且可以看出一定存在多个数组的情况。
还是照原来的技巧来做,去掉变量名“polylines”,得到变量的类型“double (*[5])[2]”,是不是还是很混乱?这里的理解,的确甚至比前面的函数声明更加困难。
但是,只要牢记数组声明的特点,就可以理解它。
在C/C++中,数组的声明是这样的:
元素类型 变量名[元素数量]
与上面解读函数声明类似地,解读数组的声明,最重要的就是区分开“元素类型”和“元素数量”。
而元素数量紧跟在变量名之后,那么对上面的变量类型进行分割,就得到了下面的两个东西:
double (* )[2];
↓
↓
polylines[5]
现在一下子就清晰了吧!polylines是一个数组(元素个数5),其元素的类型是指向double的数组(元素个数2)的指针。
总结
经过上面的一系列介绍,相信读者已经对C/C++中复杂的声明有了一些头绪。
C/C++是古老的,它是人类探索计算机高级语言领域的先驱,因此它是为了功能而设计的,并没有站在后继者的高度,难免会出现一些不太令人舒适的地方。
但这门语言同样也是强大、充满生命力的,因此不妨感叹,我们现在写的语言,和几十年前的计算机程序员们写出来的,居然没什么本质上的变化。
C/C++中的那些复杂声明,你搞懂了吗?