我们在数学学习的过程中就遇到了各种各样的函数,比如一次函数等

C语言中也引入了函数这一概念,它的英文是function,有时候也将它翻译为子程序。C语言中的函数就是完成某项特定任务的代码,这段代码是有特殊的写法和调用方法的。

函数是C语言的核心组成部分,用于封装可复用的代码块,提高程序模块化和可维护性。

在C语言中我们经常会见到两类函数:

  • 库函数
  • 自定义函数 

库函数 

C语言标准库(Standard Library)提供了一系列预定义的函数,涵盖输入输出、数学计算、字符串处理、内存管理等功能。这些函数通过头文件(.h)引入,极大提高了开发效率。

之前我们常用的printf,scanf等就是库函数。

库函数的种类有很多,我们之后也会一一学习,想要提前学习库函数的小伙伴可以点击以下链接:C library - C++ Reference 

这里面也提供了库函数的使用方法与说明。

自定义函数

前面我们简单了解了库函数,这一小节我们的关注度应当在自定义函数上,自定义函数能给程序员写代码提供更多的创造性。

函数的语法形式

ret_type  fun_name (形式参数)

       函数体

}   

  • ret_type :函数的返回类型,当然,函数的返回类型也可以是void类型,表示什么也不返回
  • fun_name :函数名,可以根据实际需求自己命名
  • 函数的参数相当于工厂加工的原材料,如果函数没有参数,括号内可以什么也不写,或者可以写上void,如果有参数,就要交代清楚参数的类型和名字。
  • {}内部的就是函数体,实现函数所能完成的功能

函数的举例 

要求:写一个加法函数,返回两个整数的和

首先,我们要明确函数的功能,就是实现整数的加法。那我们写的函数就一定要能接收两个整数,也就是说函数的参数会是两个整型类型。同时,经过函数计算以后,我们要得到两个函数的和,也就是说,这个函数要返回一个整形类型。

通过以上分析,可以写出代码:

#include<stdio.h>
//定义函数
int add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int a, b;
	//输入
	scanf("%d%d", &a, &b);
	//调用加法函数,完成a和b的相加
	//将函数返回的结果放在r中
	int r = add(a, b);
	//输出结果
	printf("%d\n", r);
	return 0;
}

其实,add函数也可以简化:

int add (int a , int b)
{
    return a + b ;
}

实参和形参

  • 实参

上述代码中,我们在main函数中调用了add函数,并在调用过程中给函数传入了两个参数a和b,这两个参数就叫做实际参数,简称实参。

实际参数就是真实传递给函数的参数。 

  • 形参 

上述代码,在函数定义的时候,函数名add后的括号中所写的x和y,称为形式参数,简称形参。

为啥要叫它为形参?

实际上,如果只是定义了add函数,而不去调用,add函数的参数就只是形式存在的,不会向内存去申请空间,所以并没有真实存在,故称之为形参。

形式参数只有在函数被调用的过程中为了存放实参传递过来的值的时候才会向内存申请空间,这个过程就是形参的实例化。 

形参和实参所使用的变量是可以相同的。

  • 形参和实参的关系 

实参可以将自身的值传递给形参,由此建立联系,实际上,形参和实参在内存中的存放是相互独立的,这一现象我们可以通过对上面代码的调试来观察。

有上述调试过程我们可以看到,在调用函数时x和y都分别得到了a和b的值,但是x与a,y与b的地址并不相同,所以我们可以理解为:形参是实参的一份临时拷贝。 

return语句

return 是函数中控制流程和返回结果的关键语句,正确使用它直接影响程序的正确性和健壮性。在使用过程中,应当注意以下方面:

  •  return 后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
  •  return 后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是 void 的情况。
  •  return 语句执行后,函数就彻底返回,后边的代码不再执行。
  •  return 返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。
  •  如果函数中存在 if 等分支的语句,则要保证每种情况下都有 return 返回,否则会出现编译错误。
  •  函数的返回类型如果不写,编译器会默认函数的返回类型是 int。
  •  函数写了返回类型,但是函数中没有使用 return 返回值,那么函数的返回值是未知的。

数组作为函数参数 

当函数的参数涉及数组时,形参和实参应当如何写,有哪些需要注意的地方?

当数组作为函数参数时,实参直接写数组名即可,由于函数体内部要对数组进行相关操作。所以需要知道数组的大小,所以在定义函数时,形参中还应当有数组的大小。那么,数组在形参中应当如何表示呢,我们需要先知道数组传参的几个知识:

  •  函数的形式参数要和函数的实参个数匹配
  •  函数的实参是数组,形参也是可以写成数组形式的
  •  形参如果是一维数组,数组大小可以省略不写
  •  形参如果是二维数组,行可以省略,但是列不能省略
  •  数组传参,形参是不会创建新的数组的,也就是说形参和实参共用一份内存空间,而不是独立存在的
  •  形参操作的数组和实参的数组是同一个数组

由此我们可以写一个简单的函数来感受数组传参 

//写一个函数,打印数组中的内容
#include<stdio.h>
void print(int arr[], int size)  //这里也可以写为:int arr[10]
{
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	print(arr, 10);
	return 0;
}

函数的声明与定义 

一般我们在使用函数时,直接将函数定义好了就可以拿去使用了,比如,我们写一个函数判断某一年是否是闰年:

#include<stdio.h>
#include<stdbool.h>

bool is_leapyear(int year)
{
	if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
	{
		return true;
	}
	return false;
}

int main()
{
	int year;
	scanf("&d", &year);
	if (is_leapyear(year))
	{
		printf("是闰年\n");
	}
	else
	{
		printf("是平年\n");
	}
	return 0;
}

上述代码函数的定义在函数的调用之前,并没有出现错误,但如果我们把函数的定义写在函数的调用之后,如下:

int main()
{
	int year;
	scanf("&d", year);
	if (is_leapyear(year))
	{
		printf("是闰年\n");
	}
	else
	{
		printf("是平年\n");
	}
	return 0;
}

bool is_leapyear(int year)
{
	if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
	{
		return true;
	}
	return false;
}

将代码在vs2022上进行编译,就会出现警告信息:

 这是因为C语言在对源代码进行编译的时候,是从上到下顺序执行代码的,所以会提前遇到函数的调用,却没有看到函数的定义,就发出了上述警告。

怎么解决这个问题呢?

函数在调用之前应该声明一下,声明函数时需要交代清楚函数名,函数的返回类型和函数的参数。比如上面例子中函数的声明应当写为:bool is_leapyear(int year)。函数的声明中形参部分只保留参数类型,而忽略掉形参的名字也是可以的,如:bool is_leapyear(int )

修正后的代码就能正常编译了:

bool is_leapyear(int year);
int main()
{
	int year;
	scanf("&d", year);
	if (is_leapyear(year))
	{
		printf("是闰年\n");
	}
	else
	{
		printf("是平年\n");
	}
	return 0;
}

bool is_leapyear(int year)
{
	if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
	{
		return true;
	}
	return false;
}

所以,函数的调用一定要满足先声明再使用,函数的定义也是一种特殊的声明,所以把函数定义放在函数调用之前也是可以正常编译的。 

多个文件

一般我们在写一个项目的时候,代码可能比较多,不会把所有的代码放在一个文件中,这时我们就会根据文件的功能,将代码拆分放在多个文件中。

一般情况下,函数的声明,类型的声明,我们可以放在头文件(.h)中,函数的具体实现过程可以放在源文件(.c)文件中。此外还可以定义一个测试文件(.c文件),来测试我们写的函数是否可以达到预期功能,比如:

add.c

//函数的定义

int add (int x , int y)

{

     return x+y ;

}

add.h 

//函数的声明

int add (int x , int y);

//其他头文件的包含

#include<stdio.h>

test.c

//注意是用双引号包含自己定义的头文件

 #include"add.h"

int main()

{

    int a=10,b=20;

    int c=add(a , b);

    printf("%d",c);

return 0;

}

static 和 extern

staticextern都是C语言中的关键字。

static是“静态的”意思,可以用来:

  • 修饰局部变量
  • 修饰全局变量
  • 修饰函数

extern是用来声明外部符号的,如果一个全局的符号(如变量)在A文件中定义,而我们想在B文件中使用,就可以使用extern进行声明,然后使用。

在讲解static和extern之前,我们先来讲一下作用域生命周期

作用域 是程序中变量、函数或其他标识符的 有效访问范围,决定了这些标识符在代码的哪些部分可以被使用。

局部变量的作用域是变量所在的局部范围;全局变量的作用域是整个工程(项目)。

生命周期 指变量或对象在程序运行期间 从创建到销毁的时间段,决定了何时分配内存、何时释放内存。

局部变量的生命周期是进入作用域变量创建,生命周期开始,出作用域生命周期结束;全局变量的生命周期是整个程序的生命周期。

static修饰局部变量

对比以上两个代码,理解static的作用:

代码1的test函数中的局部变量i是每次进入test函数先创建变量(生命周期开始)并赋值为0,然后++,再打印,出函数的时候变量生命周期结束(释放内存)。

代码2中,从输出结果来看,i的值具有累加的效果,其实test函数中的i创建好后,出函数时是不会被销毁的,重新进入函数也就不会重新创建变量,直接用上次累积的数继续计算。

结论:static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,本来一个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样了,只有程序结束,变量才销毁,内存才回收。但是作用域不变的。

所以,如果一个变量在出函数以后,我们还想保留值,等下次进入函数继续使用我们就可以使用static进行修饰。

static修饰全局变量

一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
本质原因是全局变量默认是具有外部链接属性的,在外部的文件中想使用,只要适当的声明就可以使用;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明了,也是无法正常使用的。
所以,如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用
static 修饰。

static修饰函数

其实 static 修饰函数和 static 修饰全局变量是一模一样的,一个函数在整个工程都可以使用,
被 static 修饰后,只能在本文件内部使用,其他文件无法正常的链接使用了。
本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个工程中只要适当的声明就可以被使用。但是被 static 修饰后变成了内部链接属性,使得函数只能在自己所在源文件内部使用。
所以,一个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用 static 修
饰。

 

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐