(本文为自己学习记录,如有错误欢迎指正)

一、什么是函数

C语言中也可叫子程序,是完成某项特定任务的一段代码。

函数一般分为两类,

1.库函数:标准库中提供的函数,比如scanf,printf函数等,这些都是已经写好并校正多次的函数,bug相对较少,可以在https://zh.cppreference.com/w/c/header中查询。

(因为C语言并不提供现成的库函数,所以应用不同的头文件进行声明)

2.自定义函数

可用来实现自己的一个需求。

二、怎么使用函数

1.库函数:先声明再使用。

举例:可以实现两数平方的函数。

#include <stdio.h>
#include<math.h>
int main() {
	float a;
	a = sqrt(16.0);
	printf("%lf", a);
	return 0;
}

在这段代码中我们调用了printf和sqrt函数,他们分别包含在stdio.h和math.h头文件中,需先声明再调用。

2.自定义函数

在编写和调用自定义函数时,我们需要了解以下知识。

(1)语法形式:

ret_type是函数返回类型,

fun_name是函数名,

括号中放的是形式参数,

{}中的是函数体。

某些简单的函数,不需要参数和返回值,void表示不需要参数和返回值。

p.s.函数参数列表为  void  时(如  void func(void) ),明确说明该函数调用时不需要传入任何参数。
注意 :若仅写  void func() (省略参数列表中的  void ),在C语言中表示“参数数量和类型不确定”,而非“无参数”,二者含义不同,推荐始终用  void func(void)  表达无参数。

举例:

void print() {
	printf("hello world\n");
}

                                                                           图1

实现两数之和的函数:

int Add(int x, int y) 
{
	return x + y;
}

int main() {
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("%d\n", Add(a, b));
	return 0;
}

                                                                              图2

(2)形参与实参

在图2代码中,a,b为实际参数,x,y为形式参数。

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

形式参数是在定义了函数而没有调用时里面的参数,在没有调用时不会向内存申请空间。

当函数被调用后,实参传到形参,形参才开辟内存空间,形参与实参的地址不同,相当于是实参的临时拷贝。

(对形参的修改,不会影响实参)

(3)在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项。

 • return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式 的结果。

举例

图二(return的是表达式)以及

//编写一个函数,判断数字是奇数还是偶数
//是奇数返回1
//是偶数返回0

int is_even_number(int n)
{
	if (n % 2 == 1)
		return 1;
	else
		return 0;
}

int main() {
	int a = 0;
	scanf("%d", &a);
	int ret = is_even_number(a);
	if (ret == 1)
		printf("奇数\n");
	else
		printf("偶数\n");
}

                                                                       图三(return的是数值)

 • return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。

(在一个函数执行的过程中,需要提前返回,但是不需要带回任何值,则写return)

例如

//这串代码只能返回1
void test()
{
	printf("1\n");
	int n = 5;
	if (n % 2 == 1)
		return;
	printf("2\n");
}
int main()
{
	test();
	return 0;
}


//这串代码输入奇数只返回1
//输入偶数返回1,2
void test(n)
{
	printf("1\n");
	if ( n % 2 == 1)
		return;
	printf("2\n");
}
int main()
{
	int a;
	scanf("%d", &a);

	test(a);
	return 0;
}

                                                                                     图四

 • return语句执⾏后,函数就彻底返回,后边的代码不再执⾏。

图四为例

 • return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。

例如

 //return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。

 //如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
int test()
{
	int n = 1;
	if (n == 1)
	
		return 3.5;
	else
		return -3.5;
	
}

int main() {
	int a = test();
	printf("%d", a);
	return 0;

}

                                                                                图五

• 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。

//如果某些返回值不明确
int test()
{
	int n = 3;
	if (n == 1)
		return 3.5;
}

int main() {
	int a = test();
	printf("%d", a);
	return 0;

}
//这两段代码的返回值将不一样
int test()
{
	int n = 3;
	printf("hello\n");
	if (n == 1)
		return 3.5;
}

int main() {
	int a = test();
	printf("%d", a);
	return 0;

}

                       图六(返回的值是不固定的,两个代码返回了不同的值,即返回值是不可预测的)

• 函数的返回类型如果不写,编译器会默认函数的返回类型是int。

//不写返回值类型默认是int类型
test()
{
	printf("hello\n");
}

int main() {
	int a = test();
	printf("%d", a);
	return 0;

}

图七(此代码的运行结果为整数)

• 函数写了返回类型,但是函数中没有使⽤return返回值,那么函数的返回值是未知的。

(4)数组传参

在函数里数组也是可以当作参数传递的。

例如:

//创建一个函数使数组中的元素全部改为1
void set_arr(int arr[10], int sz)
{
	int i = 0;
	for (i = 0;i < sz;i++)
	{
		arr[i] = -1;
	}
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0;i < sz;i++)
	{
		printf("%d", arr[i]);
	}
	printf("\n");
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//计算出数组中的元素个数
	print_arr(arr, sz);
	//写一个函数用于打印数组内容
    set_arr(arr, sz);
	//写一个函数将数组的内容全部置为-1
	print_arr(arr, sz);
	//再打印出来
	return 0;
}

图八(一维数组传参)

数组作为参数传递给了set_arr 和 print_arr函数了,这里我们需要知道数组传参的几个重点知识:
函数的形式参数要和函数的实参个数匹配,有几个形参
函数的实参是数组,形参也是可以写成数组形式的
形参如果是一维数组,数组大小可以省略不写(因为本身就不创建新数组,这个大小不重要)(图八函数2)
形参如果是二维数组,行可以省略,但是列不能省略
数组传参,形参是不会创建新的数组的
形参操作的数组和实参的数组是同一个数组

//二维数组传参
void print_arr2(int arr[3][5], int r, int c)
{
	int i = 0;
	for (i = 0;i < r;i++)
	{
		int j = 0;
		for (j = 0;j < c;j++)
		{
			printf("%d", arr[i][j]);
		}
		printf("\n");
	}
}

int main() {
	int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
	print_arr2(arr,3,5);
	return 0;
}

图九(二维数组传参)

3.函数的嵌套调用和链式访问

(1)什么是嵌套调用呢?

在一个函数中调用其他函数,

//输入某年某月,这段代码输出这个月有多少天

int is_leap_year(int y)
{
	if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
		return 1;
	else
		return 0;
}

int get_days_of_month(int y, int m)
{
	int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int d = days[m];
	//闰年的月要特殊处理
	if (is_leap_year(y) && (m == 2))
		d += 1;
	return d;
}

int main() {
	int year = 0;
	int month = 0;
	scanf("%d %d", &year, &month);
	int d = get_days_of_month(year,month);
	printf("%d\n", d);
	return 0;
}

图十(在main函数中调用了函数1,在函数1中调用了函数2)

(2)链式访问

顾名思义就是将函数像链子一样串起来,用的就是一个函数的返回值被另一个函数调用这种方式来串起来。

//函数的嵌套
int main() {
	printf("%zd", strlen("abcdef"));
	return 0;
}

//以上代码等同于这个形式

int main() {
	size_t  a = strlen("abcdef");
	printf("%zd", a);
	return 0;
}

图十一(strien函数的返回值被printf函数调用)

p.s.一段有趣的代码

已知printf函数的返回值是

总共被写的字符个数

//printf函数的返回值为打印的字符的个数
int main() {
	printf("%d", printf("%d", printf("%d", 43)));
	printf("\n");
	printf("%d", printf("%d", printf("%d\n", 43)));
	return 0;
}

图十二

4.函数的声明

函数的定义和调用有一定顺序,一般是先定义再调用。

如果函数的定义在调用之后,应该在调用前先声明。

即:先声明再调用。


//函数如果在调用之后定义,应先声明再调用。
int is_leap_year(int y);

int main() {
	int year = 0;
	scanf("%d", &year);
	if (is_leap_year(year)) 
		printf("是闰年\n");
	else
		printf("不是闰年\n");
	
}

int is_leap_year(int y)
{
	if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
		return 1;
	else
		return 0;
}

图十三(函数的先声明再调用)

以上代码是在同一个文件中的情况,但是一般我们不能把所有函数都放在同一个文件中,那么在多文件中我们又应该如何进行函数的声明和调用呢?

在一个.c文件中定义函数,在一个.h(头文件)中声明函数,再在主函数所在的.c文件中包含这个头文件(#include<xxxx.h>)即可。

图十四(一个简单的加法函数)

(易错点,我自己敲代码遇到的问题,自定义的头文件用  “  ”  而不是   < >  ,用错了代码就运行不了啦)

这样就能实现函数和主函数分开(即不放在同一文件中)的效果,利于保留函数的使用和维护主权等,也便于多人的分工。

5.static和extern

(1)首先说下作用于和生命周期这两个概念:

作用域:限定一个名字可用性的代码范围就是这个名字的作用域。

生命周期:一个变量从创建到销毁之间的一个时间段。

能直观体现出作用域对变量定义和使用的影响的是 “ { } ” ,
 
1.它可以限定局部变量的作用域:在花括号内定义的局部变量,仅能在该花括号所包裹的代码块内部被访问和使用,出了花括号则失效(内存被释放)。
2. 隔离变量作用域:不同花括号内可定义同名变量,它们相互独立,互不干扰。

(不同的函数同理)

(2)extern:用来声明外部符号(声明或者定义函数时才用)

static:一个全局变量或函数被关键字static修饰后,就只能在本源文件中使用。(起到一个保护的作用),

修饰局部变量会改变其生命周期(不改变作用域),(即创建后不销毁还能继续用,只不过可用范围还是在相同函数,效果就是这个函数被再次调用后这个变量的值会被保留)将变量的储存位置从栈区改变到静态区,生命周期和程序的生命周期一样了。

这里举一个例子:

//static改变变量的生命周期
void test() {
	int a = 1;
	a++;
    printf("%d", a);
}

int main()
{
	int i = 0;
	for (i = 0;i < 5;i++) {
		test();
	}
	return 0;
}


//加上static后
void test() {
	static int a = 1;
	a++;
	printf("%d", a);
}

int main()
{
	int i = 0;
	for (i = 0;i < 5;i++) {
		test();
	}
	return 0;
}

图十五

这张图中上下两个代码作对比,第二个代码test函数中的a变量在定义时前面加上了static修饰,

第一组代码运行结果为22222,第二组运行结果为23456,显然,第二组的a的生命周期被改变了。

图十六

extern可以声明外部函数或变量,但如果该函数或变量被static修饰则不能被调用。如图十七

图十七

函数同理。

如果一个全局变量或函数只想在当前原文件中使用,不想被别的文件发现,就使用static修饰。

今天的分享就先到这里啦!

Logo

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

更多推荐