一个爱历史的程序猿

我知道这个世界很大。但只有疯狂到相信自己能改变世界的人,才能改变世界。

导航

复杂数据类型(signal)的解读-C语言基础

Posted on 2019-04-15 22:40  梁小满  阅读(965)  评论(0编辑  收藏  举报

 

在这里插入图片描述这一篇文章要探讨的是C语言中复杂数据类型的解读。涉及到signal()函数数据类型的解读(并不解释signal()的作用)以及对于数据类型的理解,属于C语言基础篇。


在开始解读signal()这种复杂类型之前,先给大家分享一个技巧。我老师曾经教过我。如果你想知道一个变量的数据类型,最简单的方法就是找到这个变量的定义处,然后把变量名去掉,剩下的就是这个变量的数据类型了。例如数组a的定义是int a[5]那么把变量名“a”去掉,剩下的int[5]便就是变量a的数据类型了。

说到这,可能有人立刻就定义了一个int[5] a;来验证一下int[5]这个数据类型是否真的存在,然后发现居然在编译的时候就给报错拦下来了,现在可能正拿着39米长的大刀在找博主来的路上了。但你能不能先在40米那里等会儿,容我狡辩一下。

确实,我得承认你直接定义一个int[5] a;是会连编译都过不了。但这并不是我的错啊!这不过是C语言语法规定导致的问题而已。C语言在定义变量的时候,规定把对于这个变量的描述关键字放在变量名的左边,把和数组相关的描述放在变量名的右边。至于为什么要这么规定,愚不才,答不上来。

不过int[5] a;在Java里面其实是可以编译通过的。事实上甚至有很大一部分人推崇用int[5] a;来定义数组比用int a[5];来定义数组更加合适。
因为前者能更加明确的表明变量a的数据类型。为什么在Java里面可以这么定义数组而在C语言里面不行呢?我估摸着应该是由于数组这个类型本身的特殊性导致的。首先C语言是一门很老的语言,C语言之父丹尼斯·里奇在创造C语言的时候可能并没有把数组当做是一个类型来看待。什么是数组?数组其实就是同一种数据类型数据的组合。所以在C语言中,数组的定义是int a[5];这样的,数组标示符[5]并没有参与到数组名a的描述中去。

但是这里有一个问题,数组本身也应该是一个数据类型啊,所以你定义一个数组的时候,数组标识符[]即应该是脱离数组名的数据类型而对原数据类型组合的描述,又应该是参与到数组名本身数据类型的描述中去作为"数组名数据类型"的一部分。所以在后面Java之父詹姆斯·高斯林创造Java的时候,就考虑到了这个问题,便从int a[5]的定义语法中引申出int[5] a的语法。因为这样或许能更合理的描述数组a是个什么东西。好了,说了这么多,这也只是我极端个人主义的猜想而已,并没有去深究。


但是,虽然在C语言中你并不能直接用int[5]这个数据类型来定义一个数组,但是这也并不能代表int[5]在C语言中就不是一个数据类型了。

我们来看一下下面的代码:

#include <stdio.h>
int main(void)
{
    int a[5] = {0};

    printf("(a):sizeof[%lu]\n", sizeof(a));
    printf("(int[5]):sizeof[%lu]\n", sizeof(int[5]));
    return 0;
}

这篇代码的运行结果是这样子的:

在这里插入图片描述

在C语言中,虽然我们不能直接用int[5]来定义一个数组变量,但是sizeof运算符却能够识别int[5]这个数据类型,而且用sizeof计算int[5]这个数据类型出来的值和直接求这个数组变量名得出来的值是一样的。这其实已经间接说明了int[5]其实就是数组变量a的数据类型了。这就和你定义了一个int a变量,然后用sizeof来求int和用sizeof来求a得到的结果是一样的一个道理。


事实上,如果你不知道一长串关键字拼接在一起的东西是不是一个合法数据类型,那么你完全可以把它们扔到sizeof运算符里面,如果编译不报错的话,那么多半就是一个数据类型了。

例如我们待会要分析的信号处理函数的原型很复杂像这样:

void(*signal(int sig,void(*func)(int)))(int)

按照规律,我们找到定义处把左边起第一个非关键字的单词去掉,那么剩下的便是它的数据类型了。

void(*(int sig,void(*func)(int)))(int)

很复杂的一个数据类型,不过不管它再复杂,它最根本的属性不过是一个函数类型而已。

验证代码:

#include <stdio.h>
void f(void)
{
}
int main(void)
{
    printf("signal类型大小为[%lu]\n", 
    			sizeof(void(*(int sig,void(*func)(int)))(int)));
    printf("f类型大小为[%lu]\n", sizeof(f));
    return 0;
}

运行结果:

在这里插入图片描述

嗯,类型再复杂本质都是一样的。


好了,说了那么多,我们是时候该来解读一下signal()这个类型到底是个什么样子了,我们又是如何看出来它是一个函数的。

void(*signal(int sig,void(*func)(int)))(int)

按照惯例,我们得先找到这个变量名字,这么长一串东西里面,那个单词才是这个变量的名字呢?按照规定,从左边看起,第一个非关键字signal,即是这个变量的名字。那么周围这一整串东西其实都是在描述它的,都是它的数据类型。我们就从这个变量名开始,向左右两边扩展的看。变量名的左边是一个*号,右边是一个()号。

最靠近变量名的运算符:*signal()

这里有些人可能就不理解了,右边明明只有一个“(”号而已,那里冒出来了个“)”号了?但是啊,()号这个东西就是整体出现的啊,有时候当你看到表达式里面出现()号的时候,你甚至可以不用管()号里面是什么,而直接看)号后面的东西,丝毫不影响你理解整个表达式。

说了那么多,其实就是让你直接把:

void(*signal(int sig,void(*func)(int)))(int)

看成:

void(*signal())(int)


好了,找到了最靠近变量名的两个运算符了,那么接下来就要看优先级了,这直接决定了signal是个什么。查阅手册可知,()号的优先级是大于*号的,所以signal会先和()号匹配,于是signal首先是一个函数。知道了signal是个函数之后那就好办了,定义一个函数都需要提供什么?嗯,参数返回值。所以现在我们只要找到signal参数返回值就可以了。

函数名后面的()里面的内容便是它的参数。所以我们刚刚忽略掉的()里面的内容就是这个函数的参数了。

参数是:(int sig, void(*func)(int))

一个整形变量:int sig
一个函数指针:void(*func)(int)

在这里别给我整个举一反三说func左右两边是*号和()号所以func它也是一个函数啊!

在这里func只有左边没有右边,因为func*号是在()里面的,而()号里面就只有*func,func只能是和*号结合,所以func是一个指针。这个指针指向什么数据类型呢?在定义处把指针变量名func和指针符号*去掉,剩下的就是这个指针指向的数据类型了。

指针指向的数据类型:void ()(int)

()号里面什么都没有,相当于这个()不存在。

最终指针指向的数据类型:void (int) 一个函数

相当于函数void f(int a)去掉变量名f后的数据类型。所以func是一个函数指针,它指向函数f这种类型的函数。


既然函数signal的参数找到了,那么把函数名signal和它的参数去掉,剩下的就是它的返回值了。

原型:

void(*signal(int sig,void(*func)(int)))(int)

去掉函数名和函数的参数之后:

返回值是:void(*)(int)

诶,有点眼熟。。。
刚刚我们分析的signal的第二个参数是什么来着?

第二个参数: void (*func)(int)
要分析的返回值:void (*)(int)

嗯,没错,signal的返回值就是func的类型。也是一个函数指针。而且,这两个东西对于signal来说其实是一样的。

如果在仅仅只讨论signal数据类型的时候

void(*signal(int sig,void(*func)(int)))(int)

其实就是

void(signal(int,void()(int)))(int)

这两句在描述signal的数据类型的时候是等价的。因为数据类型本身就不包含对于非关键字的描述。


所以,最后,我们总算是分析完signal的数据类型了。

它首先是一个函数:

signal()

然后它的参数有两个,一个是整形变量,一个是指针,该指针指向的是void (int)这种类型的函数。

signal(int sig,void(*func)(int))

最后它有一个返回值,返回一个函数指针,这个指针也指向void (int)这种类型的函数。

void(*signal(int sig,void(*func)(int)))(int)


好了,至此,整篇文章算是完了。最后说点题外话。
写在后面:

已经很久没有发博客了,最近一直很忙。之前就有朋友和我反馈说我写的文章过长,嗯,认真思考过,这是个问题。我写的文章往往都涉及过于广。常常在写一个知识点的时候,又引申出一些别的知识点。然后全部都给写出来。很容易让人抓不住重点。其实我在写的时候,是有很认真的规划文章的脉络的,我也有信心在把读者的思维发散到其他知识点之后能够再把读者的思绪给拉回来。很多时候一段句子我会改很久很久,就是为了能让文章的脉络清晰易懂。而且我写博客的时候,也习惯于用口头语来书写,虽然口头语没有书面语那么整洁,会造成文章过长。但却会有很好的通俗易懂的效果。我更愿意把博客写成一篇像是在聊天在说故事的文章而不想把博客写成教科书式的死板。我更愿意告诉别人为什么而不是告诉别人是什么。但是过长的文章确实不适合当代的快餐式阅读。后面我会尽量把一篇原本很长的文章拆分开来,作为几篇文章来发。


原博客始发于CSDN,在如今博客界的转载抄袭泛滥的环境下,原创不易,点个赞再走呗。以下是博客首页的链接。


 

零BUG是原则性问题。