C语言学习笔记4 —— 函数

一、什么叫函数 ?有啥用?

函数就是功能模块,要来完成一定的功能,是实现模块化程序设计的重要方式。每一个函数都有一定的特定功能需要实现的,所以在起名字的时候要注意尽量做到应当适当反映其代表的功能。

函数的好处:

  • 1、方便阅读,降低复杂性: 使用函数的最首要原因是为了降低程序的复杂性,可以用函数来把信息装起来,阅读代码的时候只需看名字就可以知道干了什么,从而使你不必再考虑这些信息。
  • 2、提高代码的复用: 若在多个地方存在相似的代码,这时候可以利用函数把相似的代码写在一个函数里,然后在这些地方直接调用该函数即可,那就不用多个地方都要重复去写。
  • 3、限制改动带来的影响: 由于在独立区域进行改动,由此带来的影响也只限于一个或最多几个区域中。
    例如: 若要改动某个值,但该值涉及地方比较多,这是使用函数就只需在一个地方改动代码就可以了,这样改动的代码也更可靠了。
  • 4、隐含顺序: 如果程序通常先从用户那里读取数据,然后再从一个文件中读取辅助数据,在设计系统时编写一个函数,隐含哪一个首先执行的信息。
  • 5、改进性能: 把代码段放入函数也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码的工作变得容易些。
  • 6、进行集中控制: 专门化的函数去读取和改变内部数据内容,也是一种集中的控制形式。
  • 7、隐含数据结构: 可以把数据结构的实现细节隐含起来。
  • 8、隐含指针操作: 指针操作可读性很差,而且很容易引发错误。通过把它们独立在函数中,可以把注意力集中到操作意图而不是集中到的指针操作本身。
  • 9、隐含全局变量: 参数传递。

拓展知识:
(1)一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。对于较大的程序,通常都是由若干个源程序文件组成一个C程序,这样便于分别编写和编译,提高开发、调试的效率,并且一个源程序文件可以为多个C程序共用。
(2)程序编译时是以一个源程序文件为单位进行编译,而不是以函数为单位。
(3)C程序的执行是从main函数开始,从main函数结束,如果调用了其他函数,调用结束后返回main函数。
(4)所以函数都是平行关系,互相独立的,没有从属的关系(即:函数不能嵌套定义),但可以函数间互相调用,但不能调用main函数,main函数只能被系统调用。
(5)C语言中,要求函数必须 “先定义,后使用。”

(6)从用户使用角度看,函数分两种:

  • (6-1)库函数:由系统提供的,用户不必定义,可直接使用。
  • (6-2)自定义函数:自己定义要来解决用户专门需求。

(7)从函数的形式看,函数分两种:

  • (7-1)无参函数:调用的时候不需要传参数给函数。
  • (7-2)有参函数:调用的时候需要传参数给函数。

二、函数的定义

1、定义函数需要指定的内容

(1)函数名,方便以后的按名调用;
(2)函数的类型,也等同于指定函数的返回值类型;
(3)函数的参数的名字、类型和个数,方便有参函数的使用,无参函数不需要;
(4)函数体,即:函数的实现功能。

2、定义格式

(1)无参函数

特点: 函数名后面的括号是空的或者是void(表示 “空” ),即:没有任何参数。
用处: 用于不需要传递参数到调用的函数中的情况。
格式:

类型名   函数名()       或       类型名   函数名(void{                                {
	函数体;                      	函数体;
}                                {
// 函数体:可以包含 声明部分、执行语句部分 和 函数返回部分。1:定义一个常用的输出语句
void print_message()
{
	printf("Hello world");
}2:定义一个计算从 1 加到 100 的结果的函数
int sum()
{
    int i, sum=0;
    for(i=1; i<=100; i++){
        sum+=i;
    }
    return sum;
}

(2)有参函数

特点: 函数名后面的括号里有参数。
用处: 用于需要接收用户传递的数据的情况。
格式:

类型名   函数名(形参类型  形参名1,··· ,形参类型  形参名n)        
{                                
	函数体;                      	
}                  
// 函数体:可以包含 声明部分、执行语句部分 和 函数返回部分。
// 声明部分:对函数用到的变量进行定义,还有对要调用的函数进行声明。
//函数返回部分:把函数中需要带到主函数上的结果返回去主函数,但只能带回一个结果。
//多个形式参数之间用逗号 “,”  隔开。            
例如:
int max(int x,int y)
{
	int z;    // 声明部分
	z = x>y ? x : y;   //执行语句部分
	return(z);    //函数返回部分
}
(1)有参函数定义时给出的参数称为形式参数,简称形参;
(2)函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。
(3)函数调用时,将实参的值传递给形参,相当于一次赋值操作。
(4)因为函数所操作的是形式参数,而不是实际参数,所以通常情况下形参的值改变是
	不会影响实参的值,但如果是对地址进行操作,那就会改变实参的值,这也是实现多
	返回的原理。
(5)原则上讲,实参的类型和数目要与形参保持一致。如果能够进行自动类型转换,
	或者进行了强制类型转换,那么实参类型也可以不同于形参类型,
	例如:将 int 类型的实参传递给 float 类型的形参就会发生自动类型转换。

(3)空函数

特点: 函数体为空。
效果: 调用空函数时,什么工作也不做,没有任何实际作用。
作用: 通常都是在程序设计阶段会用到,在后来才补上具体详细代码,或是对于一些次要功能、以后扩充功能留位置,等后期再陆续补充。这样做可以使程序结构清晰,可读性好,方便日后维护。空函数对程序影响不大,不占用内存。
格式:

类型名   函数名()       
{}                                

例如:
void dummy()
{}

注意:函数不能嵌套定义

C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。

错误示例1:   子函数嵌套子函数
#include <stdio.h>
void func1(){
    printf("hello world 1");

    void func2(){
        printf("hello world 2");
    }
}

int main(){
    func1();
    return 0;
}

错误示例2:   主函数嵌套子函数
int main(){
	void func1(){
        printf("hello world");
    }
    
    func1();
    return 0;
}

三、函数的调用

1、调用形式

调用语句:函数名(实参表列)
【调用无参函数时,“实参表列”则为空,但括号不能省略。若有多个实参时,要按照函数定义的顺序传入,且每个参数之间用逗号隔开】

若按照函数调用的出现形式和位置来分,可以分成以下3种:
(1)调用语句形式调用
把函数调用语句单独作为一个语句,这时不要求函数带回值,只要求函数完成一定的操作,常用在 无参函数 和 无返回值函数 中。
如:调用无参函数:print_star();
(2)函数表达式
把函数调用语句放在表达式中,这时通常需要函数完成一定的操作,然后返回一个结果,常用在有参函数中。
如:调用实参函数给变量c赋值: c=max(a,b);
(3)函数参数
把一个函数放进另一个函数中当实参或者参数进行调用。【典型用法:递归】
如:m=max(a,max(b,c)); 或者 `printf(“最大值为:%d”,max(a,b));

2、参数的传递

2-1 名词概念

(1)形式参数(或称:虚拟参数):函数定义处的函数名后面括号中的变量名,简称“形参”。
(2)实际参数:在主函数调用子函数时,写在调用语句中的参数,简称“实参”。实参可以是常量、变量 或 表达式。

2-2 两者关系

  1. 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

  2. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,都必须是确定的值,以便把这些值传给形参,所以应该提前用赋值、输入等办法使实参获得确定值。

  3. 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

  4. 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。

  5. 形参和实参虽然可以同名,但它们之间是相互独立的,互不影响,因为实参在函数外部有效,而形参在函数内部有效。

  6. 实参的值传递形参时的位置是根据调用时这个值的位置与定义函数时的位置所决定的。

数据的传递的示例:

#include<stdio.h>
int main()
{
	int exchange1(int x,int y);  //函数声明
	int exchange2(int* x, int* y);  //函数声明
	int a, y, max1, max2;
	int * pa, * py;
	printf("主函数中max1、max2的地址:max1=%p   max2=%p\n", &max1,&max2);  
	printf("请输入a,y 两个变量的值:");
	scanf_s("%d %d",&a,&y);
	printf("主函数中,在进行exchange1交换前a,y的数值:a=%d  y=%d  \n", a, y);
	max1=exchange1(a,y);  //函数调用
	printf("主函数中采用exchange1(int x,int y)函数的结果:a=%d  y=%d   max1=%d\n",a,y,max1);
	printf("exchange1后,主函数中max1的地址:max1=%p\n\n", &max1);

	printf("请再次输入a,y 两个变量的值:");
	scanf_s("%d %d", &a, &y);
	printf("主函数中,在进行exchange2交换前a,y的数值:a=%d  y=%d  \n", a, y);
	pa = &a;
	py = &y;
	max2 = exchange2(pa, py);  //函数调用
	printf("主函数中采用exchange2(int* x,int* y)函数的结果:a=%d  y=%d   max2=%d\n", a, y, max2);
	printf("exchange2后,主函数中max2的地址:max2=%p\n\n", &max2);
	return 0;
}

//exchange1函数定义
int exchange1(int x, int y) {
	int max1,t;
	printf("exchange1中,执行前max1的地址:max1=%p\n", &max1);
	printf("exchange1(int x,int y)函数中运行前:x=%d  y=%d  \n", x, y);
	if (x > y) {
		max1 = x;
	}
	else
	{
		max1 = y;
		t = x;
		x = y;
		y = t;
	}
	printf("exchange1(int x,int y)函数中运行后:x=%d  y=%d  max1=%d\n", x, y,max1);
	printf("exchange1中,执行后max1的地址:max1=%p\n", &max1);
	return(max1);
}
//exchange2函数定义
int exchange2(int* x, int* y) {
	int max2,t;
	printf("exchange2中,执行前max2的地址:max2=%p\n", &max2);
	printf("exchange2(int* x,int* y)函数中运行前:x=%d  y=%d  \n", *x, *y);
	if (*x > *y) {
		max2 = *x;
	}
	else
	{
		max2 = *y;
		t = *x;
		*x = *y;
		*y = t;
	}
	printf("exchange2(int* x,int* y)函数中运行后:x=%d  y=%d   max2=%d\n", *x, *y,max2);
	printf("exchange2中,执行后max2的地址:max2=%p\n", &max2);
	return(max2);
}

运行结果:
在这里插入图片描述
代码分析:
(1)主函数上的最大值变量与子函数上的最大值变量虽然是同名,但可以从地址上来看,其实他们两不是同一个变量,只是名字相同而已,主函数上的变量跟子函数上的局部变量是相互独立、互补干扰的;

(2)观察 exchange1(int x, int y) 函数,你会发现该函数的调用【max1 = exchange1(a, y);】跟它的定义的第二个参数名字都是叫y,但最后虽然在exchange1函数里实现了交换,却在主函数中输出时仍是原来的值,由此可以见,形参与实参之间的传值是单向的,形参在传值的情况下是无法影响到实参的,同时也验证了上面关系中的第(5)条【形参与实参同名也只是个名字相同而已】。

(3)代码还突出了一个问题,那就是当实参传形参的时候,传值的时候形参是不会对实参参数影响,但当实参跟形参之间传的是地址,这时的双向的,形参的改变会影响实参。该代码用到的是指针来传地址(后面会详细介绍指针),传地址的形式还可以采用结构体、数组来实现(其实这三者的实质操作都是对地址进行操作),对于传数组,数组元素的作用与变量相当,在传地址的时候是把数组的第一位元素的地址传过去。

对于传数组元素需要注意的是:
(1)数组元素只能作函数的实参,不能用来作形参,因为形参的存储单元是临时分配的,不能单独的为一个数组而分配一串连续的存储单元在那;
(2)在用数组元素作实参时,采用的传递方式是值传递,是单向的。

对于传数组名注意的是:
(1)要在主函数与被调函数分别定义数组【即:将实参跟形参都定义为数组形式,不能只在一方定义】;
(2)实参数组与形参数组类型应该一致;
(3)形参数组那可以不指定数组大小,后面跟个方括号"[ ]"就好,因为编译系统不会检查形参数组的大小,只是看数组首地址而已。

(4)关于函数的声明问题,若子函数在主函数前,则可以不需要函数的声明,但若主函数在子函数之前,则必须进行子函数的声明,否则会报错说子函数未定义。(声明就如告诉主函数叫什么名字的这个东西是子函数来的,你后面会遇到的。)

(5) 在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,只有在发生函数调用的时候形参才会被临时分配内存单元,调用结束后就会自动释放。

2-3 返回值

返回值是函数被调用执行后返回到调用的函数处时所返回的结果,通过return语句来返回。
(1)返回语句一般形式:return(表达式)return 表达式
(2)没有返回值的函数为空类型,用void表示,一旦函数的返回值类型被定义为 void,就不能再接收它的值了。一般习惯是若不需要返回,那么就全部定义为void型。
(3)return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用都只能有一个 return 语句被执行,且每个return只能返回一个值,所以只有一个返回值(少数的编程语言支持多个返回值)。
(4)函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用。return 语句也是提前结束函数的唯一办法。
(5)返回值的类型要与函数类型一致,若不一致,则以函数类型为准,不用返回的时候就用void。

3、函数被调用的前提条件

(1)被调用的函数必须是已经定义好的函数;
(2)如果是调用函数库的函数,应该在开头用#include指令导入库的头文件;
(3)同一个文件中,如果在使用自定义的函数时,该函数的位置在主函数后面,那应该在主函数中对该函数进行声明,若在主函数前定义,则不需要声明。

函数原型:是函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息
声明格式:类型名 函数名(形参类型 形参名1,··· ,形参类型 形参名n);【形参名字可以省略,但形参类型不能省略】
其实就是没有函数体跟花括号,然后加多个分号而已,
如:int exchange1(int x,int y);

声明的作用:把函数的信息告知编译系统,以便遇到函数调用时,编译系统能正确识别函数,并检查调用是否合法。

“定义”与“声明”的区别:
  “定义”是指对函数功能的确立,明确函数的细节信息,是一个完整的、独立的函数单位;
  “声明”只是把函数的信息(不包括函数体)告知编译系统,以便系统按此进行对照检查。

声明的技巧:对于多个文件的程序,通常是将函数定义放到源文件(.c文件)中,将函数的声明放到头文件(.h文件)中,
使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。

四、局部变量和全局变量

在这有一个新的名词要跟大家提一下,那就是 “作用域” ,因为每一个变量都有属于自己的作用域,从而导致了出现了 “局部变量”“全局变量” 这两个新名词。决定变量作用域的关键因素是变量的定义位置。

变量定义的情况:
(1)在函数的开头定义; 【局部变量】
(2)在函数内的复合语句内定义; 【局部变量】
(3)在函数的外部定义。 【全局变量】

局部变量
定义在函数内部的变量称为局部变量,它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。
例如:

int f1(int a){
    int b,c;  //a,b,c仅在函数f1()内有效
    return a+b+c;
}
int main(){
    int m,n;  //m,n仅在函数main()内有效
    return 0;
}
注意事项:
1) main 函数也是一个函数,与其它函数地位平等。在 main 函数中定义的变量也是局部变
量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。
2) 形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变
量赋值的过程。
3) 可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干
扰,也不会发生混淆。
4) 在语句块中也可定义变量,它的作用域只限于当前语句块。

全局变量:
在所有函数外部定义的变量称为全局变量,它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。

注意:
(1)尽量少用全局变量,因为全局变量在程序的全部执行过程中都占用存储单元。
(2)全局变量会使函数的通用性和可移植性降低,因为当函数使用了全局变量,则执行时候
会受全局变量的数值影响,移植的时候也要考虑全局变量的移植(如:移植的时候需要担心
目标地址处是否与其余的变量有冲突)。
(3)使用全局变量过多,会降低程序的清晰性。
(4)同一个程序中,当局部变量与全局变量同名时,在局部变量的作用范围内,局部变量有
效,全局变量此时被“屏蔽”不起作用。

全局变量与局部变量区别示例: 【此示例源至 “C语言中文网” 】

#include <stdio.h>
int n = 10;  //全局变量

void func1(){
    int n = 20;  //局部变量
    printf("func1 n: %d\n", n);
}

void func2(int n){
    printf("func2 n: %d\n", n);
}

void func3(){
    printf("func3 n: %d\n", n);
}

int main(){
    int n = 30;  //局部变量
    func1();
    func2(n);
    func3();
    //代码块由{}包围
    {
        int n = 40;  //局部变量
        printf("block n: %d\n", n);
    }
    printf("main n: %d\n", n);
    return 0;
}

运行结果:
func1 n: 20
func2 n: 30
func3 n: 10
block n: 40
main n: 30

五、内部函数与外部函数

根据函数能否被其他源文件调用,将函数区分为 内部函数外部函数

1、内部函数(又称:静态函数)

概念: 只能被本文其他函数所调用,用 static 修饰。
定义格式: 定义时在函数首行的类型前加 static 。
static 类型 函数名(形参表) 如:函数的首行:static int fun(int a,int b)
使用规则: 函数的作用域只局限于所在的文件,这样,在不同的文件中即使有相同名字的内部函数,那也互不干扰。
用处: 多人开发的时候,分模块写,在各人编写自己的文件模块时,就不用担心与别人的重名了,可以提高代码的可靠性。

2、外部函数

概念: 如果在定义函数时,在函数首部的最左端加关键字 extern,则此函数是外部函数,可供其他文件调用。
定义格式: 定义时在函数首行的类型前加 extern。
extern 类型 函数名(形参表) 如:函数的首行:extern int fun(int a,int b)

注意:在定义函数时,可以省略extern,默认定义的函数都是外部函数,(即:平常我们没有
加修饰字符的函数都是外部函数)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值