第 5 章 C语言的函数 本章主要内容: (1)函数。 (2)局部变量和全局变量。 (3)动态存储和静态存储。 (4)跑马灯实验。 函数 .. 5.1 函 数 函数是C语言支持程序模块化设计的基本单元。程序员可以直接调用系统 提供的库函数,从而简化程序的开发;也可以根据需要自己定义函数,从而实现一 个独立的功能模块。函数的使用使程序结构层次清晰,便于阅读和调试。函数也 是代码重用的一种重要手段。 【例5-1】 从键盘任意输入一个正整数n,计算1到n之间所有奇数的和并 输出。程 序代码: /************************************************************************** 源程序名:D:\C_Example\5_Function\oddSum.c 功能:计算1 到正整数n 之间所有奇数的和并输出 输入数据:正整数n 的值 输出数据:1 到正整数n 之间所有奇数的和 **************************************************************************/ #include int oddSum(int m) //函数定义 { int i, s; s=0; //存放奇数和的变量初始化 for(i=1;i<=m;i++) //循环n 次,判断1 到n 之间的每个整数是否是奇数 if((i%2)!=0) //判断是否是奇数 s=s+i; //奇数累加 return s; }i nt main() 2 46 鸿蒙OSC语言程序设计(微课版) { long int sum; //定义存放奇数和的变量sum int n; printf("请输入正整数n="); scanf("%d",&n); //输入正整数n sum = oddSum(n); //调用求奇数和的函数 printf("奇数和sum=%d",sum); //输出奇数和 return 1; } 编辑、编译和运行例5-1的程序,运行结果如图5-1所示。 1.函数的定义 函数定义的一般形式如图5-2所示。 图5-1 oddSum.exe程序运行结果图5-2 函数定义的一般形式 类型说明符是函数返回值的类型。当函数有返回值时,要通过return语句返回,因此 return语句中的表达式类型或者返回值的类型应该与类型说明符保持一致;当函数无返回 值时,类型说明符为void。函数名是用户自定义的函数名称。形式参数表是用逗号分隔的 参数列表,参数是实现函数与外部传递数据的通道。若不需要传递数据则参数为空,称为无 参函数;否则为带参函数。 2.函数的调用 函数调用的一般形式为 函数名(实际参数表); //无参函数省略实际参数表 例5-1中的intoddSum(intm)语句是oddSum 函数的定义,其中参数m 称为形式参 数,简称形参,形参在内存中没有分配存储空间;sum = oddSum(n)语句是对oddSum 函数 的调用,其中参数n称为实际参数,简称实参,是在main函数中定义的变量。 参数是调用函数与被调用函数之间交换数据的通道。当函数被调用时,系统才为形参 在内存中分配存储空间,并将实参n的值传递给形参m,完成数据传递。函数执行结束时, 为形参分配的存储空间将被释放。 注意:函数调用时提供的实参个数、类型、顺序应与定义的形参一致。 函数的参数可以是一般变量、指针或引用形式,分别传递变量值、地址值。当形参是变 量值时,函数调用时实参向形参单向传递值,它们各自占用不同的存储空间,因而形参值的 改变不会影响实参值。当形参是指针时,传递的是地址值,具体参见6.1节。 第5章 C语言的函数2 47 3.函数原型 函数只有通过调用才能被执行,与函数的定义位置无关。在例5-1的程序中,是将 被调用函数oddSum(intm)定义在主调函数main之前,现在将oddSum(intm)函数定 义在main函数之后,但在编译时函数调用语句sum = oddSum(n);将会出现编译错 误:“' oddSum ' :undeclaredidentifier”,这是因为函数必须在使用之前进行声明或者 定义。 函数原型是对函数的声明,其作用是告诉编译器有关函数的接口信息,包括:函数的名 称,函数的参数个数、类型和顺序,函数的返回值类型。编译器需要根据这些信息检查函数 调用是否正确。 函数原型声明的一般形式为 返回值类型函数名(形参类型 形参名[,…]); 如果函数定义在被调用函数之前,则可省略函数原型的声明。因为,函数定义中就给出 了函数的原型信息。在例5-1的程序中添加函数声明语句,并将oddSum 函数定义在main 函数之后,代码如下: /************************************************************************************ 源程序名:D:\C_Example\5_Function\oddSum01.c 功能:计算1 到正整数n 之间所有奇数的和并输出 输入数据:正整数n 的值 输出数据:1 到正整数n 之间所有奇数的和 ************************************************************************************/ #include int oddSum(int m); //函数声明 int main() { long int sum; int n; printf("请输入正整数n="); scanf("%d",&n); sum = oddSum(n); printf("奇数和sum=%d",sum); return 1; }i nt oddSum(int m) { int i, s; s=0; for(i=1;i<=m;i++) if((i%2)!=0) s=s+i; return s; } 2 48 鸿蒙OSC语言程序设计(微课版) C语言的 局部变量和 全局变量 .. 5.2 C语言的局部变量和全局变量 在前面的程序中,使用变量时要考虑的基本特性包括变量名、变量的数据类型和变量的 值。除此之外,C语言的变量还分为局部变量和全局变量,这两种变量在C语言程序中的作 用域是不同的。 变量的作用域是指变量在程序中的有效作用范围,即被声明的变量在程序中的有效代 码区域,也称变量在该区域是可见的或可用的。下面首先通过例5-2展示局部变量和全局 变量及其作用域问题。 【例5-2】 从键盘任意输入两个整数,将其中较大的数输出。 程序代码: /************************************************************************************ 源程序名:D:\C_Example\5_Function\maxData.c 功能:展示变量作用域,局部变量和全局变量 输入数据:任意输入两个整数 输出数据:两个整数中较大的数以及变量的地址 ************************************************************************************/ #include void maxdata(int a,int b); //函数参数中定义局部变量a 和b int max; //定义全局变量max int main() { int a,b; //定义局部变量a 和b max = 0; printf("请输入两个整数a="); scanf("%d",&a); printf("请输入两个整数b="); scanf("%d",&b); printf("\n\n"); printf("main 函数中复合语句前面变量a 的地址:%x\n",&a); printf("main 函数中复合语句前面变量a 的值a=%d\n",a); printf("main 函数中复合语句前面变量b 的地址:%x\n",&b); printf("main 函数中复合语句前面变量b 的值b=%d\n",b); printf("main 函数中复合语句前面变量max 的地址:%x\n",&max); printf("main 函数中复合语句前面变量max 的值max=%d\n",max); printf("\n\n"); { //复合语句开始 int a,b,max; a=3; b=5;max=3; printf("复合语句中变量a 的地址:%x\n",&a); printf("复合语句中变量a 的值a=%d\n",a); printf("复合语句中变量b 的地址:%x\n",&b); printf("复合语句中变量b 的值b=%d\n",b); printf("复合语句中变量max 的地址:%x\n",&max); printf("复合语句中变量max 的值max=%d\n",max); printf("\n\n"); 第5章 C语言的函数2 49 } //复合语句结束 printf("main 函数中复合语句后面变量a 的地址:%x\n",&a); printf("main 函数中复合语句后面变量a 的值x=%d\n",a); printf("main 函数中复合语句后面变量b 的地址:%x\n",&b); printf("main 函数中复合语句后面变量b 的值b=%d\n",b); printf("\n\n"); maxdata(a,b); //调用maxdata()为全局变量max 赋值 printf("maxdata 函数执行后main 函数中变量max 的地址:%x\n",&max); printf("maxdata 函数执行后main 函数中变量max 的值max=%d\n",max); printf("\n\n"); printf("%d,%d 中的最大的数是:%d",a,b,max); //引用全局变量 return 1; }v oid maxdata(int a,int b) //为全局变量赋值,函数不需要返回值 { if(a>=b) max=a; else max=b; //引用全局变量 printf("maxdata 函数中变量a 的地址:%x\n",&a); printf("maxdata 函数中变量a 的值a=%d\n",a); printf("maxdata 函数中变量b 的地址:%x\n",&b); printf("maxdata 函数中变量b 的值b=%d\n",b); printf("maxdata 函数中变量max 的地址:%x\n",&max); printf("maxdata 函数中变量max 的值max=%d\n",max); } 在例5-2的程序中分别定义了如下变量: (1)在所有函数的外部、main函数的前面用intmax;定义了整型变量max。 (2)在main函数内部第一行用inta,b;定义了整型变量a和b。 (3)在main函数内部用一对花括号括起来的复合语句中用inta,b,max;定义了整型 变量a、b和max。 (4)在voidmaxdata(inta,intb)函数用圆括号括起来的形参表中用inta和intb定义 了整型变量a和b。 可以看出,在例5-2的程序中存在2个重名的max变量、3个重名的a变量和3个重名 的b变量。那么,这些变量中同名的变量是同一个变量吗? 它们之间存在什么关系? 它们 之间又有什么差别呢? 要想搞清楚这些问题,首先编辑、编译和运行例5-2的程序,观察运行结果,如图5-3所 示。需要强调的是,由于计算机内存大小不同,正在运行的操作系统以及正在执行的程序也 不同,不同的计算机运行例5-2的程序,得到的各个变量的地址会不一样。也就是不同的计 算机执行例5-2的程序的结果会跟图5-3类似,但可能有区别。 图5-3显示了程序中定义的这些变量的地址和存储的值,图5-4是根据这些变量的地 址以及这些变量所存储的值绘制的变量占用内存的示意图。从图5-3和图5-4可以得出变 量、变量地址、变量存储的值的关系,如表5-1所示。 250鸿蒙OSC语言程序设计(微课版) 图5c程序执行结果图5c程序变量内存地址 - 3 maxData.- 4 maxData. 第5章C语言的函数251 表5程序mc中的变量、变量地址、变量存储的值的关系 - 1 axdata. 序号变量所属函数变量类型和名称变量地址变量存储的值变量类别 1 无intmax 0x407990 0→35 全局变量 2 iinta 0x22fe4c 23 局部变量 3 man函数 intb 0x22fe48 35 局部变量 4 复合语句intmax 0x22fe3c 3 局部变量 5 main函数复合语句inta 0x22fe44 3 局部变量 6 复合语句intb 0x22fe40 5 局部变量 7 inta 0x22fe10 23 局部变量 8 maxdata函数intb 0x22fe18 35 局部变量 9 max 0x407990 35 全局变量 从表5-1中可以清晰地看出: (1)前两个max变量(序号1和4)不但地址不同,存储的数据也不同,显然是两个不同 的变量。其中,第一个max变量不属于任何函数,被称为全局变量;第二个max变量是在 main函数内部的复合语句中定义的变量,被称为局部变量。 (2)第一个和第三个max变量(序号1和9)地址相同(都是0x40790),存储的数据也相同 (都是35),实际上它们是同一个变量,也就是程序唯一的全局变量,通过这个全局变量实现了变 量max在函数main和maxdata之间的共享应用,起到在不同函数之间传递数值35的作用。 (3)3个a变量(序号为2、5、7)虽然同名,但地址不相同,就像3个不同的人尽管取了 相同的名字,仍然是3个不同的人一样,它们是3个不同的变量,属于不同的函数或者复合 语句,被称为局部变量。 (4)3个b变量(序号为3、6、8)和上面讲的a变量类似,它们的地址也不相同,属于不 同的函数或者复合语句,也是局部变量。 全局变量(globalvariable)是指定义在函数外,不属于任何函数的变量,其作用域是从 定义位置开始到程序所在文件结束。全局变量的定义格式与局部变量完全相同,只是定义 位置不同,它可以定义在程序的开始、中间等任何位置。全局变量对定义位置之后的所有函 数都有效。因此,全局变量成为多个函数共享的变量,可以用于函数之间的数据传递。例 如,在例5-2中,main函数前面定义的全局变量max既在main函数中得到使用,也在 maxaa(nitb) 起到了传递数值35的作用, xaa(nitb) dtita,n函数中得到使用, 将madtita,n 函数中的变量a和b中较大的值通过max传递给main函数。 局部变量(localvariable)通常是指在函数的形参表、函数内部、复合语句内部定义的变 量,例如在maxdata函数中定义的3个局部变量。局部变量的作用域局限于函数内部或者 复合语句内部,从定义位置开始至本函数或者本复合语句结束。使用局部变量可以避免不 同函数之间同名变量的互相干扰,也就是说不同函数内部可以出现同名变量,它们有各自的 存储空间和作用范围,不会产生冲突。图5-4直观地展示了局部变量的这种情况。 在复合语句内定义的局部变量,其作用域只限于复合语句内;在函数原型声明语句内的 局部变量,其作用域只限于函数原型。具体可参见例5-2中此种变量的作用情况。 2 52 鸿蒙OSC语言程序设计(微课版) 注意:函数原型声明语句voidmaxdata(inta,intb);等价于voidmaxdata(int,int);。 函数原型声明语句中的形参作用域只在声明语句内。因此,声明语句中的函数形参名 是可以省略的。即使函数原型中带有形参名,编译器在编译时也将忽略它。 注意:使用全局变量会破坏函数的独立性,容易使函数之间相互干扰,因此要谨慎使用。 由于作用域的不同,程序中可能出现全局变量与局部变量同名,例如,例5-2在main函数 外部定义了全局变量max,在main函数内部复合语句中定义了局部变量max,在这种情况下, 复合语句内部的局部变量max会屏蔽全局变量max,也就是只有局部变量max有效。 C语言变量 的静态存储 和动态存储 .. 5.3 C语言变量的静态存储和动态存储 C语言变量不但分为全局变量和局部变量,在变量存储类别上还分为静态存储和动态 存储。变量存储类别决定了变量的生命周期,也就是变量在内存中的生存时间,对应变量从 开始获得存储空间到最后释放存储空间的整个过程。为了更好地理解变量的静态存储和动 态存储,首先看一个变量静态存储和动态存储的C语言程序。 【例5-3】 变量静态存储和动态存储示例。 程序代码: /************************************************************************************ 源程序名:D:\C_Example\5_Function\variableStorage.c 功能:展示变量静态存储和动态存储 输入数据:无 输出数据:见图5-5 ************************************************************************************/ #include int Fun(); int main() { int f,i; //自动变量 printf("f 和i 未初始化时的值:f = %d, i= %d\n\n",f,i); for(i = 1;i<= 5;i++) { f = Fun(); printf(" 自动变量:f = %d\n",f); } return 0; }i nt Fun() { int m = 0; //自动变量 static int n=0; //静态局部变量 m++; printf(" 自动变量:m = %d ",m); n++; printf(" 静态局部变量:n = %d ",n); return n; } 编辑、编译和运行例5-3的程序,运行结果如图5-5所示。从程序运行结果可以看出,在函 第5章C语言的函数253 图5-5variableStorage.exe程序运行结果 数Fun中定义的变量m和n差异比较大,比较一下这两个变量在函数Fun中的定义、初始化 和运算:两个变量的初始化分别是intm=0;和staticintn=0;, 两个变量都进行自增运算。 两者的差别在于变量n的定义前面加了static,就是因为这一差别,导致两个变量运算的值差 异巨大:变量m的值每次调用函数Fun时一直是1;变量n的值每次调用函数fun时都会增加 1,一直到5。原因就在于m变量是动态存储的,变量n因为定义时前面加了static,所以就成 为静态存储的变量。其实,例5-3程序的main函数中定义的变量f和i也都是动态存储的。 1.动态存储 采用动态存储方式时,由系统自动完成内存的分配和释放。动态存储由系统的堆栈实 现,系统自动根据函数的执行情况为变量分配和回收堆栈空间。动态存储分配的数据区就 是动态存储区,存放程序中函数内部的局部变量,例如例5-3的main函数中定义的变量f、i 以及例5-xaa(nitb)函数形参中定义的变量a、b等。 2的madtita,n 局部变量和形参都默认为auto存储类别。auto类型的变量也称自动变量,都采用动态 存储方式。一般自动变量在定义时都省略auto。 inta,utointa, 因此,b;等价于ab;, 这两种定义的效果是一样的。 自动变量只有在函数被调用时才由系统分配相应的存储单元;到函数调用结束时,存储 单元被自动释放。这一切由系统自动完成。在例5-3的Fun函数中,m被定义为自动变量。 在运行程序的过程中,Fun函数被循环调用5次。每次被调用时,m变量均被分配存储空 间,并被初始化为0;然后通过m++ 运算自增其值,变为1,并输出m=1;最后在函数结束 时m变量的存储空间被释放,进入下一轮循环。这样就导致每次循环执行时都是输出m= 1,一共输出5次。因此,局部变量、形参在函数被调用之前并不占有存储单元。 注意:自动变量如果未被赋初值,则其值是随机的,因此,程序中一定要注意自动变量 的初始化。例如,例5-3的main函数中定义的自动变量f和i在未被初始化时输出的随机 值分别是1和0。 2. 静态存储 与动态存储不同的是,静态存储方式是在变量定义时就由系统分配了存储空间并保持 不变,直至整个程序结束。全局变量和static类型的局部变量都是静态存储方式,被分配在 静态存储区。 全局变量在整个程序运行期间都占用存储空间,其生命周期贯穿于整个程序。全局变 量在定义时如果未被初始化,系统将自动为其赋初值0,例如例5-2的main函数前定义的 全局变量max。 2 54 鸿蒙OSC语言程序设计(微课版) 除了全局变量,还有一种特殊的static类型的局部变量,称为静态局部变量。当局部变 量在定义时加上static就成为静态存储方式的局部变量。 例如,在例5-3的Fun函数中,staticintn;将n定义为静态局部变量。在运行程序过 程中,Fun函数被循环调用5次。第一次被调用时n变量被分配存储空间并被初始化为0, 然后通过n++运算自增其值变为1,输出n=1,在Fun函数结束时n变量的空间依然保 留;随后进入第二次调用,此时n变量的空间不但存在,而且n=1,在此基础上通过n++运 算自增其值变为2,Fun函数执行结束;随后进入第三次调用,使n=3;随后进入第四次调 用,使n=4;最后进入第五次调用,使n=5。 因此,静态局部变量在函数第一次被调用时获得存储单元,其后一直保持该存储单元, 直到程序运行结束,即使所在的函数被调用结束,存储空间也不会被回收,只有整个程序执 行结束时,静态变量占据的存储空间才被释放和回收。静态局部变量只在第一次使用时完 成初始化。如果变量未被赋值,系统自动将其初始化为0。其后变量的值一直保持到下一 次函数被调用时。 如果要指定变量的存储类别,其一般定义形式为 存储类别类型标识符变量名1[,…]; 例如: auto int x,y; //auto 可以省略 static float f1,f2; //static 不能省略 虽然静态局部变量与全局变量的生命周期都贯穿了整个程序的运行过程,但静态局部 变量只能在其定义的函数内使用,不能用于其他函数,也就是其作用域始终不变。 C语言程序 在内存中的 存储结构 .. 5.4 C语言程序在内存中的存储结构 C语言程序在内存中的存储结构主要由代码区、常量区、静态区、堆区和栈区组成。 (1)代码区。顾名思义,用于存放编写的C语言程序的代码(编译后的二进制代码)。 (2)常量区。常量(包括数值常量、字符常量和字符串常量)存储在该区域,不允许修改。 (3)静态区。存储全局变量和静态变量的区域,初始化的全局变量和静态变量在一块 区域,未初始化的全局变量和静态变量在相邻的另一块区域。在程序结束后,该区域内的变 量由系统释放。 (4)堆区。在程序运行过程中,通过new、malloc、realloc函数申请分配的内存块称为 堆区。编译器不会负责该区域的释放工作,需要由程序员编写执行释放内存的程序代码予 以释放。堆区的分配方式类似于数据结构中的链表。内存泄漏通常说的就是堆区。堆区往 地址增大方向增长。 (5)栈区。用于存放函数的参数值、函数内部的局部变量等,由编译器自动分配和释 放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。栈的内存分配运算 内置于CPU 的指令集,效率很高,但是分配的内存量有限,例如iOS中栈区的大小是2MB。 栈区往地址减小方向增长。