C语言之路---函数
概念:
库函数和自定义函数
函数声明 函数定义 函数调用 函数嵌套
主调函数 被调函数
参数 形参 实参
返回值
模块化开发
一、函数声明
在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;
也可以不写形参,只写数据类型
有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。
在实际开发中,往往都是几千行、上万行、百万行的代码,将这些代码都放在一个源文件中简直是灾难,不但检索麻烦,而且打开文件也很慢,所以必须将这些代码分散到多个文件中。对于多个文件的程序,通常是将函数定义放到源文件(
.c
文件)中,将函数的声明放到头文件(.h
文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。除了函数,变量也有定义和声明之分。实际开发过程中,变量定义需要放在源文件(
.c
文件)中,变量声明需要放在头文件(.h
文件)中,在链接程序时会将它们对应起来,
二、函数定义
将代码段封装成函数的过程叫做函数定义。
如果把函数比喻成一台机器,那么参数就是原材料,返回值就是最终产品;从一定程度上讲,函数的作用就是根据不同的参数产生不同的返回值
函数不能嵌套定义 函数不能嵌套定义,但可以嵌套调用,也就是在一个函数的定义或调用过程中出现对另外一个函数的调用。
函数是一段可以重复使用的代码,用来独立地完成某个功能,它可以接收用户传递的数据,也可以不接收。接收用户数据的函数在定义时要指明参数,不接收用户数据的不需要指明,根据这一点可以将函数分为有参函数和无参函数。
1)、无参函数的定义
无返回值函数
有的函数不需要返回值,或者返回值类型不确定(很少见),那么可以用 void 表示,例如:
2)、有参函数的定义
多个参数之间由
,
分隔。参数本质上也是变量,定义时要指明类型和名称。与无参函数的定义相比,有参函数的定义仅仅是多了一个参数列表。
数据通过参数传递到函数内部进行处理,处理完成以后再通过返回值告知函数外部。
函数定义时给出的参数称为形式参数,简称形参;
函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。
函数调用时,将实参的值传递给形参,相当于一次赋值操作
原则上讲,实参的类型和数目要与形参保持一致。如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型,
参数
返回值
函数返回值有固定的数据类型(int、char、float等),用来接收返回值的变量类型要一致。
凡不要求返回值的函数都应定义为 void 类型。
return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个 return 语句被执行,所以只有一个返回值
函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用。return 语句是提前结束函数的唯一办法
三、模块化开发
1、多文件编程
C语言中,我们可以将一个.c
文件称为一个模块(Module);所谓模块化开发,是指一个程序包含了多个源文件(.c 文件)以及头文件(.h 文件)。
在C语言中,模块之间的依赖关系主要有两种:一种是模块间的函数调用,另外一种是模块间的变量访问。
.h 和 .c 在项目中承担的角色不一样:.c 文件主要负责实现,也就是定义函数和变量;
.h 文件主要负责声明(包括变量声明和函数声明)、宏定义、类型定义等。
这些不是C语法规定的内容,而是约定成俗的规范,或者说是长期形成的事实标准。
根据这份规范,头文件可以包含如下的内容:
- 可以声明函数,但不可以定义函数。
- 可以声明变量,但不可以定义变量。
- 可以定义宏,包括带参的宏和不带参的宏。
- 结构体的定义、自定义数据类型一般也放在头文件中。
- 在项目开发中,我们可以将一组相关的变量和函数定义在一个 .c 文件中,并用一个同名的 .h 文件(头文件)进行声明,其他模块如果需要使用某个变量或函数,那么引入这个头文件就可以。
这样做的另外一个好处是可以保护版权,我们在发布相关模块之前,可以将它们都编译成目标文件,或者打包成静态库,只要向用户提供头文件,用户就可以将这些模块链接到自己的程序中。
2、C语言头文件的路径
尖括号< >:编译器会到系统路径下查找头文件;
双引号" ":编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
使用双引号比使用尖括号多了一个查找路径,它的功能更为强大,我们完全可以使用双引号来包含标准头文件
这里所说的“系统路径”和“当前路径”是什么意思呢?
3、绝对路径和相对路径
以 Windows 为例,在 D 盘下创建一个自定义的文件夹,名字为
abc
,它里面有一个头文件叫做xyz.h
,那么在程序开头使用#include "D:\\abc\xyz.h"
就能够引入该头文件。
a、绝对路径
像D:\\abc\xyz.h
这种从盘符开始、完整地描述文件位置的路径就是绝对路径(Absolute Path)。
绝对路径从文件系统的“根部”开始查找文件:
1) 在 Windows 下,根部就是 C、D、E 这样的盘符,例如D:\\a.h
、E:\images\123.jpg
、E:/videos/me.mp4
、D://abc/xyz.h
等,分隔符可以是正斜杠/
也可以是反斜杠\
,盘符后面的斜杠可以有一个也可以有两个。
2) Linux 没有盘符,根部就是/
,例如/home/xxx/abc.h
、/user/include/module.h
等,分隔符只能是正斜杠/
,比 Windows 简洁很多。6
为了增强代码的可移植性,引入头文件时请尽量使用正斜杠/
。
b、相对路径
相对路径(relative path)是从当前目录(文件夹)开始查找文件;当前目录是指需要引入头文件的源文件所在的目录,这也是本文开头提到的“当前路径”。
以 Windows 为例,假设在E:/cDemo/
中有源文件 main.c 和头文件 xyz.h,那么在 main.c 中使用#include "./xyz.h"
语句就可以引入 xyz.h,其中./
表示当前目录,也即E:/cDemo/
。需要注意的是,我们可以将
./
省略,此时默认从当前目录开始查找,例如#include "xyz.h"
、#include "include/xyz.h"
、#include "../xyz.h"
、#include "../include/xyz.h"
。
在实际开发中,我们都是将头文件放在当前工程目录下,非常建议大家使用相对路径,这样即使后来改变了工程所在目录,也无需修改包含语句,因为源文件的相对位置没有改变。
4、系统路径
当使用相对路径的方式引入头文件时,如果使用
< >
,那么“相对”的就是系统路径,也就是说,编译器会直接在这些系统路径下查找头文件;如果使用" "
,那么首先“相对”的是当前路径,然后“相对”的才是系统路径,也就是说,编译器首先在当前路径下查找头文件,找不到的话才会继续在系统路径下查找。
而使用绝对路径的方式引入头文件时,< >和" "没有任何区别,因为头文件路径已经写死了(从根部开始查找),不需要“相对”任何路径。
总起来说,相对路径要有“相对”的目标,这个目标可以是当前路径,也可以是系统路径,< >
和" "
决定了到底相对哪个目标。
5、条件编译(防止头文件被重复包含)
对于头文件的交叉包含的现象,有一种行之有效的方案,使得头文件可以被包含多次,但效果与只包含一次相同。
在实际开发中,我们往往使用宏保护来解决这个问题:
这种宏保护方案使得程序员可以“任性”地引入当前模块需要的所有头文件,不用操心这些头文件中是否包含了其他的头文件。
#ifndef x (if not define) //先测试x是否被宏定义过
#define x
程序段1 //如果x没有被宏定义过,定义x,并编译程序段1
#endif
程序段2 //如果x已经定义过了则编译程序段2的语句,“忽视”程序段1。
#ifndef 和 #endif 要一起使用,如果丢失#endif,可能会报错。
这是宏定义的一种,它可以根据是否已经定义了一个变量来进行分支选择,一般用于调试等等.实际上确切的说这应该是预处理功能中三种(宏定义,文件包含和条件编译)中的一种----条件编译。
每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前面加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STDIO_H
#define _STDIO_H
......
#endif
但是在c++语言中,#ifndef的作用域只是在单个文件中。所以如果h文件里定义了全局变量,即使采用#ifndef宏定义,一个c文件包含同一个h文件多次还是会出现全局变量重定义的错误。
6、C语言static变量和函数
全局变量和函数的作用域默认是整个程序,也就是所有的源文件,有时候也会引发命名冲突的问题。
实际开发中,我们通常将不需要被其他模块调用的全局变量或函数用 static 关键字来修饰,static 能够将全局变量和函数的作用域限制在当前文件中,在其他文件中无效。
使用 static 修饰的变量或函数的作用域仅限于当前模块,对其他模块隐藏,利用这一特性可以在不同的文件中定义同名的变量或函数,而不必担心命名冲突。
static 除了可以修饰全局变量,还可以修饰局部变量,被 static 修饰的变量统称为静态变量(Static Variable)
不管是全局变量还是局部变量,只要被 static 修饰,都会存储在全局数据区(全局变量本来就存储在全局数据区,即使不加 static)。
全局数据区的数据在程序启动时就被初始化,一直到程序运行结束才会被操作系统回收内存;对于函数中的静态局部变量,即使函数调用结束,内存也不会销毁。
总结起来,static 变量主要有两个作用:
1) 隐藏
程序有多个模块时,将全局变量或函数的作用范围限制在当前模块,对其他模块隐藏。
2) 保持变量内容的持久化
将局部变量存储到全局数据区,使它不会随着函数调用结束而被销毁。