【C初阶】函数

 

目录

1. 函数的概念

2. 库函数

3. 自定义函数

4. 形式参数和实际参数

4.1 实参(实际参数)

4.2 形参(形式参数)

4.3 形参和实参的关系

 5. return 语句

6. 数组做函数参数

7. 嵌套调用和链式访问(非常重要!)

7.1 嵌套调用

7.2 链式访问

8. 函数的声明和定义

 8.1 单个文件

8.2 多个文件

8.3 static 和 extern

8.3.1 static修饰局部变量

8.3.2 static修饰全局变量

8.3.3 static修饰函数


我们已经用过C标准库函数一段时间了,比如说:printf()、scanf()等等,现在呢,我们来具体学习一下函数。深入了解一下它。

1. 函数的概念

首先,什么是函数?函数(function)是完成特定任务的独立代码单元。语法规则定义了函数的结构和使用方式。

为什么要使用函数?首先,函数可以省去编写重复代码的苦差。如果程序要多次完成同一项任务,那么只需要编写一个合适的函数,就可以在需要时使用这个函数,或者在不同程序中使用该函数。其次,函数可以让程序更加模块化,提高了代码的可读性,方便了后期的更改。完善。

在C语言中,我们一般会见到两类函数:

  1. 库函数 
  2. 自定义函数

2. 库函数

库函数的体量是特别大的,我们是不可能在短时间内将它学习透彻的,但我们不需要死记硬背,随着时间的推移,我们会一点一点的学习到,这是急不来的,就像学习英语一样,是个细水长流的活儿,在这里我就不过多介绍了,(毕竟目前我也就用那么几个)

感兴趣的小伙伴可以自己在官网查看学习。我们在未来遇到不会的可以在以下两个工具中查找:

C/C++官方链接:https://en.cppreference.com/w/

cplusplus.com:https://legacy.cplusplus.com/

那么这些库文档该如何读?其一般格式为:

  1. 函数原型
  2. 函数功能介绍
  3. 参数和返回类型说明
  4. 代码举例
  5. 代码输出
  6. 相关知识链接

3. 自定义函数

函数的大家族内不只有库函数,库函数也不是万能的,我们也可以根据自己的需求来自定义函数

编写自定义函数,就可以创造无限的可能!

函数的形式大体上是一样的,其形式如下:

// 自定义函数
// 这里我们想实现俩个整数求和
// 自己编写一个函数
#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}
// Add 函数名 -- 就是我们给自己编写的函数起的名字,尽量和功能相联系。
// int (Add前的) 返回类型 -- 就是我们我们需要返回的值的类型,类型特别多,这里以int为例。
// 也可以啥都不返回void.
// int x, int y; 形式参数 -- 其中 int 是形式参数的类型 ,x,y就是形式参数,可有可无。有时候给个类型就行。
// {  } -- 大括号里面的叫做函数体,在这里面实现自己想实现的功能,

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int r = Add(a, b);      // 调用函数
	printf("%d", r);
	return 0;
}

以上的Add函数就是自定义函数,其形式就是自定义函数的形式,我们举一反三,就可以自己定义函数了,比如说:减法函数、乘法函数、除法函数等等。

由此,我们在定义函数的时候,要注意参数问题:参数的个数,参数的类型,形参的名字。

未来我们在设计自己的函数的时候,函数名、参数、返回类型的是灵活多变的。 

4. 形式参数和实际参数

4.1 实参(实际参数)

在第3节的示例代码中,有一行注释了调用函数,在调用Add函数时,传递给Add的参数a和b就是实际参数,简称实参

顾名思义,实际参数就是真实传递给函数的参数。

4.2 形参(形式参数)

在第3节示例代码中,定义Add函数的时候,在函数名后面的括号中的x、y就是形式参数,简称形参

为什么叫形式参数呢?因为,如果只定义了Add函数,但不调用,函数中的x、y只是形式上存在,不会向内存申请空间,不是真实存在的,所以叫做形式参数。

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

4.3 形参和实参的关系

虽然实参是传递给形参的,它们之间是有联系的,但是它俩所处的空间是独立的。

就拿第3节的示例代码为例,我们监视一下它们四个参数的地址:

所以我们可以看出以下几点:

  • 形参与实参有各自独立的空间
  • 形参的修改不会影响实参的值
  • 形参是实参的一份临时拷贝
  • 形参的名字和实参的名字可以相同,依然处于不同空间

 5. return 语句

在函数的设计中,我们经常会用到return语句,我们来详细介绍一下:

1.return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式后面的结果。表达式的话,第3节的代码示例就是表达式。

// 编写一个函数,判断用户输入的是否是奇数。

#include <stdio.h>
int is_odd(int n)
{
	if (n % 2 == 1)
		return 1;
	else
		return 0;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	if (is_odd(n))
		printf("是奇数\n");
	else
		printf("是偶数\n");

	return 0;
}

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

// 随便举个例子

#include <stdio.h>
void test(int n)
{
	// 如果n 是负数就直接返回
	// 如果n>20 就打印haha
	// 否则就打印hehe
	if (n < 0)
		return;
	else if (n > 20)
		printf("haha\n");
	else
		printf("hehe\n");
    printf("heihei");
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	test(n);

	return 0;
}

3. return语句执行后,函数就彻底返回,后面的代码就不再执行。

执行2内的代码,输入-10后直接返回。后面的heihei就直接不执行。

4.return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。

但是编译器会报警告。

这个时候直接把double类型的值转换为int类型的值,其中会丢失部分数据。

5.如果函数中存在if等分支语句,则需要保证每种情况下都有return返回,否则会有编译错误。(逻辑漏洞)

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

建议:如果返回类型明确,就写好;如果没有返回类型,返回类型就写void。

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

6. 数组做函数参数

在使用函数的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。

就随便举个例子:

// 写一个函数将一个整型数组的内容全部置为-1.并再写一个函数将其打印

#include <stdio.h>

// 将数组置为-1
void set_arr(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
		arr[i] = -1;
}

// 打印数组的内容
void print_arr(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
		printf("%d", arr[i]);
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	set_arr(arr, sz);    // 调用函数
	print_arr(arr, sz);  // 调用函数
	return 0;
}

// -1-1-1-1-1-1-1-1-1-1

在这个例子中,set_arr函数对数组内的元素进行了转换(全部变为-1),在这里面,我们需要遍历数组才能对数组内的每个元素进行赋值,因此我们需要知道数组内元素的个数。所以还需要第2个参数,数组元素的个数。同理,print_arr函数也是一样的。

一维数组的函数传参是这样,那么二维数组呢?二维数组传参的时候,行可以省略,但是列不能省略。其他的都是一个模式。

7. 嵌套调用和链式访问(非常重要!

7.1 嵌套调用

函数嵌套调用如同搭建积木,每个函数都是独立模块。当开发者将这些模块按需组合、层层嵌套,零散的代码就能串联成流畅的程序逻辑,最终构建出功能完备的应用系统,实现从代码片段到完整程序的蜕变。

举一个例子:确定某年某月有多少天?

题目分析:

  1. 每一年的每月有多少天是确定的,但是,二月是例外,闰年有29天,平年有28天
  2. 首先我们需要知道要输出多少天,而在知道具体输出多少天之前,需要知道是什么年
  3. 需要一个判断是闰年还是平年的程序
  4. 之后在2月上加1天
// 确定某年某月有多少天
#include <stdio.h>
#include <stdbool.h>
bool is_leap_year(int y)
{
	if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
		return true;
	else
		return false;
}
int get_days_of_month(int y, int m)
{
	int day[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	//            0 1  2  3  4  5  6  7  8  9  10 11 12
	int d = day[m];
	if (is_leap_year(y) && m == 2)
	{
		d += 1;
	}
	return d;
}
int main()
{
	int year = 0;
	int month = 0;
	printf("请输入年份和月份\n");
	scanf("%d %d", &year, &month);
	int day = get_days_of_month(year, month);
	printf("%d", day);
	return 0;
}

在这串代码中,每一个函数的功能都是单一的,这样做的原因是,方便函数的重复调用,只要足够单一,它就只完成一个任务,方便组合。

函数是可以嵌套调用的,但是函数是不能嵌套定义的。

7.2 链式访问

 把一个函数的返回值作为另一个函数的参数,这就是链式访问。

示例:

// 链式访问
#include <string.h>
#include <stdio.h>
int main()
{
	printf("%d", strlen("abcdef")); // 链式访问
	return 0;
}

这个就是链式访问,把strlen的返回值直接作为printf的参数。

8. 函数的声明和定义

 8.1 单个文件

一般情况下,我们使用函数,就是写出来直接使用了。

就比如:编写一个函数判断是不是闰年。

// 直接写一个函数,判断是不是闰年

#include <stdio.h>
int is_leap_year(int y)    // 函数定义
{
	if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
		return 1;
	else
		return 0;
}
int main()
{
	int year = 0;
	scanf("%d", &year);
	if (is_leap_year(year))      // 函数调用
		printf("是闰年\n");
	else
		printf("是平年\n");
	return 0;

}

 这种情况是函数定义在函数调用之前,程序会正常运行。但是如果把is_leap_year,放在main函数后呢?

编译器会报警告。这是为什么?因为编译器执行程序是从上到下执行的,当遇到函数调用那一行时,编译器就会报警告。那么如何消除警告呢?

就是在函数调用之前先声明一下这个函数,声明函数只需要交代清楚:函数名,函数的返回类型,函数的参数,就行。就是如下:

// 函数声明
// 就是告诉编译器,有一个函数,它的返回类型是什么,参数是什么,名字是什么。
int is_leap_year(int y);
int main()
{
	int year = 0;
	scanf("%d", &year);
	if (is_leap_year(year))
		printf("是闰年\n");
	else
		printf("是平年\n");
	return 0;

}

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

至于为什么第一种写法,编译器不报警告,这是因为,函数定义是一种特殊的函数声明,而在函数声明中参数的名字是可以省略的只需要告诉参数类型就行。

所以:函数调用时,需要先声明后使用。

8.2 多个文件

当程序规模变大,单文件代码会变得杂乱难维护。多文件编程能把不同功能拆分到独立文件里,让代码结构清晰、方便修改和重复使用。下面,我们就来学习如何编写多文件程序。

一般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现是放在源文件(.c)中的 。

至于如何使用,我们在下一节扫雷游戏中可以见到,在这里就不多加叙述了。只需要看个示例,相信聪明的你一定会在心中有谱的。

8.3 static 和 extern

static和extern都是C语言的关键字。

static是 静态的 可以用来:修饰局部变量,修饰全局变量,修饰函数

extern是用来声明外部符号的。外部符号可以是函数、全局变量等。

在学习static和extern之前,我们先来了解一下:作用域和生命周期。

作用域:(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总不有效(可用)的,而限定这个名字的可用性代码范围就是这个名字的作用域。 

  • 局部变量的作用域是变量所在的局部范围。
#include <stdio.h>
int main()
{
	{
		int n = 100;
		printf("1: %d", n);
	}
	// 这个n只能在大括号内使用,也就是作用域
	printf("2: %d", n); // err  未定义
	return 0;
}
  • 全局变量的作用域是整个项目。 
#include <stdio.h>
int n = 100;
int main()
{
	{
		
		printf("1: %d", n);
	}
	
	printf("2: %d", n); 
	// 这样子,就可以在该项目中任意使用。
	return 0;
}

 生命周期是变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。

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

8.3.1 static修饰局部变量

请看如下,两段代码,试着考虑它们的输出是否一样。

#include <stdio.h>
void test()
{
	int i = 0;
	i++;
	printf("%d ", i);
}
int main()
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		test();
	}
	return 0;
}
#include <stdio.h>
void test()
{
	static int i = 0;
	i++;
	printf("%d ", i);
}
int main()
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		test();
	}
	return 0;
}

 这两段代码的输出分别是:1 1 1 1 1;1 2 3 4 5。这是为什么?当然是static的作用。

第一段代码:test函数中的局部变量 i 是每次进入test函数先开始生命周期,并赋值为0,然后 ++ 再打印,出了test函数,其的生命周期终止。

第二段代码:i 的值具有累加效果,顾可以分析出,static修饰的变量出函数时未被销毁,直接累计上次的值。

8.3.2 static修饰全局变量

 这两张图片,一张是有static修饰全局变量的,一张没有,从运行结果来看,

static修饰的全局变量,只能在本源文件中使用,不能在其它源文件使用。其原因是,把外部属性转为了内部属性。在其他源文件中即使声明了也是无法使用的。

8.3.3 static修饰函数

同样的,函数直接使用是可以正常运行的,但是加了static就出现了链接错误。

和全局变量是一样的,函数被static修饰后,只能在自己所在的源文件中使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值