做者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。
转载请注明出处。
原文: https://www.jianshu.com/p/207...《C语言探索之旅》全系列程序员
上一课是 C语言探索之旅 | 第一部分练习题 。编程
话说上一课是第一部分最后一课,如今开始第二部分的探索之旅!小程序
在这一部分中,咱们会学习 C语言的高级技术。这一部份内容将是一座高峰,会挺难的,可是咱们一块儿翻越。安全
俗语说得好:“一口是吃不成一个胖子的。”微信
可是一小口一小口,慢慢吃,仍是能吃成胖子的嘛。因此要细水长流,肥油慢积,一路上有你(“油腻”)~编辑器
一旦你跟着咱们的课程一直学到这一部分的结束,你将会掌握 C语言的核心技术,也能够理解大部分 C语言写的程序了。模块化
到目前为止咱们的程序都只是在一个 main.c 文件里捣腾,由于咱们的程序还很短小,这也足够了。函数
但若是以后你的程序有了十多个函数,甚至上百个函数,那么你就会感到所有放在一个 main.c 文件里是多么拥挤和混乱。学习
正由于如此,计算机科学家才想出了模块化编程。原则很简单:与其把全部源代码都放在一个 main.c 当中,咱们将把它们合理地分割,放到不一样的文件里面。测试
到目前为止,写自定义函数的时候,咱们都要求你们暂时把函数写在 main 函数的前面。
这是为何呢?
由于这里的顺序是一个重要的问题。若是你将本身定义的函数放置在 main 函数以前,电脑会读到它,就会“知道”这个函数。当你在 main 函数中调用这个函数时,电脑已经知道这个函数,也知道到哪里去执行它。
可是假如你把这个函数写在 main 函数后面,那你在 main 函数里调用这个函数的时候,电脑还不“认识”它呢。你能够本身写个程序测试一下。是的,很奇怪对吧?这绝对有点任性的。
那你会说:“C语言岂不是设计得很差么?”
我“彻底”赞成(可别让 C语言之父 Dennis Ritchie 听到了...)。可是请相信,这样设计应该也是有理由的。计算机先驱们早就想到了,也提出了解决之道。
下面咱们就来学一个新的知识点,借着这个技术,你能够把你的自定义函数放在程序的任意位置。
咱们会声明咱们的函数,须要用到一个专门的技术:函数原型,英语是 function prototype。function 表示“函数”,prototype 表示“原型,样本,模范”。
就比如你对电脑发出一个通知:“看,个人函数的原型在这里,你给我记住啦!”
咱们来看一下上一课举的一个函数的例子(计算矩形面积):
double rectangleArea(double length, double width) { return length * width; }
怎么来声明咱们上面这个函数的原型呢?
;
)。很简单吧?如今你就能够把你的函数的定义放在 main 函数后面啦,电脑也会认识它,由于你在 main 函数前面已经声明过这个函数了。
你的程序会变成这样:
#include <stdio.h> #include <stdlib.h> // 下面这一行是 rectangleArea 函数的函数原型 double rectangleArea(double length, double width); int main(int argc, char *argv[]) { printf("长为 10,宽为 5 的矩形面积 = %f\n", rectangleArea(10, 5)); printf("长为 3.5,宽为 2.5 的矩形面积 = %f\n", rectangleArea(3.5, 2.5)); printf("长为 9.7,宽为 4.2 的矩形面积 = %f\n", rectangleArea(9.7, 4.2)); return 0; } // 如今咱们的 rectangleArea 函数就能够放置在程序的任意位置了 double rectangleArea(double length, double width) { return length * width; }
与原先的程序相比有什么改变呢?
其实就是在程序的开头加了函数的原型而已(记得不要忘了那个分号)。
函数的原型,实际上是给电脑的一个提示或指示。好比上面的程序中,函数原型
double rectangleArea(double length, double width);
就是对电脑说:“老兄,存在一个函数,它的输入是哪几个参数,输出是什么类型”,这样就能让电脑更好地管理。
多亏了这一行代码,如今你的 rectangleArea 函数能够置于程序的任何位置了。
记得:最好养成习惯,对于 C语言程序,老是定义了函数,再写一下函数的原型。
那么不写函数原型行不行呢?
也行。只要你把每一个函数的定义都放在 main 函数以前。可是你的程序慢慢会愈来愈大,等你有几十或者几百个函数的时候,你还顾得过来么?
因此养成好习惯,不吃亏的。
你也许注意到了,main 函数没有函数原型。由于不须要,main 函数是每一个 C程序必须的入口函数。人家 main 函数“有权任性”,跟编译器关系好,编译器对 main 函数很熟悉,是常常打交道的“哥们”,因此不须要函数原型来“介绍” main 函数。
还有一点,在写函数原型的时候,对于圆括号里的函数参数,名字是不必定要写的,能够只写类型。
由于函数原型只是给电脑作个介绍,因此电脑只须要知道输入的参数是什么类型就够了,不须要知道名字。因此咱们以上的函数原型也能够简写以下:
double rectangleArea(double, double);
看到了吗,咱们能够省略 length 和 width 这两个变量名,只保留 double(双精度浮点型)这个类型名字。
千万不要忘了函数原型末尾的分号,由于这是编译器区分函数原型和函数定义开头的重要指标。若是没有分号,编译时会出现比较难理解的错误提示。
头文件在英语中是 header file。header 表示“数据头,页眉”,file 表示“文件”。
每次看到这个术语,我都想到已经结婚的“咱们的青春”:周杰伦 的《头文字D》。
到目前为止,咱们的程序只有一个 .c 文件(被称为“源文件”,在英语中是 source file。source 表示“源,源头,水源”),好比咱们以前把这个 .c 文件命名为 main.c。固然名字是无所谓的,起名为hello.c,haha.c 都行。
在实际编写程序的时候,你的项目通常确定不会把代码都写在一个 main.c 文件中。固然,也不是不能够。
可是,试想一下,若是你把全部代码都塞到这一个 main.c 文件中,那若是代码量达到 10000 行甚至更多,你要在里面找一个东西就太难了。也正是由于这样,一般咱们每个项目都会建立多个文件。
那以上说到的项目是指什么呢?
以前咱们用 CodeBlocks 这个 IDE 建立第一个 C语言项目的时候,其实已经接触过了。
一个项目(英语是 project),简单来讲是指你的程序的全部源代码(还有一些其余的文件),项目里面的文件有多种类型。
目前咱们的项目还只有一个源文件:main.c 。
看一下你的 IDE,通常来讲项目是列在左边。
如上图,你能够看到,这个项目(在 Projects 一栏里)只有一个文件:main.c 。
如今咱们再来展现一个包含好多个文件的项目:
上图中,咱们能够看到在这个项目里有好几个文件。实际中的项目大可能是这样的。你看到那个 main.c 文件了吗?一般来讲在咱们的程序中,会把 main 函数只定义在 main.c 当中。
固然也不是非要这样,每一个人都有本身的编程风格。不过但愿跟着这个课程学习的读者,能够和咱们保持一致的风格,方便理解。
那你又要问了:“为何建立多个文件呢?我怎么知道为项目建立几个文件合适呢?”
答案是:这是你的选择。一般来讲,咱们把同一主题的函数放在一个文件里。
在上图中,咱们能够看到有两种类型的文件:一种是以 .h 结尾的,一种是以 .c 结尾的。
因此,一般来讲咱们不常把函数原型放在 .c 文件中,而是放在 .h 文件中,除非你的程序很小。
对每一个 .c 文件,都有同名的 .h 文件。上面的项目那个图中,你能够看到 .h 和 .c 文件一一对应。
但咱们的电脑怎么知道函数原型是在 .c 文件以外的另外一种文件里呢?
须要用到咱们以前介绍过的预处理指令 #include
来将其引入到 .c 文件中。
请作好准备,下面将有一波密集的知识点“来袭”。
怎么引入一个头文件呢?其实你已经知道怎么作了,以前的课程咱们已经写过了。
好比咱们来看咱们上面的 game.c 文件的开头
#include <stdlib.h> #include <stdio.h> #include "game.h" void player(SDL_Surface* screen) { // ... }
看到了吗,其实你早就熟悉了,要引入头文件,只须要用 #include 这个预处理指令。
所以咱们在 game.c 源文件中一共引入了三个头文件:stdlib.h, stdio.h,game.h。
注意到一个不一样点了吗?
在标准库的头文件(stdlib.h,stdio.h)和你本身定义的头文件(game.h)的引入方式是有点区别的:
<>
用于引入标准库的头文件。对于 IDE,这些头文件通常位于 IDE 安装目录的 include 文件夹中;在 Linux 操做系统下,则通常位于系统的 include 文件夹里。""
用于引入自定义的头文件。这些头文件位于你本身的项目的目录中。咱们再来看一下对应的 game.h 这个头文件的内容:
看到了吗,.h 文件中存放的是函数原型。
你已经对一个项目有大体概念了。
那你又会问了:“为何要这样安排呢?把函数原型放在 .h 头文件中,在 .c 源文件中用 #include 引入。为何不把函数原型写在 .c 文件中呢?”
答案是:方便管理,条理清晰,不容易出错,省心。
由于如前所述,你的电脑在调用一个函数前必须先“知道”这个函数,咱们须要函数原型来让使用这个函数的其余函数预先知道。
若是用了 .h 头文件的管理方法,在每个 .c 文件开头只要用 #include 这个指令来引入头文件的全部内容,那么头文件中声明的全部函数原型都被当前 .c 文件所知道了,你就不用再操心那些函数的定义顺序或者有没有被其余函数知道
例如个人 main.c 函数要使用 functions.c 文件中的函数,那我只要在 main.c 的开头写 #include "functions.h"
,以后我在 main.c 函数中就能够调用 function.c 中定义的函数了。
你可能又要问了:“那我怎么在项目中加入新的 .h 和 .c 文件呢?”
很简单,在 CodeBlocks 里,鼠标右键点击项目列表的主菜单处,选择 Add Files,或者在菜单栏上依次单击 File -> New -> File... ,就能够选择添加文件的类型了。
你脑海里确定出现一个问题:
若是咱们用 #include 来引入 stdio.h 和 stdlib.h 这样的标准库的头文件,而这些文件又不是我本身写的,那么它们确定存在于电脑里的某个地方,咱们能够找到,对吧?
是的,彻底正确!
若是你使用的是 IDE(集成开发环境),那么它们通常就在你的 IDE 的安装目录里。
若是是在纯 Linux 环境下,那就要到系统文件夹里去找,这里不讨论了,感兴趣的读者能够去网上搜索。
在个人状况,由于安装的是 CodeBlocks 这个 IDE,因此在 Windows下,个人头文件们“隐藏”在这两个路径下:
C:\Program Files\CodeBlocks\MinGW\include
和
C:\Program Files\CodeBlocks\MinGW\x86_64-w64-mingw32\include
通常来讲,都在一个叫作 include 的文件夹里。
在里面,你会找到不少文件,都是 .h 文件,也就是 C语言系统定义的标准头文件,也就是系统库的头文件(对 Windows,macOS,Linux 都是通用的,C语言原本就是可移植的嘛)。
在这众多的头文件当中,你能够找到咱们的老朋友:stdio.h 和 stdlib.h。
你能够双击打开这些文件或者选择你喜欢的文本编辑器来打开,不过也许你会吓一跳,由于这些文件里的内容不少,并且好些是咱们还没学到的用法,好比除了 #include 之外的其余的预处理指令。
你能够看到这些头文件中充满了函数原型,好比你能够在 stdio.h 中找到 printf 函数的原型。
你要问了:“OK,如今我已经知道标准库的头文件在哪里了,那与之对应的标准库的源文件(.c 文件)在哪里呢?”
很差意思,你见不到它们啦。由于 .c 文件已经被事先编译好,转换成计算机能理解的二进制码了。
“伊人已去,年华不复,吾将何去何从?”
既然见不到原先的它们了,至少让我见一下“美图秀秀”以后的它们吧…
能够,你在一个叫 lib 的文件夹下面就能够找到,在个人 Windows 下的路经为:
C:\Program Files\CodeBlocks\MinGW\lib
和
C:\Program Files\CodeBlocks\MinGW\x86_64-w64-mingw32\lib
被编译成二进制码的 .c 文件,有了一个新的后缀名:.a(在 CodeBlocks 的状况,它的编译器是 MinGW。MinGW 简单来讲就是 GCC 编译器的 Windows 版本)或者 .lib(在 Visual C++ 的状况),等。这是静态连接库的状况。
你在 Windows 中还能找到 .dll 结尾的动态连接库;你在 Linux 中能找到 .so 结尾的动态连接库。暂时咱们不深究静态连接库和动态连接库,有兴趣的读者能够去网上自行搜索。
这些被编译以后的文件被叫作库文件或 Library 文件(library 表示“库,图书馆,文库”),不要试着去阅读这些文件的内容,由于是看不懂的乱码。
学到这里可能有点晕,不过继续看下去就会渐渐明朗起来,下面的内容会有示意图帮助理解。
小结一下:
在咱们的 .c 源文件中,咱们能够用 #include 这个预处理指令来引入标准库的 .h 头文件或本身定义的头文件。这样咱们就能使用标准库所定义的 printf 这样的函数,电脑就认识了这些函数(借着 .h 文件中的函数原型),就能够检验你调用这些函数时有没有用对,好比函数的参数个数,返回值类型,等。
如今咱们知道了一个项目是由若干文件组成的,那咱们就能够来了解一下编译器(compiler)的工做原理。
以前的课里面展现的编译示例图是比较简化的,下图是一幅编译原理的略微详细的图,但愿你们用心理解并记住:
上图将编译时所发生的事情基本详细展现了,咱们来仔细分析:
预处理指令有好多种,目前咱们学过的只有 #include
,它使咱们能够在一个文件中引入另外一个文件的内容。#include 这个预处理指令也是最经常使用的。
预处理器会把 #include 所在的那一句话替换为它所引入的头文件的内容,好比
#include <stdio.h>
预处理器在执行时会把上面这句指令替换为 stdio.h 文件的内容。因此到了编译的时候,你的 .c 文件的内容会变多,包含了全部引入的头文件的内容,显得比较臃肿。
编译器会把 .c 文件先转换成 .o 文件(有的编译器会生成 .obj 文件),.o 文件通常叫作目标文件(o 是 object 的首字母,表示“目标”),是临时的二进制文件,会被用于以后生成最终的可执行二进制文件。
.o 文件通常会在编译完成后被删除(根据你的 IDE 的设置)。从某种程度上来讲 .o 文件虽然是临时中间文件,好像没什么大用,但保留着不删除也是有好处:假如项目有 10 个 .c 文件,编译后生成了 10 个 .o 文件。以后你只修改了其中的一个 .c 文件,若是从新编译,那么编译器不会为其余 9 个 .c 文件从新生成 .o 文件,只会从新生成你更改的那个。这样能够节省资源。
如今你知道从代码到生成一个可执行程序的内部原理了吧,下面咱们要展现给你们的这张图,很重要,但愿你们理解并记住。
大部分的错误都会在编译阶段被显示,但也有一些是在连接的时候显示,有多是少了 .o 文件之类。
以前那幅图其实还不够完整,你可能想到了:咱们用 .h 文件引入了标准库的头文件的内容(里面主要是函数原型),函数的具体实现的代码咱们还没引入呢,怎么办呢?
对了,就是以前提到过的 .a 或 .lib 这样的库文件(由标准库的 .c 源文件编译而成)。
因此咱们的连接器(linker)的活还没完呢,它还须要负责连接标准库文件,把你本身的 .c 文件编译生成的 .o 目标文件和标准库文件整合在一块儿,而后连接成最终的可执行文件。
以下图所示:
这下咱们的示意图终于完整了。
这样咱们才有了一个完整的可执行文件,里面有它须要的全部指令的定义,好比 printf 的定义。
为告终束这一课,咱们还得学习最后一个知识点:变量和函数的做用范围(有效范围)。
咱们将学习变量和函数何时是能够被调用的。
当你在一个函数里定义了一个变量以后,这个变量会在函数结尾时从内存中被删除。
int multipleTwo(int number) { int result = 0; // 变量 result 在内存中被建立 result = 2 * number; return result; } // 函数结束,变量 result 从内存中被删除
在一个函数里定义的变量,只在函数运行期间存在。
这意味着什么呢?意味着你不能从另外一个函数中调用它。
#include <stdio.h> int multipleTwo(int number); int main(int argc, char *argv[]) { printf("15 的两倍是 %d\n", multipleTwo(15)); printf("15 的两倍是 %d", result); // 错误! return 0; } int multipleTwo(int number) { int result = 0; result = 2 * number; return result; }
能够看到,在 main 函数中,咱们试着调用 result 这个变量,可是由于这个变量是在 multipleTwo 函数中定义的,在 main 函数中就不能调用,编译会出错。
记住:在函数里定义的变量只能在函数内部使用,咱们称之为 局部变量,英语是 local variable。local 表示“局部的,本地的”,variable 表示“变量”。
全局变量的英语是 global variable。global 表示“全局的,整体的”。
咱们能够定义能被项目的全部文件的全部函数调用的变量。咱们会展现怎么作,是为了说明这方法存在,可是通常来讲,要避免使用能被全部文件使用的全局变量。
可能这样作一开始会让你的代码简单一些,可是不久你就会为之烦恼了。
为了建立能被全部函数调用的全局变量,咱们需要在函数以外定义。一般咱们把这样的变量放在程序的开头,#include 预处理指令的后面。
#include <stdio.h> int result = 0; // 定义全局变量 result void multipleTwo(int number); // 函数原型 int main(int argc, char *argv[]) { multipleTwo(15); // 调用 multipleTwo 函数,使全局变量 result 的值变为原来的两倍 printf("15 的两倍是 %d\n", result); // 咱们能够调用变量 result return 0; } void multipleTwo(int number) { result = 2 * number; }
上面的程序中,咱们的函数 multipleTwo 再也不有返回值了,而是用于将 result 这个全局变量的值变成 2 倍。以后 main 函数能够再使用 result 这个变量。
因为这里的 result 变量是一个彻底开放的全局变量,因此它能够被项目的全部文件调用,也就能被全部文件的任何函数调用。
注:这种类型的变量是很不推荐使用的,由于不安全。通常用函数里的 return 语句来返回一个变量的值。
刚才咱们学习的彻底开放的全局变量能够被项目的全部文件访问。咱们也可使一个全局变量只能被它所在的那个文件调用。
就是说它能够被本身所在的那个文件的全部函数调用,但不能被项目的其余文件的函数调用。
怎么作呢?
只须要在变量前面加上 static 这个关键字。以下所示:
static int result = 0;
static 表示“静态的,静止的”。
注意:
若是你在声明一个函数内部的变量时,在前面加上 static 这个关键字,它的含义和上面咱们演示的全局变量是不一样的。
函数内部的变量若是加了 static,那么在函数结束后,这个变量也不会销毁,它的值会保持。下一次咱们再调用这个函数时,此变量会延用上一次的值。
例如:
int multipleTwo(int number) { static int result = 0; // 静态变量 result 在函数第一次被调用时建立 result = 2 * number; return result; } // 变量 result 在函数结束时不会被销毁
这到底意味着什么呢?
就是说:result 这个变量的值,在下次咱们调用这个函数时,会延用上一次结束调用时的值。
有点晕是吗?没关系。来看一个小程序,以便加深理解:
#include <stdio.h> int increment(); int main(int argc, char *argv[]) { printf("%d\n", increment()); printf("%d\n", increment()); printf("%d\n", increment()); printf("%d\n", increment()); return 0; } int increment() { static int number = 0; number++; return number; }
上述程序中,在咱们第一次调用 increment 函数时,number 变量被建立,初始值为 0,而后对其作自增操做(++ 运算符),因此 number 的值变为 1。
函数结束后,number 变量并无从内存中被删除,而是保存着 1 这个值。
以后,当咱们第二次调用 increment 函数时,变量 number 的声明语句(static int number = 0;
)会被跳过不执行(由于变量 number 还在内存里呢。你想,一个皇帝还没驾崩,太子怎么能继位呢?)。
咱们继续使用上一次建立的 number 变量,这时候变量的值沿用第一次 increment 函数调用结束后的值:1,再对它作 ++ 操做(自加 1),number 的值就变为 2 了。
依此类推,第三次调用 increment 函数后 number 的值为 3。第四次 number 的值为 4。
因此程序的输出以下:
1 2 3 4
咱们用函数的做用域来结束咱们关于变量和函数的做用域的学习。
正常来讲,当你在一个 .c 源文件中建立了一个函数,那它就是全局的,能够被项目中全部其余 .c 文件调用。
可是有时咱们须要建立只能被本文件调用的函数,怎么作呢?
聪明如你确定想到了:对了,就是使用 static 关键字,与变量相似。
把它放在函数前面。以下:
static int multipleTwo(int number) { // 指令 }
如今,你的函数就只能被同一个文件中的其余函数调用了,项目中的其余文件中的函数就只“可远观而不可亵玩焉”…
今天的课就到这里,一块儿加油吧!
下一课:C语言探索之旅 | 第二部分第二课:进击的指针,C语言的王牌!
我是 谢恩铭,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」