c语言教学资料:04_函数.docx

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

本章主要内容:
函数的定义、声明、调用
函数形参和实参
按值传递和按地址传递
递归算法
局部变暈和全局变暈
变量存储类型
内部函数和外部函数
1.1函数概述
函数是构成C 程序的基石,使用函数可以将程序分解成一个个的模块,每个模块实现一 个相对独立的功能,这些模块相互联系,共同组成了完成某项任务的程序。

在前儿章给出的 示例代码都是相对简单的程序,只有一个main()函数,实际上,随着问题规模的加大,要进 行的操作和处理的变量很多,程序的复杂度很高,如果程序不分模块,所有的操作都在main() 函数内完成,那么这只会使程序变成一团乱麻。

函数,是合理组织过程式程序的冇效手段。

引入函数的目的:方便结构化、模块化编程;解决代码的重复。

一个C 程序可由一个主函 数和若干个函数构成。

从软件工程的角度上说,降低程序复杂性的有效方法是合理的模块化。

也就是我们经常 说的“高内聚低耦合”。

在设计一个复杂的程序时,往往把整个程序划分为若干个功能较为 单一的模块,分别了以实现,再把所有的模块像搭积木一样搭起來,这种在程序设计中分而 治/的策略,被称为模块化程序设计方法。

在C 屮,模块就是一个个的函数,函数也是C 构造程序的重要的基本单位。

语句是构成程序的最基本单位,函数也是由语句构成的,从程序设计的角度上说,函数 (一系列语句)常被当成一个整体來看,这就降低了程序的复杂度。

函数从用户使用的角度可以分为:标准函数和用户口定义函数。

标准函数:即库函数。

由系统提供的设计好的函数,用户可以直接调用,例如: printf()—scanf()一fabs()—sqrt()一gets()一getchar()等等都是库函数。

用户自定义函数:用户自己根据需要定义的函数。

函数从形式上又可以分为:无参函数和有参函数。

实现一个函数主要分为3个步骤:函数的定义、函数的声明、函数的调用
1.2函数的定义
函数定义由函数头和函数体两部分组成, 返回类型函数名(形参列表)
1>.返回类型:用来说明函数返回值的类型,如果没有返回值,则应声明为void 。


省情 况下函数的返回值类型是hn
2>.函数名:必须是一个合法的标识符,与变量的命名规则相同,不能与其他的函数名或 变量重名。

3>.形参列表:0个或多个变量,用于向函数传送数值或从函数带冋数值(传入参数和传出 参数),每个参数都应采取“类型变量名”形式,参数列表中的参数称为形式参数,简称形参。

编译器并不会在函数定义时为这些参数分配内存空间,只有在函数调用时,向函数传递了实 参后,这些参数才称为程序实体。

如果没有形参,此函数叫无参函数,可以写一个VOido
4>.函数体:由一对{}括起来的语句,主要包括两部分。

变量说明部分通常用来定义在函 数体屮使用的变量、数组等;执行部分是函数功能的实现。

程序执行到函数体屮的retum 语 句返回,在函数
其基本形式为:
〃函数头 〃函数开始 〃函数体
〃函数结束
变暈说明部分和执行部分
体中可以有多个心um语句,但函数只能有一个出口,换句话说,只执行一条返回语句,返回语句的基本形式为:return表达式;表达式的类型应当与函数头屮指定的返冋类型一致,否则,编译器会根据函数头屮指定的返冋类型对表达式进行强制类型转换。

返回主要起如下作用:撤销函数调用时为参数和变量分配的栈内存空间:
向调用函数(上级)返回最多一个值(表达式的值);
将程序流程从当前函数返回上级函数。

当函数的返冋类型是void时,表明函数不向上级函数返回任何值,这时可以用一个空的"return^语句,将程序流程返回,撤销函数调用吋为参数和变量分配的栈内存空间,空的"return^®句位于函数末尾时,该语句可以省略,用函数体的后花括号实现函数的返回即可。

(参见代码Example 1)
1.3函数的声明
函数声明,也称函数原熨。

函数声明,用以通知编译器函数的存在,有多少形式参数,参数类型和是否有返回值,返回值类型等等,以获得函数的使用许可。

和变量一样,函数在调用之前要“先声明,后使用”。

调用库函数时,一般需要在程序的开头用include命令,包含这个库函数所在的头文件。

调用用户口定义的两数,而且该函数与主函数在同一个程序中,一般应该在主调函数中对被调函数作声明。

函数声明的基本格式如下:
返回类型函数名(形参列表);eg: int add(int a, int b);或int add(int, int);
当被调函数定义在主调函数之前吋,可以省略被调函数的定义。

1.4函数的调用
函数调用的一般格式:有参的:函数名(实参列表);无参的:函数名();
如果是无参函数,则没有实参列表。

如果有参数,则各实参之间用逗号隔开,并且实参的个数、类型应该与函数的形参个数、类型一致。

不•致吋会进行实参到形参的强制类型转换。

函数的调用过程:1.被调函数的所有形参分配内存,在计算实参的值,并一一对应的赋给相应的形参(无参函数不做该步骤)。

2.为函数说明部分中定义的变量分配内存,再依次执行函数的语句。

执行到reurni表达式;时,计算返回值。

3.释放在木函数屮定义的变量所占用的存储空间(对于static类型的变量,其空间不释放),返冋主调函数继续执行。

在前面的学习中大家已经熟悉了main()函数,一个C程序里包含一个主函数(即main 函数)和若干个其它函数,main()处于最顶层,其他函数作为其下层模块,main()函数调用其它函数,其它函数之间也可以互相调用。

下图为一个C程序的模块结构图,程序的执行过程为ABCDEFGHIJKMNo 任何一个C程序都是从main()函数开始执行,而且是只执行main()函数,从main()的前花括号开始,到main()的后花括号为止,在此过程中,如果碰到函数调用语句(如中的“函数1;"),便暂时中断main()的执行,将程序流程转到被调用函数(对应中的B),执行完被调用函数或遇到return语句则返回main()函数(对应中的H),继续执行,一个函数也可以调用其他函数(如屮函数1调用函数3),这时的调用过程与main。

函数调用其他函数过程类似,常称为函数嵌套调用。

函数1
1・5形参和实参
形参是函数定义时函数名后括号中的变量。

实参是函数调用时函数名括号中的常量、变量或表达式。

在调用函数时,将实参的数值求解出来赋给形参。

当形参和实参的类型不一致时,会进行强制类型转换。

主调函数与被调函数之间有数据传递关系。

主调函数向被调函数传递数据主要是通过函数的参数进行的(即实参到形参),而被调函数向主调函数传递数据一般是利用「eturn语句实现。

当需要返回多个值时,可以通过指针或引用作为传出参数。

C语言提供了两种参数传递数据的方式:按值传递和按地址传递。

C++还提供了引用传递。

实参变量的地址是递减存放,形参变量的地址是递增存放。

在内存中都是顺序存放,由此引申出可变参数的函数的概念。

参见《深入理解参数个数可变的函数的本质.txt》。

另外形参的形式还可以分为两种:一种是传入参数,一种是传出参数。

当一个函数需要返冋多个值的时候。

利用全局变量可以实现,但破坏程序的结构,要尽量少用。

方法一:可以通过一个结构体实现返回多个值。

Eg:
# include <stdio.h>
typedef struct tag VALUE
{
int value 1;
int value2;
[VALUE;
VALUE func(int a , int b)
{
VALUE value;
value, value 1 = a + b;
value.value2 = a - b;
return value;
int main()
VALUE value;
value = func(3, 4);
printf("3+4=%d\n", value.valuel);
printf(',3-4=%d\n n, value.value2);
return 0;
}
方法二:通过传出参数nJ以实现返冋多个值。

Eg:
# include <stdio.h>
int func(int a , int b, int *c) //可以用int &c, C++后面的内容,引用的效率最高
{
*c = a-b; 〃利用指针作为输出参数,保存a-b的值
retum (a + b); 〃利用return 返回一个a+b 的值
}
int main()
{
int iValuel = 0;
int iValue2 = func(3,4, &iValuel);
printf(n3+4=%d\n n, iValue2);
printf(,,3-4=%d\n n, iValuel);
return 0;
1
利用这种方法的时候,通常会用下面的形式:
#define OUT //OUT不具备任何意义,只表示一个标识
#define IN // IN不具备任何意义,只表示一个标识
int func(IN int a , IN int b, OUT int *c)
〃用IN标识的是表示输入参数,用OUT标识的表示输出参数
{
*c = a - b; 〃利用指针作为输出参数,保存的值
return (a + b); 〃利用return 返回一个a+b 的值
}
函数的返回类型也可以有3中形式:
C语言提供了2种函数返回类型:基本类型和指针类型。

C++提供了1种惭数返回类型:引用类型。

基本类型:int add(int a, int b)
指针类型:int* add(int a, int b)
引用类型:int& add(int a, int b)
其中函数返冋值类型是引用或指针的时候,要注意不要返冋一个临时的局部变量。

因为临时的局部变量在函数{}之后内存空间就会口动释放。

在函数调用的时候,系统会为函数的形参分配内存空间,并将实参的值赋给形参,而且会开辟一个存放返回值的公用内存空间。

char* GetStr(void)
// char pTemp[] = "hello H;
// char* pTemp = NULL;
// strcpy(pTemp, "hello");
return pTemp; 〃利用return返回一个临时的值
}
函数调用的过程,不论是值传递还是指针传递,编译器都要为每个函数制作临时副本, 函数体中对参数的修改都是对副本的修改,对传值调用来说,对副本的任何操作不会对传入的参数对象有任何的影响,这很好理解,但对传指针调用来说,情况稍显复杂,很多人对此存在误解。

#include <iostream>
using namespace std;
void GetMem(char* p, int num) //定义一个函数用来申请一块动态内存,传递指针p
{
p = new char[num]; 〃为形参p分配一块动态内存
cout « "p在内存中的地址:” vv &p « endl;
}
int main()
{
char* pChar = NULL; 〃外部char 型指针pChar
cout «M pChar 在内存中的地址:” « &pChar « endl; GetMem(pChar, 10); 〃复制拷贝
的过程
cout « "pChar 在内存屮的地址:"<v &pChar « endl;
〃前后两次的pChar的地址没有变化,且里面的值也没有改变if (pChar != NULL) 〃
判断pChar是否指向一块合法内存
{
cout « "内存申请成功"« endl;
deletef] pChar; 〃释放资源
}
else
{
cout vv "内存申请失败"« endl;
}
return 0;
}
此时,可以采用void GetMem(char** p, int num);或者void GetMem(char* &p, int num); 或者char* GetMem(char* p, int num);的方式解决。

函数执行完毕后,函数内部声明的局部变量会自动消亡,对应的内存被释放,由内存管理器收回,但返回值会被放置(复制)到指定位置(可能是CPU寄存器,也可能是某个内存单元),然后上级函从这个位置取得返回值。

这个位置,可以看成是函数返回值的“副本二这解释了为什么可以用Return局部变量來返回一个值。

#include <iostream>
using namespace std;
char* GetMem(int num) //函数定义,返冋char型指针
char* p = new char[num];
〃将指向一块动态内存的指针p返冋,复制到外部,p会被撤销
return p;
int main()
{
char* pChar = NULL;
pChar = GetMem(lO); 〃函数调用,pChar指向函数GetMem中申请的动态内存
if (pChar != NULL) //如果申请成功
{
cout vv "内存申请成功"« endl;
deletef] pChar; 〃释放内存资源
}
else
{
cout « ”内存申请失败"« endl;
}
return 0;
}
1.6值传递
函数调用时,主调函数把实参的值传给被调用甫数的形参,形参值的变化不会影响实参的值,这是一种单向的数据传递方式。

因为在内存屮,形参和实参是不同的存储单元。

当实参是变量、常量、表达式或数组元索,形参是变量时,函数传递数据方式釆用的是按值传递。

(参见代码Example2)
1.7地址传递
函数调用时,主调函数把实参的地址传给被调函数的形参。

由于传递的是地址,使形参和实参共享同一存储单元中的数据,这样通过形参可以直接引用或处理该地址中的数据,形参改变会影响到实参。

当数组名、指针等作函数参数吋,函数传递数据的方式采用的是按地址传递。

(参见代码Example3)
1.8C++的引用传递(后续内容)
引用就是变量的别名,常用的形式:int &m二n;这样对m的操作实质上就是对n的操作。

例如湖南省的简称是湘,湘就可以认为是湖南省的引用(别名),新闻上说“湘发生。

”实质上就是“湖南省发生。

”。

所以说,m既不是n的拷贝,也不是指向n的指针,可以简单的认为m就是n自己。

就相当一个人拥有两个名字。

在C++中允许幣数通过引用的方式实现参数传递。

此时,在调用函数时,会为形参分配内存空间,但是表现出来的是实参的地址。

因为此时,形参就是实参自身,在函数中对形参的任何操作,本质上就是对实参的操作,即形参的改变会影响实参。

(参见代码Example4)
1.9递归
一个函数调用自己的过程称为递归调用。

构造递归函数的关键在于寻找递归算法和终结条件。

例如:n ! = n*(n-l)*(n-2)*(n-3)...*2*l = n*(n-l)!递归算法1!二1 终结条件。

计算n!必须算出(n-1)!,计算(n-1)!必须计算出(n-2)!…由此推到1!二1。

可以归纳阶乘的递归算法如下:f(n) = n * f(n-l); 结
朿:f(l)=l。

(参见代码Example5)
1.10局部变量和全局变量
C语言只允许在3个地方定义变量:函数内部、所有函数外部、复合语句中。

其中在函数内部和复合语句中声明的变量是局部变量。

在所有函数外部声明的变量叫全局变量。

局部变量:对于在函数的形参、内部声明的变量及结构变量等,编译器将在函数执行时为形参自动分配存储空间,在执行到变量和结构变量等的声明语句时为其自动分配存储空间,因此称其为自动变量(automatic variable),也称为局部变量,在函数执行完毕返回时,这些变量将被撤销,对应的内存空问将被释放。

自动变量的生存期只局限于它所在的代码块。

所谓代码块,是包含在花括号对中的一段代码,函数只是代码块的一种。

全局变量:在所有断数之外定义的变量叫做全局变量。

全局变量的有效范圉从定义全局变量的位置开始到本源文件结束。

若在全局变量定义Z前的函数想要引用该全局变量,则应在该函数中使用关键字extern作外部变量声明。

如果在同一个源文件中,外部变量和局部变量同名,则在局部变量的作用范围内,外部变量不起作用。

要引用外部变量可以通过域作用符::。

假如a.c中有个变量int x = 2;要想在b.c中用到a.c中的x以在b.c中直接声明extern int x;就可以使用,如果想要在C++屮调用c屮的,如b.cpp屮要用到a.c屮的变量x就要采用如下形式:extern "C” int x;
1.11动态存储和静态存储
静态存储区中存放的变量在程序开始执行时分配存储单元,程序执行完毕时释放。

在程序执行过程屮它们占据固定的存储单元,如全局变量就是存放在静态存储区。

动态存储区屮存放的变量在函数调用开始时白动分配动态存储空间,函数结束时自动释放这些空I'可。

如局部变量就是存放在动态存储区。

C语言屮存储类型主要有以下4中:
auto (自动的):局部变量默认的存储类型。

register (寄存器的):变量存放在寄存器屮而不是内存,现在已经不常用了static (静态的):static变量的存储单元被分配在内存的静态存储区中。

局部和全局变量都可以说明为static类型。

静态局部变量的生存期与全局变量相同,作用域和局部变量相同。

extern (外部的):外部变量(即全局变量)是在函数的外部定义的,作用域为从变量的定义处开始到本程序的结束。

一个C程序可以由一个或多个源程序文件组成。

如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量,此时不能在两个文件中各自定义相同名称的外部变量,否则就会出错。

正确的做法是:在其中一个文件中定义一个外部变量,而在另一个文件中用extern进行声明。

(参见代码Example6)
1.12内部函数和外部函数
内部函数:如果一个函数只能被本文件的其他函数调用,不能被其他源文件的函数调用,则称为内部函数,有时也叫静态函数。

在定义内部函数时,在数据类型前需要加上static关键字,其定义格式为:static返回类型函数名(形成列表);由于内部函数只局限于本源文件中使用,因此不同的源文件中即使有同名的内部函数,也互不干扰。

外部函数:外部函数是指nJ•以被其他源文件调用的函数,在定义函数时如果在最前面加上关键字extern,则该函数是外部函数,其定义格式为:extern返回类型函数名(形参列表); 函数默认情况下是extern的。

1.13函数指针和指针函数
指针函数:函数的返回值是指针。

一般形式如下: 数据类型*函数名(参数列表)
}
函数指针:指向函数的指针。

在定义一个函数Z后,编译系统会为每一个函数确定一个入口地址,当调用该函数的时候,系统就从这个入口地址开始执行。

存放函数的入口地址的指针变量就是一个指向函数的指针。

一般形式如下:
数据类型(*函数指针变量名)(参数1,参数2...);
和普通变量的指针一样,函数的指针也必须赋初值,才能只想具体的函数。

rh于函数名代表该函数的入口地址,因此,一个简单的方法就是直接用函数名为函数指针赋值。

B|J:
函数指针名=函数名;
Eg: int add(int a, int b); int (*p)(int, int); p = fun; p(3, 5);
另外一种方法就是用typedef定义一个通用的函数名类型(参见代码Example7)
1.14 函数的一些注意点
1.内存泄露问题:
使用new或malloc()动态申请的内存,如杲不再使用,应该把它释放掉,为程序节省内存空间,方便后面的使用。

在C/C++中,内存管理器不会自动回收不再使用的内存。

如果忘记释放不再使用的内存,在程序的运行过程中,这些内存就不能再被使用用,就造成了所谓的“内存泄露避免内存泄露:free(p);或delete p;或delete [] p;
2.野指针问题:
指针被free或delete/delete[J/H, 一定要置为null,没有置为null的指针常称为“野指针”, 释放掉的堆内存会被内存管理器重新分配,野指针指向的内存己经被赋了新的意义。

如果使用野指针释放或再次访问这块内存,会给程序带来灾难性的后果。

避免野指针:if(p!二NULL) p = NULL;
3.当数组作为函数的形参时用sizeof求数组占用内存空间问题:
当数组作为函数的形参时,编译器会将数组弱化为指针。

此时M sizeof求数组占用的内存空间时,始终是4 (指针类型占用的内存空间)。

void func(char str[20])
printf(“%d\n'; sizeof(str)); //值为4,不是20。

相关文档
最新文档