C语言之函数
提出问题:
从n个数据中取出m(m<=n) 个数据组成一组,总共可以形成多少种组合?
求组合数的公式如下:
程序:
#include <stdio.h>int main( )
{int m, n, x=1, y=1, z=1, i, cmn; scanf("%d%d", &m, &n);for(i=1; i<=n; i++) /* 求 n! */ x = x * i;for(i=1; i<=m; i++) /* 求 m! */ y = y * i;for(i=1; i<=n-m; i++) /* 求 (n-m)! */ z = z * i;cmn = x / (y * z); printf("Cmn = %d", cmn);return 0;
}
由程序可以看出,求阶乘都是相似类型的操作,那是否可以减少相似类型的操作而使程序变得简洁一些?这样就引出了函数这个概念
一, 函数概述:
1.1 什么是函数?
函数就是完成一定功能的程序代码。
1.2 模块化(结构化)程序设计:
将一个复杂的功能划为为不同的功能模块,把复杂问题分解为若干较小的、功能简单的、相互独立又相互关联的模块来进行设计,最终通过功能模块的"组合"实现复杂功能,这样的一种程序设计方法。
在C语言中,函数(function)是构成程序的基本模块。 一个C程序由一个或多个函数组成,有且仅有一个主函数,即main()函数。 每个函数完成一个相对独立的且功能明确的任务。 由主函数调用其他函数,其他函数也可以互相调用。 同一个函数可以被一个或多个函数调用任意多次。
1.3 使用函数的目的?
1. 方便功能模块可被别人使用,就像我们使用别人编写的功能一样;
2. 减少程序中重复性的代码
3. 实现模块化程序设计
1.4 函数分类:
1. 函数的实现方式:
标准库函数:例如 scanf()、printf()、fabs()、sqrt()等
用户自定义函数 :例如add()。
2. 函数的形式:
有参函数:主调函数通过参数向被调函数传递数据。 一般执行被调函数会得到一个返回值,供主调函数使用。
无参函数:主调函数不向被调函数传递数据。用来执行指定的一组操作,一般不返回值。
相关概念: 主调函数: 调用其他函数的函数
被调函数: 被调用的函数
二, 函数定义:
函数组成: 1. 函数首部(函数头)
2. 函数体
定义格式: 类型标识符 函数名(形式参数列表)
{
函数体语句;
}
函数名:一个有效的标识符。
类型标识符:返回值的类型说明符。
void :表示函数不返回任何值。
形式参数列表: 参数是用于接收主调函数传递的数据的。参数列表中的参数可以有多个,多个参数之间,隔开,并且每一个参数必须要指定参数类型。
参数列表可以省略,但是函数名后的()不能省略,省略了参数列表的函数称为无参函数,它表明函数被调用时,不接收主调函数传递的任何数据。
定义无参函数:
定义无参函数的一般形式为:
类型标识符 函数名( )
{
函数体
}
函数不接受主调函数传递过来的数据,所以没用参数。没有了参数,所以称为无参函数。
定义有参函数:
定义有参函数的一般形式为:
类型标识符 函数名( 形式参数列表)
{
函数体
}
如果省略函数的类型标识符,则默认为是int型。
主调函数通过参数向被调函数传递数据。
定义空函数:
定义空函数的形式为:
类型标识符 函数名( )
调用此函数时,什么都不做。常用来在准备扩充功能的地方先写上一个空函数,以方便以后使用。
在定义C函数时要注意以下几点:
1.函数类型标识符变量类型说明符相同,它表示返回的函数值的类型。
2.在C语言中还可以定义无类型(即void类型)的函数,这种函数不返回函数值,只是完成某种功能。
3.如果省略函数的类型标识符,则默认为是int型。
4.函数中返回语句的形式为 return(表达式);或 return 表达式;其作用是将表达式的值作为函数值返回给调用函数。其中表达式的类型应与函数类型一致。
5.如果“形参表列”中有多个形式参数,则它们之间要用 ,分隔。
6.如果形参表中有多个形参,即使它们的类型是相同的,在形参表中也只能逐个进行说明。
例子:
计算并输出两个圆面积之和。
#include "stdio.h"
double q(double r) /*计算圆面积的函数,为双精度实型*/{ double s;s=3.1415926*r*r;return(s);}int main(){ double r1,r2;printf("input r1 ,r2: "); /*输入前的提示*/scanf("%lf,%lf",&r1,&r2); /*输入r1与r2*/printf("s=%f\n",q(r1)+q(r2));return 0;}
运行结果:
三,形参与实参
形参(形式参数): 定义函数时,函数名后括号中的变量称为形式参数。
函数定义时,系统是不会为参数分配内存,所以说参数是形式上的,形参只有当函数被调用时才会分配内存;
实参(实际参数): 函数被调用时,传递给函数的数据,实际参数与形式参数可以同名,但两者占据不同的内存空间。
关于形参与实参的说明:
1.在函数调用时才给形参分配存储空间,且调用结束后所占空间立即释放。
2.实参可以是常量、变量、表达式,只要有确定的值。
3.实参和形参必须类型相同。若不同时,按赋值规定自动进行类型转换。
4.在C语言中,实参向形参的数据传递是“值传递”,是单向传递,即只由实参传给形参,而不能由形参传回来给实参。在内存中,实参、形参占不同的存储单元,因此形参值的改变不会影响实参值。
四,函数的返回值:
通过调用函数使主调函数得到一个确定的值,这就是函数的返回值。
函数的返回值: 返回给主调函数的结果数据。
1)函数的返回值是通过 return 语句来实现的。
1.若不需要返回值,函数中可以没有return语句。
2.一个函数中可以有多个return语句,但任一时刻只有一个return语句被执行。
3.return后面的值可是常量、变量或表达式,且圆括号可有可无,返回的是其值。如:
int max(int x,int y) { return (x>y ? x:y); } /*完成了计算和返回两个功能*/
return 语句:
格式: return (表达式) ;
功能: return 语句不仅仅是为了给主调函数返回数据,而是结束函数。
2)函数值的类型
若需要返回值,定义函数时一定要说明函数的类型;
凡不加类型说明符的函数,C语言按int型对待,但C++必须要说明函数类型;
3)定义函数时指定的函数类型一般应和return语句中的表达式类型相同。若函数值的类型和 return语句中表达式的值不一致时,以函数类型为准。对数值型数据可以自动进行类型转换。
4)对不要求返回值的函数,应用“void”定义为无类型(空类型),以保证函数不返回任何值,而不论是否有return
五,函数使用(函数调用)
函数的调用过程: 当主调函数调用被调函数时,主调函数暂停运行,转而执行被调函数,当被调函数返回,主调函数再从之前暂停处继续执行。
函数的调用方式:
1. 常见调用方式:
1) 函数名(实参列表);
2) 函数表达式; 将函数调用放置在一个表达式语句中;
3) 函数参数, 将函数调用作为另一个函数的参数。
2. 函数的嵌套调用
被调函数有作为主调函数调用其他函数。这样的函数调用形式,称为函数嵌套调用
3. 函数的递归调用
在函数内部直接或者间接的调用了自身,这样的函数调用形式,称为函数递归调用
C语言函数调用、定义与声明的核心原则:
在C语言中,程序的基本构成单位是函数。要正确地使用函数,必须遵循“先声明(或定义),后调用”的黄金法则。编译器在从上到下解析代码时,遇到一个函数调用,它必须提前知道这个函数的信息,即函数的返回类型和参数类型、个数。
1.函数定义:
函数定义是指函数体的具体实现,它包含了函数要执行的所有代码。一个完整的函数定义包括:
函数头 (Function Header): 函数返回类型 函数名(参数类型 参数名1, ...)
函数体 (Function Body): { 和 } 之间包裹的代码块
这是一个完整的函数定义:
int add(int a, int b) // 函数头
{ // 函数体开始int sum = a + b;return sum;
} // 函数体结束
2.函数调用:
函数调用就是使用已经定义好的函数执行特定的任务
例如:
int main()
{int result = add(5, 3); // 这就是对add函数的调用return 0;
}
3.函数声明:
当在一个函数(如 main)中调用另一个函数(如 add)时,如果 add 函数的完整定义出现在 main 函数的后面,编译器在读到 main 里面的 add(5, 3) 时,就不知道 add 是什么。它不知道 add 需要几个参数、参数是什么类型、返回值又是什么类型。
为了解决这个问题,C语言引入了函数声明(也常被称为函数原型)。
作用:函数声明就像一个“预告”,它提前告诉编译器某个函数的基本信息,让编译器在没有看到完整函数定义的情况下,也能正确地检查函数调用是否合法。
声明格式:
函数声明本质上就是函数头加上一个分号 ;。
它提供了三项关键信息给编译器:
函数名:要调用的函数叫什么。
参数列表:函数需要接收什么类型、多少个参数。
返回类型:函数执行完毕后会返回一个什么类型的值。
具体的声明方式:
1.包含形式参数名:
函数返回类型 函数名(参数类型1 形参名1, 参数类型2 形参名2, ...);
int add(int a, int b);
2.不包含形式参数名:
函数返回类型 函数名(参数类型1, 参数类型2, ...);
int add(int, int);
当函数没有参数,则应该使用void关键字。
示例:
void print_hello(void);
对于函数定义与调用声明的两种情况:
1.先定义后调用(无需额外声明)
#include <stdio.h>// 1. 函数定义在main函数之前
int add(int a, int b) {return a + b;
}int main() {// 2. 直接调用,此时编译器已经知道add函数的一切int result = add(5, 3);printf("Result is: %d\n", result);return 0;
}
2.先调用,后定义(必须进行声明)
#include <stdio.h>// 1. 函数声明 (Function Prototype)
// 提前告诉编译器,后面会有一个叫'add'的函数
int add(int, int); // 或者 int add(int a, int b);int main() {// 2. 函数调用// 编译器根据上面的声明来检查这次调用是否正确int result = add(5, 3);printf("Result is: %d\n", result);return 0;
}// 3. 函数定义 (Function Definition)
// 函数的具体实现
int add(int a, int b) {return a + b;
}
六,函数的嵌套调用和递归调用
1.嵌套调用
C语言虽不允许嵌套定义函数,但可以嵌套调用函数。
例如:输入4个整数,找出其中最大的数。用函数的嵌套调用来处理
分析:在主函数中调用一个max_4函数来求4个整数中的最大数。然后在max_4函数中再调用一个max_2函数来求2个整数中的最大数。最后在主函数中输出结果。
编写程序如下:
#include <stdio.h>
int main()
{ int max_4(int a,int b,int c,int d); /*函数max_4原型声明*/int a,b,c,d,max;printf("Please enter 4 interger numbers:");scanf("%d%d%d%d",&a,&b,&c,&d);max=max_4(a,b,c,d); /*调用函数max_4*/printf("max=%d \n",max);return 0;
} int max_4(int a,int b,int c,int d) /*定义函数max_4*/
{ int max_2(int a,int b); /*函数max_2原型声明*/int m; m=max_2(a,b); /*调用函数max_2,求a、b中的大者存入m*/m=max_2(m,c); /* 求m、c中的大者存入m*/m=max_2(m,d); /* 求m、d中的大者存入m*/return (m); /*返回m值便是4个中的大者*/
}int max_2(int a,int b) /*定义函数max_2*/
{ if (a>b) return a;else return b; /* 函数返回值是a和b中的大者 */
}
2.递归调用
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就在于允许函数的递归调用。
递归本质: 递归的本质是一种循环;
实现递归主要事项:
1. 递归必须要有结束的条件。
2. 递归需要慢慢向结束条件进行逼近,最终达到结束递归;
3. 先进行递归结束条件的判断,然后才进行递归。
例:直接递归
int f ( int x)
{ int y,z;z=f(y); /* 在执行f函数的过程中又要调用f函数自己 */return(2*z);}
间接递归
通过别的函数调用自身。例如,两个函数之间的调用关系就属于间接递归调用。
例:用递归方法求n!
分析:
求n!也可以用递归方法,即:5!=4!×5 , 4!=3!×4 …… 1!=1
可用下面的递归公式表示:
n!=1 (n=0,1)
n!=n*(n-1)! (n>1)
#include <stdio.h>long fac(int n); int main() {int n;long y;printf("input an integer number: ");scanf("%d", &n);y = fac(n);// 在主函数中检查fac函数返回的错误信号if (y == -1) {// 如果返回-1,说明输入有误,fac函数内部已经打印了原因// main函数就不再打印阶乘结果了} else {printf("%d! = %ld\n", n, y);}return 0;
}long fac(int n) {long f;// 1. 处理无效输入if (n < 0) {printf("n<0, data error!\n");return -1; // 立即返回一个错误码,不再继续执行}// 2. 正确处理基本情况 (Base Case)// 使用逻辑或 || if (n == 0 || n == 1) {f = 1;}// 3. 递归步骤 (Recursive Step)else {f = fac(n - 1) * n;}return f; // 返回计算好的值
}
七,数组做函数参数:
数组做函数参数:
注意事项:
1. 数组做实参,形参也需要是数组;
2. 数组做实参,不代表传递的是数组中所有元素,而传递是第一个元素在
内存的地址;
3. 形参数组接收了实参数组地址,此时,形参数组与实参数组共同占据
相同的内存空间。
4. 因为形参数组与实参数组共同占据相同的内存空间,则形参数组元素的修改
会更新实参数组的元素。
5. 因为实参数组传递的是首元素的地址,相当于传递了数组中元素的开始位置,
所以在数组做参数时,往往是要额外提供一个表示数组元素个数的参数,以便
表示形参在访问数组元素时,该何时结束。
例:
编写一个函数,用来分别求数组score_1(有5个元素)和数组score_2(有10个元素)各元素的平均值 。
分析:要求两个不同大小的数组中元素的均值,在函数中设一个整型形参接受调用时传递过来的元素个数;用数组名作函数的实参时,传递数组的首地址,使形参数组与实参数组占用同一段内存单元。当形参数组元素的值变化时,对应的实参数组元素的值也发生了改变。
#include <stdio.h>
void main()
{ float average(float array[ ],int n);float score_1[5]={98.5,97,91.5,60,55};float score_2[10]={67.5,89.5,99,69.5,77,89.5,76.5,54,60,99.5};printf("The average of class A is %6.2f\n",average(score_1,5));printf("The average of class B is %6.2f\n",average(score_2,10));}
float average(float array[ ],int n){ int i; float aver,sum=array[0];for(i=1;i<n;i++)sum=sum+array[i];aver=sum/n;return (aver);
}
八,变量的作用域(作用范围)
变量的作用域: 变量的作用范围,
引入问题: 我们定义函数,往往是要考虑函数参数的设计,我们知道参数是用于传递数据的,如果数据可以在多个函数中都能被使用,则我们就无需提供参数来传递数据。那么什么样的数据能够被多个函数访问,这就涉及到变量的作用域问题的。
根据变量的作用域不同,变量分为两大类:
1. 局部变量:
函数内定义的变量,称为局部变量,局部变量的作用范围仅限于函数内,
函数外就无法访问该变量了。
2. 全局变量(外部变量)
函数外定义的变量,称为外部变量(全局变量),该变量的作用从变量定义处到
本文件的末尾。
使用全局变量的优缺点:
优点:
1. 使用全局变量可以达到一个函数对外输出多个数据的效果;
2. 使用全局变量可以减少函数参数,而且降低因数据传递而造成的时间消耗
缺点:
1. 全局变量会在程序运行期间一直占据内存空间,直到程序结束;
2. 全局变量的使用会增加模块(函数)之间的耦合性,与程序设计要求
"高内聚,低耦合"是违背的;
3. 降低程序的通用性,函数使用全局变量,则在移植函数时就需要连同全局变量
一起移植,影响程序的可靠性与通用性
总结:使用全局变量弊大于利,所以尽量减少甚至不使用全局变量。
九,变量的生存期与存储类型
变量的生存期:变量在程序中存在的时间;
存储类型: 往往是表明变量存储位置的;
变量定义完整的格式:
[存储类型] 类型 变量列表;
变量存储类型:
1. auto 自动存储类型
auto 只能修饰局部变量;这也是变量默认的存储类型
注意:auto是不能用于修饰形参,原因是形参存储类型是由编译系统
内部隐式管理的。
2. static 静态存储类型
static 修饰局部变量: 延长局部变量的生存期,但不改变局部变量的作用域
static 修饰全局变量: 限制全局变量仅在本文件内使用,
3. extern 外部存储类型
extern 修饰全局变量: 说明修饰的全局变量是其他文件或者在本文件下面定义的
对变量仅是声明,不是定义。
变量的作用域会被扩展, 但生命周期不变
4. register 寄存器存储类型
register 修饰局部变量:局部变量将不存在于内存中,而是存储在CPU的寄存器中,
register常常是用于修饰循环变量的。
十,内部函数与外部函数:
1.内部函数(静态函数):
格式: static 类型 函数名(形参列表)
{
//函数体
}
例如: static int fun ( int a , int b );
内部函数(静态函数)仅限定义的文件内使用,无法被其他文件的函数调用
2.外部函数:
格式: [extern] 类型 函数名(形参列表)
{
//函数体
}
extern 关键字修饰的函数称为外部函数,当我们省略extern,函数默认也是外部函数
外部函数可被其他文件的函数调用,但在其他文件中调用外部函数时,需要进行
外部函数声明,
例:有一个字符串,内有若干个字符,今输入一个字符, 要求程序将字符串中该字符删去。用外部函数实现。
File.c(文件1)
#include <stdio.h>
void main()
{ extern void enter_string(char str[]); extern void detele_string(char str[],char ch);extern void print_string(char str[]);/*以上3行声明在本函数中将要调用的在其它文件中定义的3个函数*/char c;char str[80]; enter_string(str);scanf("%c",&c);detele_string(str,c);print_string(str);
}
file2.c(文件2)
#include <stdio.h>
void enter_string(char str[80]) /* 定义外部函数 enter-string*/{ fgets(str,80,stdin); /*向字符数组输入字符串*/}
file3.c(文件3)
void delete_string(char str[],char ch)/*定义外部函数elete_string */
{ int i,j;for(i=j=0;str[i]!='\0';i++)if(str[i]!=ch)str[j++]=str[i];str[j]='\0';
}
file4.c(文件4)
#include <stdio.h>
void print_string(char str[])
{printf("%s\n",str);
}
小结:
1.C语言中函数是用来完成一定功能的;
2.C语言中有两种函数:库函数和用户自定义函数;
3.函数的定义和声明含义是不同;
4.函数处于调用它的函数之后时,要进行原型声明。函数原型声明有两种形式;
5.调用函数是要注意:实参与形参个数应相同、类型应一致(或兼容);数据传递是从实参到形参的单向值传递;
6.函数可以嵌套调用,也可以递归调用;
7.数组元素作实参其用法与普通变量相同,传递的是元素的值。而数组名作实参,向形参传递的是数组的首地址,而不是全部元素的值;
8.变量的作用域是指变量有效的范围。根据定义变量的位置不同,分为局部变量和全局变量;
9.变量的存储类别共有4个:auto、static、register、extern 前3个用于局部变量,可改变变量的生存期。 extern只能用于全局变量,可改变变量的作用域;
10.函数有内部和外部之分。本质上是外部的,但在其它文件调用时,要用extern对其声明。若不想让调用,应在定义时加上static,将其屏蔽起来;
11.变量的生存期是指变量存在的时间。全局变量的生存期是程序运行的整个期间,局部变量则不同。Static类为程序运行的整个期间, auto和register则与所在函数调用的时间段相同,函数调用结束就不存在了。