【C语言入门必看】函数

本文详细介绍了编程中函数的概念,包括库函数、自定义函数、实际参数和形式参数。讨论了函数的两种调用方式——传值调用和传址调用,并通过实例展示了它们的区别。深入探讨了函数的递归和迭代,以及两者的优缺点。此外,还阐述了函数的嵌套调用、链式访问以及函数的声明与定义。最后,提到了函数的分文件编写,强调了头文件和源文件的作用,以提高代码组织和维护的效率。
摘要由CSDN通过智能技术生成

一、函数是什么?

维基百科中对函数的定义:子程序
①子程序是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定的任务,相较于其他代码,具有相对的独立性。
②一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
在编程中,函数可让你命名一组命令,之后你可以随时运行这组命令。

二、函数的类型

1.库函数

开发过程中,我们会频繁使用某些功能,为了提高程序和开发效率,C语言将这些功能封装成库函数,存于C语言的基础库中。常见的有scanf()和printf()函数。
注: 使用库函数,必须提前"打招呼",即是必须包含#include对应的头文件。
学习库函数网站:www.cplusplus.com
C语言常用库函数:

  • IO函数:输入和输出函数,如scanf函数、printf函数。
  • 字符串操作函数:进行字符串处理的函数,字符串拷贝,计算长度,字符查找。
  • 字符操作函数:对个别字符进行处理的函数。
  • 内存操作函数:内存复制、查找等操作的函数。
  • 时间/日期函数:处理日期型或日期时间型数据的函数。
  • 数学函数:数值计算的函数,如sqrt函数。
  • 其他库函数
    学习库函数网站:
    http://www.cplusplus.com/
    https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5

以学习strcpy库函数为例:
从上面库函数网站查找strcpy函数
在这里插入图片描述

strcpy(arr1,arr2);//将arr2中的字符串复制到arr1中

实例:

#include<stdio.h>
#include<string.h>
int main()
{
	char str1[20] = { 0 };
	char str2[] = "hello world!";
	char* str3;
	strcpy(str1, str2);
	str3 = strcpy(str1, str2);
	printf("%s\n", str1);
	printf("%s\n", str3);
	return 0;
}

str1可将内容拷贝到str2,包括’\0’字符。
strcpy返回值是char*(地址),所以用char*来定义变量str3。

2.自定义函数

自定义函数像库函数一样,有函数名,返回类型,函数参数,由我们自己自由发挥。
函数的组成:

ret_type fun_name(para1, * )
{
 statement;//语句项
 return 0;//返回值,返回值和函数返回类型必须相同,若是void型函数,可不写 
}
ret_type 返回类型
fun_name 函数名
para1    函数参数

咱举个栗子:

int get_max(int x, int y)
{
	return (x > y ? x : y);
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int max = get_max(a, b);
	printf("max=%d", max);
	return 0;
}

调用get_max()函数时,会给get_max()函数传入a,b两个实参,调用完后会返回一个值(a与b的最大值)给get_max(a,b)函数,后存于max整型变量中。

3.实际参数(实参)

真实传给函数的参数,叫实参。
实参可以是︰常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
上述例子中的a,b。

4.形式参数(形参)

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元) ,所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
上述例子中的x,y。

二、函数调用

函数调用有两种方式:传值调用和传址调用。

1.传值调用

传值调用,函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

写一个函数来实现对两个数的交换:

void Swap1(int x, int y)
{
	int temp = 0;
	temp = x;
	x = y;
	y = temp;
}
int main()
{
	int num1 = 1;
	int num2 = 2;
	printf("交换前::num1 = %d num2 = %d\n", num1, num2);
	Swap1(num1, num2);
	printf(" Swap1::num1 = %d num2 = %d\n", num1, num2);
	return 0;
}

在这里插入图片描述
运行结果发现num1和num2并没有交换。
在这里插入图片描述
从调试中的监视中可以看到,x,y虽然发生交换,但num1和num2没有交换,因为x,y地址分别与num1,num2地址不相同,两者是相对独立的空间,所以x,y 的交换并不影响num1,num2,且Swap函数调用结束后x,y便会被销毁。

2.传址调用

①传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
②这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操
作函数外部的变量。

还是写交换两个数的函数。

#include<stdio.h>
void Swap2(int* px, int* py)
{
	int temp = 0;
	temp = *px;
	*px = *py;
	*py = temp;
}
int main()
{
	int num1 = 1;
	int num2 = 2;
	printf("交换前::num1 = %d num2 = %d\n", num1, num2);
	Swap2(&num1, &num2);
	printf(" Swap2::num1 = %d num2 = %d\n", num1, num2);
	return 0;
}

在这里插入图片描述
从调试中的监视可以看到,px,px是指针变量,其中存放的是num1,num2的地址(即指针),*为解引用操作符,作用是通过地址找到地址中存放的变量,如px是num1的指针变量,它的值就是num1的地址(指针),*px—通过num1的地址找到地址中存放的变量num1,最后可以对它赋值。
传址调用使变量num1和num2达到交换的目的。

三、函数的嵌套调用和链式访问

1.嵌套调用

函数和函数之间可以有机的组合的。

#include<stdio.h>
void new_line()
{
	printf("hello world!\n");
}
void three_line()
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		new_line();
	}
}
int main()
{
	three_line();
	return 0;
}

在这里插入图片描述
主函数main里调用three_line函数,three_line函数中调用new_line函数,这种"套娃"便是嵌套调用。

2.链式访问

把一个函数的返回值作为另外一个函数的参数。

求一个字符串的长度:
在这里插入图片描述
利用链式访问的方式该这样写:
在这里插入图片描述
strlen函数的返回值作为printf函数的参数。

进一步了解链式访问:

#include<stdio.h>
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));
	return 0;
}

在这里插入图片描述
这个程序实际上就是将printf函数的返回值作为printf函数的参数。
cplusplus.com查找printf函数发现
在这里插入图片描述
printf函数的返回值就是字符总数。

程序分析:最内层printf函数打印43,第二层printf函数打印的是最内层printf函数的返回值("43"的字符个数2个),最外层printf函数打印的是第二层printf函数的返回值(“2"的字符个数1个),最后显示"4321”。

四、函数的声明和定义

1.函数声明:

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
  3. 函数的声明一般要放在头文件中的。

2.函数定义:

函数的定义是指函数的具体实现,交待函数的功能实现。

两者区别:声明就像个预告片,告诉编译器有这个函数了,但是徒有其表;定义则是正片,说明这个函数的具体操作内容,给函数注入灵魂。

#include<stdio.h>
int Add(int x, int y);//函数声明
int main()
{
	int a = 0;
	int b = 0;
	int sum = 0;
	scanf("%d %d", &a, &b);//函数调用
	sum = Add(a, b);
	printf("sum = %d\n", sum);
	return 0;
}
int Add(int x, int y)//函数定义
{
	return x + y;
}

若去掉函数声明部分:
在这里插入图片描述
没有了函数声明,编译器读到函数调用并不知道有该函数,所以编译器会报错。

为了解决遗漏或去掉函数声明部分,我们可以将函数定义部分放在函数调用前面。因为编译器读取代码是从上到下的顺序。
在这里插入图片描述
函数声明放在什么位置?
一般来说,函数声明放在头文件中,因为程序的编译过程是先将头文件的内容加载进来。

而函数定义写在.c文件中

五、函数递归

1.递归是什么?

递归:一个过程或函数在其定义或说明中有直接或间接调用自身。
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序即可。描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
主要思考方式:大事化小

递归的两个必要条件

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。

例如:用主函数调用主函数

#include<stdio.h>
int main()
{
	printf("hello world!\n");
	main();
	return 0;
}

在这里插入图片描述
运行结果:一直打印"hello world!"直到栈溢出。
什么是栈溢出?
首先,函数在调用时会向内存申请空间。而编程内存分为三大部分:栈区、堆区、静态存储区。
如图:
在这里插入图片描述
每一次的函数调用都需要在栈区分配一定的空间,随着调用的次数变多,栈区的空间变少,最后栈区空间被耗尽,便是栈溢出。

练习1:(画图讲解)

接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:123,输出 1 2 3.

参考代码:

#include<stdio.h>
void print(int n)
{
	if (n / 10)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	scanf("%d", &num);
	print(num);
	return 0;
}

程序分析:
123取模可得到3,123除以10可得到12,如此模10除以10可得到每一位数字。
print(123)—>print(12) 3—>print(1) 2 3—>1 2 3.
在这里插入图片描述
注:1.要有限制条件且每次递归逼近限制条件。
2.递归次数不宜过多,避免栈溢出。
练习2:(画图讲解)

编写函数不允许创建临时变量,求字符串的长度。

若允许创建临时变量:

#include<stdio.h>
int my_strlen(char *s)
{
	int count = 0;
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}
int main()
{
	char arr[20] = "hello world";
	int len = my_strlen(arr);
	//传过去的是数组首元素的地址
	printf("%d", len);
	return 0;
}

不创建临时变量,用递归解决:

#include<stdio.h>
int my_strlen(char *s)
{
	if (*s != '\0')
		return 1 + my_strlen(s + 1);
	else
		return 0;
}
int main()
{
	char arr[20] = "xin";
	int len = my_strlen(arr);
	printf("%d", len);
	return 0;
}

程序分析:
若第一个字符不是’\0’,就让指针变量+1跳到下一个字符,直到找到’\0’并return 0

字符指针变量+1,向后跳1个字节
整型指针变量+1,向后跳4个字节
不同指针变量类型+1,向后跳的字节也不同,但都是往后跳一个元素

在这里插入图片描述

2.函数迭代

迭代:重复执行一系列运算步骤(循环),从前面的量依次求出后面的量的过程。此过程的每一次结果,都是由前一次结果施行相同的运算步骤得到。

练习3:(画图讲解)

求n的阶乘。(不考虑溢出)

方法一:函数迭代

#include<stdio.h>
int Fac(int n)
{
	int i = 0;
	int y = 1;
	for (i = 1; i <= n; i++)
	{
		y = y * i;//变量y既参与运算又同时保存结果,当前保存的结果为下一次循环计算的初始值
	}
	return y;
}
int main()
{
	int n = 0;
	int ret = 0;
	printf("请输入一个整数:>\n");
	scanf("%d", &n);
	ret = Fac(n);
	printf("%d的阶乘是%d", n, ret);
	return 0;
}

方法二:函数递归
在这里插入图片描述

#include<stdio.h>
int Fac(int n)
{
	if (n > 1)
	{
		return n * Fac(n - 1);
	}
	else
		return 1;
}
int main()
{
	int n = 0;
	int ret = 0;
	printf("请输入一个整数:>\n");
	scanf("%d", &n);
	ret = Fac(n);
	printf("%d的阶乘是%d", n, ret);
	return 0;
}

程序分析:在这里插入图片描述

练习4:

求第n个斐波那契数。(不考虑溢出)

斐波那契数:从第三个数起,每个数等于前两个数之和。
在这里插入图片描述
在这里插入图片描述

函数迭代:

#include<stdio.h>
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
int main()
{
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = Fib(n);
	printf("%d", ret);
	return 0;
}

上一个a变成b,上一个b变成c,如此循环。

函数递归:

#include<stdio.h>
int Fib(int n)
{
	if (n > 2)
	{
		return Fib(n - 1) + Fib(n - 2);
	}
	else
		return 1;
}
int main()
{
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = Fib(n);
	printf("%d", ret);
	return 0;
}

但我们会发现问题:
当n特别大时,程序效率极低甚至崩溃。因为调用Fib函数时有很多计算是重复的。
如图,例如n=50
在这里插入图片描述
可以看到47计算了3次,随着递归,数字被拆分得更小,重复的计算更多。
我们可以添加计量count变量来测试:
在这里插入图片描述
当计算到Fib(2),就count++。可见Fib(2)总共被计算514229次!效率显然的低。
而且,Fib()调用的足够多时,栈区无法再分配空间,将导致栈溢出(前面提过)。
所以,相比之下迭代的方式效率更高,计算一个数只需n-2次,不会重复多余的计算。

3.函数递归和迭代的区别与优势

1.有些问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2.有些问题的迭代实现往往比递归实现效率更高,但是代码的可读性稍微差些。
3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

函数递归的几个经典题目(自主研究):

  1. 汉诺塔问题
  2. 青蛙跳台阶问题

六、函数的分文件编写

当写一个实现加减乘除计算的程序,相信大家都会写在一个.c文件中,写好main函数再编写加减乘除各个函数。简单的代码当然可以这样,但当各个函数的代码量很大时,或需要多人合作完成时,在同一个.c文件中编写则会效率大大下降,这时就需要进行函数的分文件编写
分文件编程的好处:

  • 分模块的编程思想
  • 主函数代码简洁明了
  • 各模块方便调试
  • 功能责任划分清楚,如写一个游戏,界面给A,时间延迟给B,图像大小给C…

注:

1.一般地,函数声明和函数定义分开写,函数声明放在头文件(.h)中,函数定义放在源文件(.c)中,防止出现编译错误
2.调用函数需要引头文件,即include"头文件名.h"

假设加减乘除各个函数是复杂的函数,需要分文件编写。如下:
分别写好各个功能的源文件和头文件,最后在main函数里引各自的头文件即可。
Add.h
在这里插入图片描述
Add.c
在这里插入图片描述
主函数
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大猩猩!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值