09. 函数
一、什么是函数
C 程序是由 函数(function) 组成的,我们写的代码都是由主函数 main() 开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。也就是说,main() 函数可以调用其它函数,其它函数也可以相互调用。在 main() 函数中调用其它函数,这些函数执行完毕之后又返回到 main() 函数中。通常,我们把这些被调用的函数称为下层函数。函数调用发生时,立即执行被调用的函数,二调用者则进入等待的状态,直到被调用者执行完毕。
使用函数后,我们可以省去编写重复代码的苦差。并且,函数让程序更加模块化,从而提供了编写代码的可读性,更方便后期修改、完善。
从函数定义的角度来看,函数可以分为系统函数和用户定义函数两种:
- 系统函数:也称为库函数,这是由编译系统提供过的,用户不必自己定义这些函数,可以直接使用它们(需要 #include 对应的头文件),如我们常用的打印函数 printf();
- 用户定义函数:用户自己编写的,用于解决用户的专门需要;
使用函数,我们可以省去重复代码的编写,降低代码重复率,让程序更加模块化,从而有利于程序的阅读,修改和完善。
二、函数的定义
C 语言的库函数在编写程序时时可以直接调用的,而自定义函数则必须由用户对其进行定义,在其函数的定义中完成函数特定的功能,这样才能被其它函数调用。
返回值类型 函数名(形参列表)
{
函数体;
return 返回值;
}
【1】、返回值类型
- 如果方法有返回值,则必须在方法声明时指定返回值类型,同时,方法中需要使用 return 关键字返回指定类型的变量或常量:"retrun 数据;"。
- 如果方法没有返回值,则方法声明时,使用 void 关键字。通常没有返回值的的方法中,就不需要使用 return,但是,如果使用的话,只能 "return;" 表示结束此方法的意思。
return 关键字可以用来结束方法;
如果函数返回的类型和 return 语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。
如果函数返回的类型和 return 语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。
【2】、方法名
方法名属于标识符,遵循标识符的规格和规范,见名知意;
【3】、形参列表
在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值。
- 方法可以声明 0个、1个、或多个形参。多个形参用
,
分割 - 格式:数据类型1 形参1,数据类型2 形参2,……
C语言 中,支持可变参数,即参数的个数可以不确定,使用 ... 表示。如果要使用可变参数,需要引入头文件 stdarg.h,例如:int fun(int num,...),其中 num 表示传递的参数格式,... 表示可以传递多个参数,参数的类型与 num 对应即可;
【4】、函数体
函数体是由一对大括号括起来的,大括号决定了函数体的范围。函数要实现的特定功能,都是在函数体部分通过代码完成的,最后通过 return 语句返回是实现的结果。
void getSum(int num1,int num2)
{
int result = num1 + num2;
return result;
}
方法不调用就不执行;
方法和方法之间是平级关系,不能相互嵌套定义;
方法的编写顺序和执行顺序无关;
三、函数声明
如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。
所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。函数的声明是要编辑器知道函数的名称、参数、返回值类型等信息。
函数的声明格式 由 函数返回值类型、函数命名、函数列表 和 分号 四个部分组成的,函数声明格式如下:
返回值类型 函数名(参数列表);
int getSum(int num1, int num2);
int getSum(int, int);
一个函数只能被定义一次,但可以声明多次。
函数定义 和 声明的区别:
函数定义 是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
函数声明 的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)通知编译系统,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。
四、函数的调用
定义函数后,我们需要调用此函数才能执行到这个函数里的代码段。这和 main() 函数不一样,main() 为编译器设定好自动调用的主函数,无需人为调用,我们都是在 main() 函数里调用别的函数,一个 C 程序里有且只有一个 main() 函数。
函数调用的格式如下:
数据类型 变量名 = 函数名(参数列表);
在函数调用过程中传递的参数称为实参(实际参数),实参由具体的值。在函数定义过程中,参数称为形参(形式参数)。在函数调用过程中,将实参传递给形参。在函数调用结束,被调函数会在内存中自动销毁。
- 如果是调用无参函数,则不能加上“实参”,但括号不能省略。
- 实参与形参的个数应相等,类型应匹配(相同或赋值兼容)。实参与形参按顺序对应,一对一地传递数据。
- 实参可以是常量、变量或表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。
- 如果函数定义有返回值,这个返回值我们根据用户需要可用可不用,但是,假如我们需要使用这个函数返回值,我们需要定义一个匹配类型的变量来接收。
int result = getSum(10, 20);
方法在调用的时候,参数的数量与类型必须与方法定义中小括号里面的变量一一对应,否则程序将报错。
在 C 语言中,函数的定义都是互相平行、独立的,也就是说,一个函数体内不能包含定义的另一个函数,但是可以嵌套调用函数,也就是说,在一个函数体内可以调用另外一个函数。
当调用(执行)一个函数时,就会开辟一个独立的空间(栈),每个栈空间都是相互独立的。如果函数有返回值,则将返回值赋给接收的变量。当函数执行完毕后或者执行到 return 语句,该函数对应的栈空间就会销毁,程序会返回到调用函数的位置,继续执行。
调用函数时,开辟的函数栈,都会完整的执行函数代码;
五、函数的返回值
如果函数的返回值类型与函数声明的返回值类型不一致时,实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值。
#include <stdio.h>
int division(double num1, double num2);
int main(void)
{
double num1 = 5, num2 = 2;
double result = 0;
result = division(num1, num2);
printf("result: %f\n", result);
return 0;
}
int division(double num1, double num2)
{
double result = num1 / num2;
return result;
}
六、函数的参数
在调用函数时,大多数情况下,主调函数 和 被调函数 之间由数据传递关系。函数参数的作用时传递数据给函数使用,函数利用接收的数据进行具体的操作处理。
6.1、函数的形参和实参
在定义函数时,函数名后面括号中的变量名称为 “形式参数”,在函数调用之间,传递给函数的值将被复制到这些形式参数中。形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。
在调用一个函数时,也就是真正使用一个函数时,函数名后面括号中的参数为“实际参数”。实参出现在主调函数中,进入被调函数后,实参也不能使用。
实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
#include <stdio.h>
int expansion10(int num);
int main(void)
{
int num = 10;
int result = 0;
result = expansion10(num);
printf("num: %d\n", num);
printf("result: %d\n", result);
return 0;
}
int expansion10(int num)
{
num *= 10;
return num;
}
函数定义时,列表的参数就是形式参数;而函数调用时,传递进来的参数就是实际参数
6.2、值传递与地址传递
C 语言传递参数(或者赋值)可以是 值传递(pass by value),也可以 传递指针(a pointer passed by value),传递指针也叫 地址传递。默认 传递值 的类型有 基本数据类型(整型类型、浮点类型、字符类型)、结构体、共用体。默认 传递地址 的类型有 指针、数组。值传递是指将变量指向的存储内容,在传递/赋值时,拷贝一份给接收变量。地址传递分为两种情况:如果是指针,就将指针变量所存储的地址值传递给接收变量,如果是数组,就将数组的首地址传递给接收变量。
传值调用的函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
其实,不管是 值传递 还是 引用传递,传递给函数的都是变量的副本,不同的是,值传递 的是 值的拷贝,引用传递 是 地址的拷贝。一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据的大小,数据越大,效率越低。
我们可以数组名作为函数参数。当数组作为函数的实际参数时,只传递数组的地址,而不是将这个数组赋值到函数中。当用数组名作为函数为实际参数调用函数时,指向该数组的第一个元素的指针就被传递到函数中。
#include <stdio.h>
void arrayExpansion10(int array[] ,int length);
int main(void)
{
int array[] = {1,2,3,4,5,6,7,8,9};
int length = sizeof(array)/sizeof(int);
arrayExpansion10(array, length);
for (int i = 0; i < length; i++)
{
printf("%d\t", array[i]);
}
printf("\n");
return 0;
}
void arrayExpansion10(int array[] ,int length)
{
int i = 0;
for(i = 0; i < length; i++)
{
array[i] *= 10;
}
}
C 语言中没有任何下标的数组名就是一个指向该数组第一个元素的指针;
如果希望函数内的变量能修改函数外的变量,可以传入变量的地址(&),函数以指针的方式操作变量(*指针);
七、局部变量与全局变量
作用域的作用就是决定程序中的哪些语句是可用的,换句话说,就是在程序中的可见性。作用域 包括 局部作用域 和 全局作用域。局部变量具有局部作用域,而全局变量具有全局作用域。
7.1、局部变量
在一个函数内部定义的变量是 局部变量。这些变量声明在函数内部,无法被其它函数所使用。函数的形式参数也属于局部变量,作用范围只限于函数内部的所有语句块。
在 C 语言中位于不同作用域的变量可以使用相同的标识符,也就是可以为变量起相同的名称。如果内层作用域中定义的变量和已经声明的某个外层作用域中的变量具有相同的名字,在内层中使用这个变量名,那么内层作用域中的变量将屏蔽外层作用域中的那个变量,直到结束内层作用域为止,即 局部优先原则。
#include <stdio.h>
int getSum(int num1,int num2);
int main(void)
{
int num1 = 10,num2 = 20;
int result = 0;
result = getSum(num1,num2);
printf("%d\n",result);
return 0;
}
int getSum(int num1,int num2)
{
int result = num1+num2;
return result;
}
在语句块内声明的变量仅在该语句块内部起作用,当然也包括嵌套在其中的子语句块。
7.2、全局变量
如果一个变量在所有函数的外部声明,这个变量就是 全局变量。全局变量可以在程序中的任何位置进行访问的变量。定义全局变量的作用是增加函数间联系的渠道。由于同一个文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响到其它函数,相当于各个函数间有直接传递通道。
如果不对全局变量进行初始化,那么它会自动初始化为默认值:
数据类型 | 默认值 |
---|---|
整型 | 0 |
浮点型 | 0.0 |
字符型 | '\0' |
指针 | NULL |
#include <stdio.h>
double getCircleArea(int radius);
double getCirclePerimeter(int radius);
double pi = 3.14;
int main(void)
{
double radius = 0;
double perimeter = 0;
double area = 0;
printf("请输入圆的半径:");
scanf("%lf",&radius);
perimeter = getCirclePerimeter(radius);
printf("圆的周长为:%.2f\n",perimeter);
area = getCircleArea(radius);
printf("圆的面积为:%.2f\n",area);
return 0;
}
double getCircleArea(int radius)
{
return pi * radius * radius;
}
double getCirclePerimeter(int radius)
{
return 2 * pi * radius;
}
全局变量不属于某个函数,而属于整个源文件。但是如果外部函数要进行使用,则要用 extern 关键字进行修饰。
八、静态变量
局部变量 被 static 关键字修饰后,称为 静态局部变量。静态局部变量在声明时未赋初值,编译器也会把它初始化为默认值。静态局部变量存储与进程的静态存储区,它只会被初始化一次,即使函数返回,它的值也会保持不变。
#include <stdio.h>
int getCount(void);
int main(void)
{
int count = 0;
count = getCount();
count = getCount();
count = getCount();
printf("getCount()函数执行了%d次\n",count);
return 0;
}
int getCount(void)
{
static int num;
return ++num;
}
static 修饰局部变量改变了局部变量的生命周期,本质上是改变了变量的存储类型。
static 修饰全局变量,使得这个全局变量只能在自己所在的源文件(.c)内部可以使用,其它源文件不能使用;全局变量在其它源文件内部可以被使用,是因为全局变量具有外部链接属性,但是被 static 修饰后,就变成了内部链接属性。这时,其它源文件不能连接到这个静态的全局变量了。
#include <stdio.h>
static double pi = 3.14;
int main(void)
{
printf("%lf\n",pi);
return 0;
}
九、内部函数和外部函数
函数是 C 语言程序中的最小单位,往往把一个函数或多个函数保存为一个文件,这个文件称为源文件。定义一个函数,该函数就会被另外的函数所调用。但当一个源程序由多个源文件组成时,可以指定函数不能被其它文件调用。这样,C语言 又把函数分为两类:一个是 内部函数,另一个是 外部函数。
9.1、内部函数
定义一个函数,如果希望这个函数只被所在的源文件使用,那么就称这样的函数为 内部函数。内部函数 又称为 静态函数。使用内部函数,可以是函数只局限在函数所在的源文件中,如果在不同的源文件中又同名的内部函数,则这些同名函数是互不干扰的。如果要使用内部函数,需要在函数的返回值前面加上 static 关键字进行修饰。
static 返回值类型 函数名(参数列表)
{
方法体;
返回值;
}
#include <stdio.h>
static void printSomething(char something[]);
int main(void)
{
printSomething("hello world!\n");
return 0;
}
static void printSomething(char something[])
{
printf("%s\n",something);
}
static 修饰函数,使得函数只能在自己所在的源文件内部使用,不能再其它源文件内部使用。本质上,static 是将函数的外部链接属性变成了内部链接属性;
不同的文件可以使用相同的静态函数,互不影响;
9.2、外部函数
外部函数是可以被其它源文件调用的函数。定义外部函数的格式与普通的函数一样,但是需要在函数声明的时候加上 extern 关键字进行修饰。
extern 返回值类型 方法名(形参列表);
#include <stdio.h>
extern void printSomething(char something[]);
int main()
{
printSomething("hello world!\n");
return 0;
}
extern void printSomething(char something[])
{
printf("%s\n",something);
}
在 C语言 定义函数时,如果不指明函数是内部函数还是外部函数,那么默认将函数指定为外部函数,也就是说,定义外部函数时可以省略关键字 extern;
十、多文件编程
在实际开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,因此我们将 函数声明 和 宏定义 可以放在头文件中。头文件是扩展名为 .h 的文件,它可以被多个源文件中引用共享。在程序中要使用头文件,需要使用 C预处理指令 #include
来引用它。#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果类似。
在 C语言 中,include 指定的书写格式有两种,分别是 #include <头文件名.h>
和 #include "头文件名.h"
,它们的区别如下:
#include <头文件名.h>
引用的是编译器的类库路径的头文件,用于引用 系统头文件。
#include "头文件名.h"
引用的是你程序目录的相对路径中的头文件,如果在程序目录中没有找到引用的头文件则到编译器的类库路径的目录下查找该头文件,用于引用 用户自定义的头文件。
所以,引用 系统头文件,两种形式都可以,#include <头文件名.h>
的效率高,引用 用户自定义的头文件,只能使用 #include "头文件名.h"
。
把函数声明放在头文件 xxx.h 中,在主函数中包含相应头文件。在头文件对应的 xxx.c 中实现 xxx.h 声明的函数。当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。为了避免同一个文件被 include 多次,C/C++中有两种方式,一种是 #ifndef
方式,一种是 #pragma once
方式。
方式一:使用 #ifndef 方式
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
// 声明语句
#endif
方式二:使用 #pragma once 的方式
#pragma once
// 声明语句
template.c 内容如下:
#include <stdio.h>
#include "test.h"
int main(void)
{
printSomething("hello world!\n");
return 0;
}
test.c 内容如下:
#include <stdio.h>
void printSomething(char something[])
{
printf("%s\n",something);
}
test.h 内容如下:
#pragma once
void printSomething(char something[]);
建议把所有的常量、宏、系统全局变量 和 函数原型 写在头文件中,在需要的时候随时引用这些头文件;
引用头文件相当于复制头文件的内容;
源文件的名字可以不和头文件一样,但是为了好管理,一般头文件名和源文件名一样;
一个 #include 指令只能包含一个头文件,多个头文件需要多个 #include 命令引入;
如果一个文件多次引用头文件,多次引入的效果和一次引入的效果相同,引文头文件在代码层面上有防止重复引入的机制;
一个被包含的文件(.c)中又可以包含另一个文件头文件(.h);
不管是标准头文件,还是自定义头文件,最好只包含变量和函数的声明,不能包含定义,否则在多次引入时,可能会引起重复定义错误;