- 函数是什么
- 库函数
- 自定义函数
- 函数的参数
- 函数的调用
- 函数的嵌套调用和链式访问
- 函数的声明和定义
- 函数递归
函数是什么?
函数又叫子程序,他能完成某项特定的任务,当我们在写程序需要实现某些特定的任务时候,就可以调用这个子程序,子程序相较于其他代码,有相对独立性,所以单独成了子程序,可以有返回值,输入参数,函数体,这些代码通常被集成为软件库。
C语言中函数的分类
- 库函数
- 自定义函数
库函数很好理解的,库里已经有的函数,c语言是经过了几代人的打磨,对于我们常用的子程序为了支持可移植和提高程序效率,他们都已经集成在库里了,当我们需要使用的时候只需要添加头文件,就可以使用了,比如我们常见的屏幕打印函数,printf,它集成在我们已经很熟悉的头文件<stdio.h>中,类似的还有很多,诸如字符串拷贝(strcpy),如果要举例子的话,会很多很多,我可以提供一个网站程序员专属网站](http://www.cplusplus.com),可以在这上面进行库函数的学习。
我们常用到的库函数包括: - IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
当我们需要使用相应的函数的时候,我们可以利用这个网站,学习库函数,我用最简单的拷贝函数举个例子:
我们输入strcpy,可以很容易看到这个函数的参数类型,返回值类型,以及对参数类型做出的解释,和要求,第一个参数是目的地,第二个参数是我们源,而且,为了保证溢出问题,目的地数组的长度必须大于等于源地数组长度,当我们看懂这个函数,再调用起来就会很容易,且不会出错,可能对我们英文要求稍微有那么一点点,不过现在翻译app很多,这个小问题应该难不倒我们程序员吧。如果我们实在困得的话 我这里还提供一个国际最流行的男性交友网站
英文版
中文版
自定义函数
当然,库函数肯定不是万能的,所以才会有我们程序员的生存空间,相比库函数,对我们来说更重要的是自定义函数,自定义函数的类型,跟库函数的类型是一样的,返回值,函数名,参数,函数体,单不同的是,这些都需要我们自己设计,给我最大化发挥空间,函数组成:
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
我们可以举一个简单的例子来实现两个整数中的最大值:
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
return (x>y)?(x):(y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
函数的参数
实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参
形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单
元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有
效。
这些我在初识c语言的函数章节已经介绍过,我们接下来用个经典的例子,把形参,实参,以及传值调用传址调用讲明白,写一个函数,实现两个整数的交换:
//*************整数交换*******************8
#include <stdio.h>
void swap1(int a,int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
int main()
{
int a = 0;
int b = 0;
printf("请输入两个整数->");
scanf("%d %d",&a,&b);
printf("交换前:%d %d\n",a,b);
//交换两个整数的函数
swap1(a,b);
printf("交换后:%d %d\n", a,b);
return 0;
}
为什么会产生这样的结果呢?我们先调出监控窗口看看 变量a b 前后变化,
通过对代码的调试,我们发现,第二张图当我们调用 交换函数 swap1时,a b的值确实交换了,但是在第三张图,对交换后的a b打印时候结果竟然没有变,这是为什么呢?我们不妨再把各自的地址打印出来看一下
通过地址打印我们可以很清晰的看到,在调用swap1函数时候,系统为x,y变量在内存上重新开辟了空间,虽然把a,b的值赋值给了x,y,而且在子程序内部交换了x,y的值,但是只是把a,b的值做了一份拷贝,重新开辟的内存中放入这两个拷贝值,本质上并没有改变a,b变量内部的情况,那我们如果想改变a,b的值应该怎么办呢?我们之前都学过指针,对指针进行解引用的话可以读取指针指向地址内部的值也可以进行修改 正确的方法如下:
//*************整数交换*******************8
#include <stdio.h>
void swap1(int *x,int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 0;
int b = 0;
printf("请输入两个整数->");
scanf("%d %d",&a,&b);
printf("交换前:%d %d\n",a,b);
//交换两个整数的函数
swap1(&a,&b);
printf("交换后:%d %d\n", a,b);
return 0;
}
程序中,将a,b传给swap1,是真实传给函数的,有确定的值所以叫实参,而调用swap1函数时候,括号内部的x y才被开辟内存空间,所以x,y叫形参,只有被调用的时候才实例化,而且在实例化后,它的值只是实参的一份临时拷贝。为什么在第一次我写函数的时候,也用a b这样的变量呢,其实就是想说明,形式参数的作用域只会在子程序内部,调用被创建,结束就销毁。
函数的调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
上面我举的例子1就是传值调用,我们改变了形参x,y的值但是实参a,b的值并没有改变。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。
上面我们举的例子2就是传址调用,我们把实参x,y的地址传给子程序,并用指针变量做一份拷贝,让外部内部建立起联系,操作内部的变量,也就能够改变外部的变量。
练习
我们可以通过几个例子来巩固一下
1、写一个函数可以判断一个年份是不是闰年
int is_leap_year(int year)
{
if(((0 == year%4)&&(0!=year%100))||(0==year%400))
{
return 1;
}
else
{
return 0;
}
}
2、写一个函数可以判断一个数是不是素数
int is_prime(int n)
{
int i = 0;
for(i=2; i<=sqrt(n); i++)
{
if(0 == n%i)
{
return 0;
}
}
return 1;
}
函数的嵌套调用和链式访问
主程序可以调用子程序,但是函数和函数之间也可以根据实际的需求进行组合的,也就是互相调用的。
嵌套调用
举个例子就能很明白了:
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
链式访问
把一个函数的返回值作为另外一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));//这里介绍一下strlen函数
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//结果是啥?
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
函数的声明和定义
我们先把声明和定义的概念和使用情况交代一下
函数声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。 - 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
函数定义
函数的定义是指函数的具体实现,交待函数的功能实现。
我们现在学习初级阶段,可能声明放在主函数的前面,或者直接用函数的定义直接代替函数的声明,但是在以后的学习,或工作中,我们会在项目中负责不同的代码块,我们不可能都把声明和定义写在主函数前面,这样就会乱套了,所以我们负责什么功能的代码块,就要把这个子程序的定义写在源文件中,函数声明写在头文件中:
test.h的内容
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
test.c的内容
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}
函数的递归
什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的
一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解的递归策略
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
递归的两个必要条件
1、存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2、每次递归调用之后越来越接近这个限制条件。
可以通过具体的例子来研究递归,会理解的更深刻
例1:把一个无符号的整形值,按照顺序把它的每一位打印显示出来
我们可以先分析:
代码演示:
#include <stdio.h>
void print(int n)
{
if(n>9)
{
print(n/10);
}
printf("%d ", n%10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
递归一定要有,一层一层的递推,还要有一级一级的回归。我们再举个例子
2、在不考虑溢出的情况,我们求n的阶乘
int factorial(int n)
{
if(n <= 1)
return 1;
else
return n * factorial(n-1);
}
3、求字符串个数
#include<stdio.h>
int my_strlen(char* pa)
{
while (*pa != '\0')
{
return 1 + my_strlen(pa + 1);
}
return 0;
}
int main()
{
char arr[] = "god";
printf("字符串个数为%d\n", my_strlen(arr) );
return 0;
}
递归这么好用的话,是不是函数我们都要用递归呢,我们用接下来用斐波那契这个例子来看看:
int count = 0;//全局变量
int fib(int n)
{
if(n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
我们发现用递归的方法实现斐波那契数列,提别耗时间,我们都知道我们在调用子程序,形参实体化就要在栈空间上开辟空间,递归在没完成之前,就会一直开辟,这样会造成栈溢出的问题。
如何解决上述问题呢,我们可以把递归改成非递归例如:
//求n的阶乘
int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n ;
n -= 1;
}
return result;
}
//求第n个斐波那契数
int fib(int n)
{
int result;
int pre_result;
int next_older_result;
result = pre_result = 1;
while (n > 2)
{
n -= 1;
next_older_result = pre_result;
pre_result = result;
result = pre_result + next_older_result;
}
return result;
}
提示
- 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
- 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
- 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开
销。