[C]基础5.函数

  • 博客主页:算法歌者
  • 本篇专栏:[C]
  • 您的支持,是我的创作动力。


0、总结

在这里插入图片描述

1、概念

函数:是一个自带声明和语句的一小段代码。

可以利用函数把程序划分成小块,便于人们理解和修改程序。函数可以复用,提升开发软件的效率。

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

  • 库函数
  • 自定义函数

2、库函数

标准库:C语言的国际标准ANSI C规定了一些常用函数的标准。

库函数:不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。

注意,C语言标准中规定了C语言的各种语法规则,并不提供库函数。

查阅库函数文档链接:

例1:查阅sqrt

// 查阅文档可知以下:
// sqrt 是函数名字
// x 是函数的参数,表示调用sqrt函数需要传递一个double类型的值
// double 是返回值类型,表示函数计算的结果是double类型的值
double sqrt (double x);

库函数文档的一般格式如下:

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

3、自定义函数

1、语法形式

自定义函数和库函数是一样的,语法如下:

ret_type fun_name(形式参数)
{
    
}
  • ret_type:函数返回类型
  • fun_name:函数名
  • 括号中放的是:形式参数
  • {}括起来的是:函数体

在这里插入图片描述

可以把函数想象为一个工厂,工厂需要输入各种原材料,经过工厂加工输出产品,那么函数也是一样的,函数一般会输入一些值(可以是0个,也可以是多个),经过函数的计算,得出输出的结果。

例1:写一个加法函数,完成2个整型变量的加法操作。

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int r = Add(a, b);
	printf("%d\n", r);
	return 0;
}
输入:
3 4
输出:
7

Add函数可以简化为:

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

2、形参和实参

函数使用的过程中,函数的参数可分为形参和实参。

分析代码:

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}

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

在上面代码中,第2~5行是Add函数的定义,有了函数后,在第12行调用Add函数。

实参(实际参数):在第12行调用Add函数时,传递给函数的参数ab

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

形参(形式参数):在第2行定义函数时,在函数名Add后的括号中写的xy

如果只定义Add函数而不去调用,Add函数的参数xy只是形式上存在的,不会向内存申请空间,所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程称之为形参的实例化。

在这里插入图片描述

3、形参和实参的关系

观察代码的调试:

在这里插入图片描述

可以总结:

  • 形参和实参是完全不同的内存空间
  • 形参是实参的一份临时拷贝
  • 形参的修改不会影响实参

4、return语句

在函数的设计中,经常出现return语句,注意事项如下:

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

5、数组做函数参数

在使用函数时,有时会遇见数组作为参数传递给函数,然后在函数内对数组进行操作。

例1:写一个函数对一个整型数组的内容全部设置为-1,再写一个函数打印数组的内容。

#include <stdio.h>

void set_arr(int arr[], 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]);
	set_arr(arr, sz); // 设置数组内容为-1
	print_arr(arr, sz);// 打印数组内容
	return 0;
}
输出:
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1

问题:为什么要对函数传数组长度,不能在函数内求数组长度吗?

#include <stdio.h>

void print_arr(int arr[])
{
	int z = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n",z);
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	print_arr(arr);
	return 0;
}
输出:
2

从代码的结果来看,函数内求不出数组的长度。

这是因为在arr定义为int [10]类型时,它明确表示了一个包含10个整数的数组,当这个数组作为参数传递给函数时,类型发生了变化,不再是原始的数组类型,而是退化为了int *类型,即一个指向int类型数据的指针。

这种类型退化意味着,在函数内部接收到的参数仅仅是一个指向数组首元素的指针,而不再包含关于原数组大小(即10个元素)的信息,函数内部无法通过指针来推断求原始数组的长度。

结果为2,是sizeof(int*) / sizeof(int) = 8 / 4 = 2

注:一个指针的大小,在64位是8字节,在32位是4字节。(1字节 = 8位)

以上总结:

  • 函数的形参和函数的实参个数匹配。

  • 函数的实参是数组,形参也可以写成数组形式的。

  • 形参如果是一维数组,数组大小可以省略不写。

  • 形参如果是二维数组,行可以省略,列不能省略。

  • 数组传参,形参不会创建新的数组。

  • 形参操作的数组和实参的数组是同一个数组。

6、嵌套调用

嵌套调用:函数之间的互相调用。

例1:计算某年某月有多少天?用函数实现。

// is_leap_year() 根据年份确定是否是闰年
// get_days_of_month() 根据月计算这个月的天数
#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 get_days_of_month(int y, int m)
{
	int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = days[m];
	if (is_leap_year(y) && m == 2)
	{
		day += 1;
	}
	return day;
}
int main()
{
	int y = 0;
	int m = 0;
	scanf("%d %d", &y, &m);
	int d = get_days_of_month(y, m);
	printf("%d\n", d);
	return 0;
}
输入:
2024 2
输出:
29

以上代码,main函数调用了scanfprintfget_days_of_monthget_days_of_month函数调用is_leap_year

注意,在一个函数的内部不能直接定义另一个函数,即函数不能嵌套定义。

7、链式访问

链式访问:将一个函数的返回值作为另一个函数的参数,像链条一样将函数串起来。

例1:

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

例2:

#include <stdio.h>
int main()
{
    // printf函数返回的是打印在屏幕上的字符的个数。
	printf("%d", printf("%d", printf("%d", 43))); 
	return 0;
}
输出:
4321

4、函数的声明和定义

1、单个文件

例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 y = 0;
	scanf("%d", &y);
	int r = is_leap_year(y);
	if (r == 1)
		printf("闰年\n");
	else
		printf("非闰年\n");
	return 0;
}
输入:
2023
输出:
非闰年

以上代码,第2行~第8行是函数的定义,第13行是函数的调用

如果把函数的定义放在函数的调用后边,如下:

#include <stdio.h>
int is_leap_year(int y);
int main()
{
	int y = 0;
	scanf("%d", &y);
	int r = is_leap_year(y);
	if (r == 1)
		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;
}
输入:
2024
输出:
闰年
  • 第2行代码是函数的声明。声明中交代清楚:函数名、函数的返回类型、函数的参数。

  • 函数声明中参数可以只保留类型,省略掉名字。

  • 函数的定义也是一种特殊的声明,如果函数定义放在调用之前也是可以的。

2、多个文件

如果代码比较多,不会将所有的代码都放在一个文件中,会根据程序的功能,将代码拆分放在多个文件中。

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

比如:

add.c

// 函数的定义
int Add(int x, int y)
{
	return x + y;
}

add.h

// 函数的声明
int Add(int x, int y);

test.c

#include <stdio.h>
#include "add.h"

int main()
{
	int a = 10;
	int b = 20;
	// 函数调用
	int c = Add(a, b);
	printf("%d\n", c);
	return 0;
}
输出:
30

在这里插入图片描述

此外,多文件书写不仅逻辑清晰,还能方便多人协同。

例如,开发一个计算器,找四个人分别负责加法功能、减法功能、乘法功能、除法功能。如图:

在这里插入图片描述

3、static和extern

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

1、static是静态的意思,可以用来:

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

2、extern是用来声明外部符号的。

讲解staticextern之前,有必要先讲作用域生命周期

1、作用域(scope):限定这个名字的可用性的代码范围。

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

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

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

例1:static修饰局部变量:

// 代码1
#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;
}
输出:
1 1 1 1 1
// 代码2
#include <stdio.h>
void test()
{
	// static修饰局部变量
	static int i = 0;
	i++;
	printf("%d ", i);
}
int main()
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		test();
	}
	return 0;
}
输出:
1 2 3 4 5

通过以上代码,发现static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,原先局部变量是存储在内存的栈区,经过static修饰后存储到静态区。存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样,只有程序结束,变量才销毁,内存才回收。但是作用域不变的。

在这里插入图片描述

例2:static修饰全局变量

代码1:

add.c

int g_val = 2018;

test.c

// 代码1
#include <stdio.h>
extern int g_val;
int main()
{
	printf("%d\n", g_val);
    return 0;
}
输出:
2018

代码2:

add.c

static int g_val = 2018;

test.c

// 代码2
#include <stdio.h>
extern int g_val;
int main()
{
	printf("%d\n", g_val);
    return 0;
}
运行错误

通过以上代码,一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。

因为全局变量默认是具有外部链接属性,在外部的文件想使用,只要适当的声明就可以使用。但全局变量被static修饰后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用。其他源文件即使声明,也无法正常使用。

注:extern是用来声明外部符号的,如果一个全局的符号在A文件中定义,在B文件中想使用,就需要extern进行声明,然后使用。

例3:static修饰函数

代码1:

add.c

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

test.c

// 代码1
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
	printf("%d\n", Add(2, 3));
	return 0;
}
输出:
5   

代码2:

add.c

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

test.c

// 代码2
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
	printf("%d\n", Add(2, 3));
	return 0;
}
运行错误

通过以上代码,发现static修饰函数和static修饰全局变量是一模一样的。原因是因为函数默认是具有外部链接属性,使得函数在整个工程中只要适当的声明就可以使用。但函数被static修饰后就变成了内部链接属性,使得函数只能在自己所在的源文件内部使用。


完。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值