带你分析C语言代码背后的细节(一): #include「stdio.h」

带你分析C语言代码背后的细节(一): #include<stdio.h>

一般来说,这是程序的第一行。对于刚入门的C小白来说,这第一行就挺难理解的了。下面我简要梳理一下相关知识。

首先记住#include 指令有两种形式:

#include <stdio.h>
#include "stdio.h"

#include 中的#符号表明,C预处理器在编译器接手之前处理这条指令。那为什么要包含(include作动词)文件呢?因为编译器需要这些文件中的信息。例如,stdio.h文件中通常包含EOF、NULL、getchar()和putchar()的定义、以及C的其他I/O函数,string.h文件则包含了strlen()函数和其他一些与字符串相关的函数(如拷贝字符串的函数和字符串查找函数),我以后还会提到。C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息,经常是一些预处理指令,下面会讲到。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件,下面也会简单提到如何创建的问题。如果你还不懂为什么要包含文件,没关系,等我们先讲讲别的,下面会再次提起。

提了一下为什么要包含文件,接下来看看代码背后的运作机理。前面提到的两种书写形式(尖括号和双引号)的本质是一样的,#include 的作用相当于把stdio.h文件中的所有内容都输入该行所在的位置。实际上,我认为这就是一种“复制-粘贴”的操作,你也可以把它看成是“链接”的操作。“include 文件”(include作动词)提供了一种方便的途径共享许多程序共有的信息。

#include这个代码是一条C预处理器指令(preprocessor directive)。通常,C编译器在编译前会对源代码做一些准备工作,即预处理(preprocessing)。因此,当预处理器发现#include 指令时,会查看后面的文件名并查找文件的内容,找到后把文件内容包含进来,即输入到源文件#include 指令所在位置,整个过程就是在替换#include 指令。

文章开头我们提到了#include 指令的两种形式,现在来讲讲它俩的区别,我以UNIX系统为例说明,尖括号告诉预处理器在标准系统目录中查找该文件,而双引号则告诉预处理器首先在当前目录中(或文件名中指定的其他目录,如#include “/usr/biff/p.h”,则查找/usr/biff目录)查找该文件,如果未找到再查找标准系统目录,如#include “hot.h”,则查找当前工作目录。
PS:不同的系统有不同的命名文件的方法,不同编译器有可能有不同的查找路径,读者可以自行了解。但是尖括号和双引号的规则与系统无关。

读到这里你可能要问,那stdio.h是什么?先说基本知识,点前面的stdio是基本名,点后面的h是扩展名(扩展名必须小写),如stdio.h、math.h是头文件,你可能还知道一种扩展名c和cpp,如HelloWorld.c、HelloWorld.cpp都是是源(代码)文件,区别是前者用的是C的规则,后者用C++的规则,一般Visual Studio会在你添加文件时默认以.cpp结尾,这时你得把.cpp修改成.c才能使用C语言的编译器。下图是Visual Studio2019的资源管理器界面,你想创建什么类型的文件就单机右键“添加”,创建相应的文件。
Visual Studio2019页面
所有的C编译器软件包都提供stdio.h文件。该文件中包含了供编译器使用的输入(I)和输出(O)函数(如,printf())信息。该文件名的含义是标准输入/输出(I/O)头文件。你可能会问,那为何不把输入和输出内置在语言中呢?原因之一是,并非所有程序都要用到I/O(输入/输出)包,轻装上阵表现了C语言的哲学。通常,在C程序顶部的信息集合被称为头文件(header)。

在大多数情况下,头文件包含了编译器创建最终可执行程序要用到的信息。例如,头文件中可以定义一些常量,或者指明函数名以及如何使用它们。但是,函数的实际代码在一个预编译代码的库文件中。简而言之,头文件帮助编译器把你的程序正确地组合在一起。

ANSI/ISO C规定了C编译器必须提供哪些头文件。有些程序要包含stdio.h,而有些不用。特定C实现的文档中应该包含对C库函数的说明。这些说明确定了使用哪些函数需要包含哪些头文件。例如,要使用printf()函数,必须包含stdio.h头文件。

头文件的“头”告诉我们,它是在程序的最前边。说到这就要提起C标准。C90标准新增了函数原型,函数原型是一种声明形式,告知编译器正在使用某函数,因此函数原型也被称为函数声明。函数原型还指明了函数的属性。例如,下图中的butler()函数原型中的第一个void表明butler()没有返回值(通常被调函数会向主调函数返回一个值,但是butler()没有这么做),第二个void (butler(void)中的void)的意思是butler()函数不带参数。(将来我会专门写一篇博客分析函数原型的更多细节,请关注)函数原型我们就提这么多,有点跑题了。我们回到C标准,C标准建议,要为程序中用到的所有函数提供函数原型。那么我们自然会想到,如果把函数原型放在头文件中,我们就不用在每次使用函数文件时都写出函数的原型。C标准库就是这样做的,例如,把I/O函数原型放在stdio.h中,把数学函数原型放在math.h中。你也可以这样用自定义的函数文件,下图的butler()函数就是自定义函数。
用Visual Studio2019编写完成
编译运行结果:
VS2019
标准include文件为标准库函数提供了函数原型。例如,在C标准中,stdio.h文件包含了printf()的函数原型。

到这里你可能要问,为什么要有ANSI函数原型?这样做是否必要?好处是什么?如何编写自定义头文件来编写自己的函数原型?

继续关注我,惊喜不断。你会发现我有讲不完的故事,问不完的问题。为了解答自己的问题,我需要继续学习C语言,梳理出自己对以上问题的理解。

参考书:《C Primer Plus》Stephen Prata第六版
费曼学习法,以教为学,开源不易,多多支持!
初学C语言,如发现不足请在评论区指出。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值