C语言初阶之函数
前言
本文主要介绍C语言中函数的基本使用,希望大家早日攀上高峰。
一、函数
1.函数是什么?
C语言中函数有很多叫法:方法、子例程、程序......一个C程序至少有一个函数,即主函数 main(),所有简单的程序都可以定义其他额外的函数。
-
函数就是一个大型程序的某部分代码,由一个或多个语句块组成,它负责完成某项特定任务,相比较于其他的的代码,具有相对的独立性。
-
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏,这些代码通常被集成为软件库。
2.C语言中函数的分类
- 库函数
- 自定义函数
库函数
问:为什么会有库函数?
答:C语言早期是没有库函数的,但是这样开发效率比较低,在慢慢发展的过程中,既然有一些功能是经常被频繁大量的使用到的,那不妨在C语言里提供一下,官方话来说就是为了支持可移植性和提高程序的效率,后来,在C语言里就给出来各种各样的函数,这些函数就被称为库函数。
C语言常用的库函数:
IO函数
字符串操作函数
字符操作函数
时间/日期函数
数学函数
其他库函数
举个栗子:调用库函数strcpy拷贝信息
#include <stdio.h>
#include <stdlib.h>
int main()
{
char arr1[]="bit";
char arr2[]="#####";
//char * strcpy (char* destination,const char * source)
//strcpy 字符串拷贝;destination 地址;source 源头
//当我们拷贝完之后,拷贝的信息是bit\0
//因为\0是字符串结束标志,所以打印字符串的时候,后面的#不会打印出来。
strcpy(arr2,arr1);//arr1拷贝到arr2
printf("%s\n",arr1);
printf("%s\n",arr2);
return 0;
}
库函数是不需要全部记住的,需要的时候查询就可以。
注意:使用库函数时,必须包含#include对应的头文件
自定义函数
比起库函数,更加重要的是自定义函数,自定义函数和库函数一样,有函数名,返回值类型和函数参数,但是不一样的是,这些都是用我们自己来设计的,这就能给程序员一个很大的发挥空间。
返回类型+函数名(函数参数, * )
{
语句项;
}
例:两个数字取最大值
#include <stdio.h>
#include <stdlib.h>
//定义函数getmax(函数定义要放在主函数之前)
int getmax(int x,int y)
{
if(x>y)
return x;
else
return y;
}
int main()
{
//函数体交代的是函数的实现
int a=10;
int b=20;
int max = getmax(a,b); //函数getmax的使用
printf("max= %d\n",max);
return 0;
}
在这个过程里, 主函数的a传递给你自定义函数的x,b传递给y,然后通过你自定义的函数获得结果,主函数里将获取的结果给了max.
二、函数的参数
1.实际参数(实参)
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
2.形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完之后就自动销毁了,因此形式参数只在函数中有效。
当实参传给形参的时候,形参其实是实参的一份临时拷贝,对形参的修改是不会影响实参的。
三、函数的调用
1.传值调用
函数的实参和形参分别占有不同的内存块,对形参的修改不会影响实参。
2.传址调用
1)传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
2)这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
3.函数的嵌套调用和链式访问
函数与函数之间可以有机的组合。
嵌套调用
例:
#include <stdio.h>
#include <stdlib.h>
void new_line()
{
printf("QaQ\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 <stdlib.h>
int main()
{
int x = 0;
//x = strlen("blue");
//printf("%d\n",x);
printf("%d\n",strlen("blue"));//链式访问
return 0;
}
例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%d",printf("%d",printf("%d",7)));
//打印的是这个函数的返回值,而printf返回的是打印的字符的个数
//第一次:7 返回值:1 第二次:1 返回值:1 第三次:1 所以打印出来的是711
return 0;
}
四、函数的声明和定义
函数声明
1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在不重要。
2.函数的声明一般出现在使用函数之前,先声明后使用。
3.函数的声明一般要放在头文件中。
函数定义
函数的定义是指函数的具体实现,交代函数的功能实现。
五、函数递归
---递归作为一种算法在程序设计语言中广泛应用,学好递归是必要的。
什么是递归?
程序调用自身的编程技巧就叫做递归(recursion)。递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可秒输出解题过程中所需要的多次重复计算,大大减少了程序的代码量。
递归的主要思想在于 —— 把大事化小
1.递归的两个必要条件
- 存在限制条件,当满足这个限制条件时,递归便不再继续。
- 每次递归调用之后越来越接近这个调用条件。
最简单的递归:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("哈哈");
main();
return 0;
}
大家运行之后会发现,屏幕上会一直打印哈哈,直到最后报错停止,这是为什么呢?递归其实在跑的时候有可能会出现一些问题——栈溢出。
简单说一下:我们这个代码是在main函数中调用了main函数,而我们知道,我们写的任何一次函数调用,都会向内存申请空间,内存会分为几个区域(栈区、堆区、静态区)。
其中,局部变量、函数的形参就是创建在栈区上的,堆区放的是动态开辟的内存,而静态区放的是全局变量、static修饰的变量,这个小伙伴们先简单了解一下就行。
所以在上边这个代码的过程中,main函数自己调用自己,一直递归,每次都向栈区申请内存空间,最后栈空间用尽了,在那一刻就发生了栈溢出。
练习1
接受一个整形值(无符号),按照顺序打印它的每一位。
#include <stdio.h>
#include <stdlib.h>
void print(int n)
{
if (n>9)
{
print(n/10);//越来越接近限制条件
}
printf("%d",n%10);
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
print(num);
return 0;
}
解释一下:我们假设num=123,当num有了值之后,直接传给自定义函数的n,n有了值之后调用print函数,然后判断比>9往下走又遇到了print函数,于是再次调用print函数,第一次的结果12>9于是再次调用print函数,这次的结果是1,1没有大于9,所以if语句就不执行了,就走到了打印这一步,首先1%10,就打印出来了1,因为现在这个函数已经不再递归、自己调用自己了,所以就该往回走了,于是12%10=2,然后123%10=3,就这样,就打印出来了1 2 3
练习2
编写函数不允许创建临时变量,求字符串长度。
#include <stdio.h>
#include <stdlib.h>
int mystrlen(char* str)//str是指针变量
{
if (*str != '\0')
return 1 + mystrlen(str + 1);
else
return 0;
}
/*
把大事化小
mystrlen("Inchon")
1+mystrlen("nchon")
1+1+mystrlen("chon")
...
1+1+1+1+1+1+mystrlen("") = 6
*/
int main()
{
char arr[] = "Inchon";
int len = mystrlen(arr);//数组传参,传过去的是首元素的地址
printf("len = %d", len);
return 0;
}
2.递归与迭代
练习3
求n的阶乘(不考虑溢出)
迭代写法:
#include <stdio.h>
#include <stdlib.h>
int fac(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d", &n);
ret = fac(n);
printf("%d", ret);
return 0;
}
递归写法:
#include <stdio.h>
#include <stdlib.h>
int fac(int n)
{
if (n <= 1)
return 1;
else
return n * fac(n - 1);
}
int main()
{
//求n的阶乘
int n = 0;
int ret = 0;
scanf("%d", &n);
ret = fac(n);//循环的方式
printf("%d", ret);
return 0;
}
练习4
求第n个斐波那契数(不考虑溢出)
*菲波那契数列:1 1 2 3 5 8 13 21 34 55......
规律:前两个数之和等于第三个数
迭代写法:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
scanf("%d",&n);
int a = 1;
int b = 1;
int c = 1;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
printf("%d",c);
return 0;
}
递归写法:
#include <stdio.h>
#include <stdlib.h>
int Fib(int n)
{
if (n <= 2)
return 1;
else
return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n = 0;
int ret = 0;
scanf("%d",&n);
ret = Fib(n);
printf("%d",ret);
return 0;
}
3.提示
补充:
1.许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2.但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性差点。
3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可它所带来的运行时的开销。
那什么时候使用递归?
1.当解决一个问题递归和非递归都可以使用,且没有明显问题。那就可以使用递归。
2.当解决一个问题递归起来非常简答,非递归比较复杂,且递归没有明显问题,那就用递归。
3.如果说用递归解决问题,写起来简单,但是有明显问题,那就不能使用递归。
一些函数递归的经典问题
- 十进制转换
- 前中序遍历确定后序遍历
- 汉诺塔
- 青蛙跳台阶
- 八皇后问题
- 细胞分裂
结尾
学如逆水行舟,不进则退;心似平原野马,易放难收。