在上一讲中,简单介绍了函数的定义和使用,只要你想完成一个新功能,首先想到的应该是定义一个新的函数来完成这个功能。这讲继续介绍函数的其他用法和注意事项。

一、函数的声明

1.在C语言中,函数的定义顺序是有讲究的:默认情况下,只有后面定义的函数才可以调用前面定义过的函数

1 int sum(int a, int b) {
2 return a + b;
3 }
4
5 int main()
6 {
7 int c = sum(1, 4);
8 return 0;
9 }

第5行定义的main函数调用了第1行的sum函数,这是合法的。如果调换sum函数和main函数的顺序,在标准的C编译器环境下是不合法的(不过在GCC编译器环境下只是一个警告)

2.如果想把函数的定义写在main函数后面,而且main函数能正常调用这些函数,那就必须在main函数的前面进行函数的声明

 1 // 只是做个函数声明,并不用实现
2 int sum(int a, int b);
3
4 int main()
5 {
6 int c = sum(1, 4);
7 return 0;
8 }
9
10 // 函数的定义(实现)
11 int sum(int a, int b) {
12 return a + b;
13 }

在第11行定义了sum函数,在第2行对sum函数进行了声明,然后在第6行(main函数中)就可以正常调用sum函数了。

3.函数的声明格式

1> 格式

返回值类型  函数名 (参数1, 参数2, ...)

只要你在main函数前面声明过一个函数,main函数就知道这个函数的存在,就可以调用这个函数。而且只要知道函数名、函数的返回值、函数接收多少个参数、每个参数是什么类型的,就能够调用这个函数了,因此,声明函数的时候可以省略参数名称。比如上面的sum函数声明可以写成这样:

int sum(int, int);

究竟这个函数是做什么用的,还要看函数的定义。

2> 如果只有函数的声明,而没有函数的定义,那么程序将会在链接时出错

下面的写法是错误的:

1 int sum(int a, int b);
2
3 int main()
4 {
5
6 sum(10, 11);
7
8 return 0;
9 }
  • 在第1行声明了一个sum函数,但是并没有对sum函数进行定义,接着在第6行调用sum函数
  • 这个程序是可以编译成功的,因为我们在main函数前面声明了sum函数(函数的声明和定义是两码事),这个函数声明可以理解为:在语法上,骗一下main函数,告诉它sum函数是存在的,所以从语法的角度上main函数是可以调用sum函数的。究竟这个sum函数存不存在呢,有没有被定义呢?编译器是不管的。在编译阶段,编译器并不检测函数有没有定义,只有在链接的时候才会检测这个函数存不存在,也就是检测函数有没有被定义。
  • 因此,这个程序会在链接的时候报错,错误信息如下:

  • 我这里的源文件是main.c文件,所以编译成功后生成一个main.o文件。链接的时候,链接器会检测main.o中的函数有没有被定义。
  • 上面的错误信息大致意思是:在main.o文件中找不到sum这个标识符。
  • 错误信息中的linker是链接器的意思,下次看到这个linker,说明是链接阶段出错了。链接出错了,就不能生成可执行文件,程序就不能运行。
  • 这个错误的解决方案就是加上sum函数的定义。

二、多源文件开发

1.为什么要有多个源文件

1> 在编写第一个C程序的时候已经提到:我们编写的所有C语言代码都保存在拓展名为.c的源文件中,编写完毕后就进行编译、链接,最后运行程序。

2> 在前面的学习过程中,由于代码比较少,因此所有的代码都保存在一个.c源文件中。但是,在实际开发过程中,项目做大了,源代码肯定非常多,很容易就上万行代码了,甚至上十万、百万都有可能。这个时候如果把所有的代码都写到一个.c源文件中,那么这个文件将会非常庞大,也非常恶心,你可以想象一下,一个文件有十几万行文字,不要说调试程序了,连阅读代码都非常困难。

3> 而且,公司里面都是以团队开发为主,如果多个开发人员同时修改一个源文件,那就会带来很多麻烦的问题,比如张三修改的代码很有可能会抹掉李四之前添加的代码。

4> 因此,为了模块化开发,一般会将不同的功能写到不同的.c源文件中,这样的话,每个开发人员都负责修改不同的源文件,达到分工合作的目的,能够大大提高开发效率。也就是说,一个正常的C语言项目是由多个.c源文件构成。

2.将sum函数写到其他源文件中

接下来就演示一下多个源文件的开发,我将前面定义的sum函数写在另一个源文件(命名为sum.c)中。这时候就有两个源文件:

1> main.c文件

1 int main()
2 {
3
4 return 0;
5 }

2> sum.c文件

1 int sum(int a, int b)
2 {
3 return a + b;
4 }

3.在main函数中调用sum函数

1> 现在想在main函数中调用sum函数,那么你可能会直接这样写:

1 int main()
2 {
3 int c = sum(10, 11);
4
5 return 0;
6 }

这种写法在标准C语言编译器中是直接报错的,因为main函数都不知道sum函数的存在,怎么可以调用它呢!!!

2> 我们应该骗一下main函数,sum函数是存在的,告诉它sum函数的返回值和参数类型即可。也就是说,应该在main函数前面,对sum函数进行声明。

main.c文件应该写成下面这样

 1 #include <stdio.h>
2
3 int sum(int, int);
4
5 int main()
6 {
7 int c = sum(10, 11);
8
9 printf("c is %d\n", c);
10
11 return 0;
12 }

注意第3行,加了一个sum函数的声明。为了检验sum函数的调用结果,在第9行用prinf函数将结果输出。

4.编译所有的源文件

sum.c和main.c都编写完毕后,就可以使用gcc指令进行编译了。同时编译两个文件的指令是:cc -c main.c sum.c

编译成功后,生成了2个.o目标文件

也可以单独编译:

cc -c main.c

cc -c sum.c

5.链接所有的目标文件

前面已经编译成功,生成了main.o和sum.o文件。现在应该把这2个.o文件进行链接,生成可执行文件。

1> 注意,一定要同时链接两个文件。如果你只是单独链接main.o或者sum.o都是不可能链接成功的。原因如下:

  • 如果只是链接main.o文件:cc main.o,错误信息是:在main.o中找到不到sum这个标识符,其实就是找不到sum函数的定义。因为sum函数的定义在sum.o文件中,main.o中只有sum函数的声明

  • 如果只是链接sum.o文件:cc sum.o,错误信息是:找不到main函数。一个C程序的入口点就是main函数,main函数定义在main.o中,sum.o中并没有定义main函数,连入口都没有,怎么能链接成功、生成可执行文件呢?

可以看出,main.o和sum.o有密不可分的关系,其实链接的目的就是将所有相关联的目标文件和C语言函数库组合在一起,生成可执行文件。

2> 链接main.o和sum.o文件:cc main.o sum.o,生成了可执行文件a.out

3> 运行a.out文件:./a.out,运行结果是在屏幕上输出了:

c is 21

说明函数调用成功,我们已经成功在main.c文件的main函数中调用了sum.c文件中的sum函数

4> 从中也可以得出一个结论:只要知道某个函数的声明,就可以调用这个函数,编译就能成功。不过想要这个程序能够运行成功,必须保证在链接的时候能找到函数的定义。

三、#include

理解完前面的知识后,接下来就可以搞懂一个很久以前的问题:每次写在最前面的#include是干啥用的?

1.#include的作用

先来看一个最简单的C程序:

1 #include <stdio.h>
2
3 int main()
4 {
5 printf("Hello, World!\n");
6 return 0;
7 }

这个程序的作用是在屏幕上输出Hello,World!这一串内容,我们主要关注第一行代码。

  • #include 是C语言的预处理指令之一,所谓预处理,就是在编译之前做的处理,预处理指令一般以 # 开头
  • #include 指令后面会跟着一个文件名,预处理器发现 #include 指令后,就会根据文件名去查找文件,并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源文件中的 #include 指令,就像你把被包含文件中的全部内容拷贝到这个 #include 指令所在的位置一样。所以第一行指令的作用是将stdio.h文件里面的所有内容拷贝到第一行中。
  • 如果被包含的文件拓展名为.h,我们称之为"头文件"(Header File),头文件可以用来声明函数,要想使用这些函数,就必须先用 #include 指令包含函数所在的头文件
  • #include 指令不仅仅限于.h头文件,可以包含任何编译器能识别的C/C++代码文件,包括.c、.hpp、.cpp等,甚至.txt、.abc等等都可以

也就是说你完全可以将第3行~第7行的代码放到其他文件中,然后用 #include 指令包含进来,比如:

1> 将第3行~第7行的代码放到my.txt中

2> 在main.c源文件中包含my.txt文件

  • 编译链接后,程序还是可以照常运行的,因为 #include 的功能就是将文件内容完全拷贝到 #include 指令所在的位置
  • 说明:这里用txt文件纯属演示,平时做项目不会这样做,除非吃饱了撑着,才会把代码都写到txt中去

2.#include可以使用绝对路径

上面的#include "my.txt"使用的是相对路径,其实也可以使用绝对路径。比如#include "/Users/apple/Desktop/my.txt"

3.#include <>和#include ""的区别

二者的区别在于:当被include的文件路径不是绝对路径的时候,有不同的搜索顺序。

1> 对于使用双引号""来include文件,搜索的时候按以下顺序:

  • 先在这条include指令的父文件所在文件夹内搜索,所谓的父文件,就是这条include指令所在的文件
  • 如果上一步找不到,则在父文件的父文件所在文件夹内搜索;
  • 如果上一步找不到,则在编译器设置的include路径内搜索;
  • 如果上一步找不到,则在系统的INCLUDE环境变量内搜索

2> 对于使用尖括号<>来include文件,搜索的时候按以下顺序:

  • 在编译器设置的include路径内搜索;
  • 如果上一步找不到,则在系统的INCLUDE环境变量内搜索

我这里使用的是clang编译器,clang设置include路径是(4.2是编译器版本):/usr/lib/clang/4.2/include

Mac系统的include路径有:

  • /usr/include
  • /usr/local/include

4.stdio.h

我们已经知道#include指令的作用了,可是为什么要在第一行代码包含stdio.h呢?

  • stdio.h是C语言函数库中的一个头文件,里面声明了一些常用的输入输出函数,比如往屏幕上输出内容的printf函数
  • 这里之所以包含 stdio.h 文件,是因为在第5行中用到了在 stdio.h 内部声明的printf函数,这个函数可以向屏幕输出数据,第7行代码输出的内容是:Hello, World!
  • 注意:stdio.h里面只有printf函数的声明。前面已经提到:只要知道函数的声明,就可以调用这个函数,就能编译成功。不过想要这个程序能够运行成功,必须保证在链接的时候能找到函数的定义。其实链接除了会将所有的目标文件组合在一起,还会关联C语言的函数库,函数库中就有printf函数的定义。因此前面的程序是可以链接成功的。

5.头文件.h和源文件.c的分工

跟printf函数一样,我们在开发中会经常将函数的声明和定义写在不同的文件中,函数声明放在.h头文件中,函数定义放在.c源文件中。

下面我们将sum函数的声明和定义分别放在sum.h和sum.c中

这是sum.h文件

这是sum.c文件

然后在main.c中包含sum.h即可使用sum函数

其实sum.h和sum.c的文件名不一样要相同,可以随便写,只要文件名是合法的。但还是建议写成一样,因为一看文件名就知道sum.h和sum.c是有联系的。

运行步骤分析:

1> 在编译之前,预编译器会将sum.h文件中的内容拷贝到main.c中

2> 接着编译main.c和sum.c两个源文件,生成目标文件main.o和sum.o,这2个文件是不能被单独执行的,原因很简单:

* sum.o中不存在main函数,肯定不可以被执行

* main.o中虽然有main函数,但是它在main函数中调用了一个sum函数,而sum函数的定义却存在于sum.o中,因此main.o依赖于sum.o

3> 把main.o、sum.o链接在一起,生成可执行文件

4> 运行程序

说到这里,有人可能有疑惑:可不可以在main.c中包含sum.c文件,不要sum.h文件了?

大家都知道#include的功能是拷贝内容,因此上面的代码等效于:

这么一看,语法上是绝对没有问题的,main.c、sum.c都能编译成功,分别生成sum.o、main.o文件。但是当我们同时链接main.o和sum.o时会出错。原因:当链接这两个文件时链接器会发现sum.o和main.o里面都有sum函数的定义,于是报"标识符重复"的错误,也就是说sum函数被重复定义了。默认情况下,C语言不允许两个函数的名字相同。因此,不要尝试去#include那些.c源文件。

有人可能觉得分出sum.h和sum.c文件的这种做法好傻B,好端端多出2个文件,你把所有的东西都写到main.c不就可以了么?

  • 没错,整个C程序的代码是可以都写在main.c中。但是,如果项目做得很大,你可以想象得到,main.c这个文件会有多么庞大,会严重降低开发和调试效率。
  • 要想出色地完成一个大项目,需要一个团队的合作,不是一个人就可以搞的定的。如果把所有的代码都写在main.c中,那就导致代码冲突,因为整个团队的开发人员都在修改main.c文件,张三修改的代码很有可能会抹掉李四之前添加的代码。
  • 正常的模式应该是这样:假设张三负责编写 main函数,李四负责编写其他自定义函数,张三需要用到李四编写的某个函数,怎么办呢?李四可以将所有自定义函数的声明写在一个.h文件中,比如 lisi.h,然后张三在他自己的代码中用#include包含lisi.h文件,接着就可以调用lisi.h中声明的函数了,而李四呢,可以独立地在另外一个文件中(比如lisi.c)编写函数的定义,实现那些在lisi.h中声明的函数。这样子,张三和李四就可以相互协作、不会冲突。

【零基础学习iOS开发】【02-C语言】11-函数的声明和定义的更多相关文章

  1. 李洪强iOS开发之零基础学习iOS开发【02-C语言】03-关键字、标识符、注释

    上一讲中已经创建了第一个C语言程序,知道了C程序是由函数构成的,这讲继续学习C语言的一些基本语法.C语言属于一门高级语言,其实,所有的高级语言的基本语法组成部分都是一样的,只是表现形式不太一样.就好像 ...

  2. 【零基础学习iOS开发】【转载】

    原文地址:http://www.cnblogs.com/mjios/archive/2013/04/24/3039357.html 本文目录 一.什么是iOS 二.主流手机操作系统 三.什么是iOS开 ...

  3. 李洪强iOS开发之【零基础学习iOS开发】【01-前言】01-开篇

    从今天开始,我就开始更新[零基础学习iOS开发]这个专题.不管你是否涉足过IT领域,也不管你是理科生还是文科生,只要你对iOS开发感兴趣,都可以来阅读此专题.我尽量以通俗易懂的语言,让每个人都能够看懂 ...

  4. 【零基础学习iOS开发】【01-前言】01-开篇

    本文目录 一.什么是iOS 二.主流手机操作系统 三.什么是iOS开发 四.学习iOS开发的目的 五.学习iOS开发的前提 从今天开始,我就开始更新[零基础学习iOS开发]这个专题.不管你是否涉足过I ...

  5. 零基础学习iOS开发

    零基础学习iOS开发不管你是否涉足过IT领域,只要你对iOS开发感兴趣,都可以阅读此专题. [零基础学习iOS开发][02-C语言]11-函数的声明和定义 摘要: 在上一讲中,简单介绍了函数的定义和使 ...

  6. [iOS]关于零基础学习iOS开发的学习方法总结

    关于零基础学习iOS开发的学习方法总结 最近很多零基础来参加蓝鸥培训的学生经常会问到一些学习方法的问题,就如下我自己见过的好的学习方法一起讨论一下. 蓝鸥iOS开发技术的学习路线图 程序员的主要工作是 ...

  7. 李洪强iOS开发之【零基础学习iOS开发】【02-C语言】02-第一个C语言程序

    前言 前面已经唠叨了这么多理论知识,从这讲开始,就要通过接触代码来学习C语言的语法.学习任何一门语言,首先要掌握的肯定是语法.学习C语言语法的目的:就是能够利用C语言编写程序,然后运行程序跟硬件(计算 ...

  8. 李洪强iOS开发之【零基础学习iOS开发】【01-前言】02-准备

    在上一讲中,介绍了什么是iOS开发.说简单一点,iOS开发,就是开发运行在iPhone或者iPad上的软件.这么一说完,应该有很多人就会产生一些疑惑,比如学习iOS开发是不是一定要买iPhone?需不 ...

  9. 【零基础学习iOS开发】【01-前言】02-准备

    一.程序设计语言 上一讲已经说到:要想开发一款软件,首先得学习一些对应的程序设计语言. 至于iOS开发,须要学习的语言主要有:C.C++.Objective-C. 回到顶部 二.是否须要计算机专业知识 ...

随机推荐

  1. java新手笔记33 多线程、客户端、服务器

    1.Mouse package com.yfs.javase; public class Mouse { private int index = 1; private boolean isLive = ...

  2. javascript笔记——工作笔记

    1.防止普通用户缓存静态文件,每次修改之后给静态文件的应用后面加上参数后缀[项目文件较多时最好使用前端构建工具] 比如: <script src="$!webPath/resource ...

  3. Windows7下安装搭建redis教程和配置详解

    作者:Sungeek 出处:http://www.cnblogs.com/Sungeek/ 欢迎转载,也请保留这段声明.谢谢! 简介: Redis是一个开源的使用ANSI C语言编写.支持网络.可基于 ...

  4. VS2005上一个坑:关于pch 的 error C1023

    昨天编译就报错: c1xx : fatal error C1023: ‘UnicodeDebug\ImEngine.pch’ : unexpected error with pch, try rebu ...

  5. C++与Lua交互(一)

    引言 之前做手游项目时,客户端用lua做脚本,基本所有游戏逻辑都用它完成,玩起来有点不爽,感觉"太重"了.而我又比较偏服务端这边(仅有C++),所以热情不高.最近,加入了一个端游项 ...

  6. python 函数应用

    #函数的参数就是个变量 #定义函数的时候,使用关键字参数,可以指定默认值 def hello(name='reboot',age=1): return 'hello %s,your age is %s ...

  7. 使用JPA TOOLS从数据库生成Entity文件

    数据库设计好后,需要生成对应的Entity文件,这是一项不怎么需要动脑筋的工作,最好的方法是交给工具完成,手工操作很容易写错或者遗漏.这里选择的工具就是JPA TOOLS. (1)先选中工程,查看右键 ...

  8. 35 个必须有的Bootstrap工具和生成器

    Bootstraptor If you think that bootstrap templates are not enough for you, you should go with bootst ...

  9. jQuery基础与实例

    一.简介 1.什么是jQuery jQuery是一个轻量级.快速简洁的javaScript库,能让我们方便快捷的选择元素操作元素属性. 2.下载地址 3.jQuery使用方式 $("div& ...

  10. 从浏览器启动应用程序 - Application URL

    关键字:Browser,Application,URL Protocol,Windows,Mac,IE,Chrome,Safari. OS: Windows 7, OS X Yosemite. Win ...