1.函数是什么
在数学中我们学过各种函数,一元,二元,三元等等,例如:y = x+1。
但是C语言中的函数是什么样的呢?在维基百科中的定义为子程序。
- 在计算机科学中,子程序,是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务的独立程序代码单元。
- 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
为什么要使用函数: 首先,使用函数可以省去编写重复代码的苦差。如果程序要多次完成某项任务,那么只需要编写一个合适的函数,就可以在需要时使用这个函数,或者在不同程序中使用该函数,就像许多程序中使用putchar()一样。
其次,即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高程序代码的可读性,更方便后期修改、完善。
2.库函数
什么是库函数: 库函数(Library function)是将函数封装入库,供用户使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include<>加到里面就可以了.
例如:#include <stdio.h>
为什么会有库函数呢?
就比如说,我们在日常写代码的过程中,想把一些东西打印到屏幕上我们就需要用到printf函数,或者我们需要求一下字符串长度,我们可能就要用到strlen函数等等。所以说像这一类功能的函数,我们可能在平常过程中频繁大量的使用。假如C语言函数库中没有这些函数,那我们程序猿需要在屏幕上打印或者求字符串长度时,这些底层的代码都得自己去实现了。这样就造成时间浪费,写出来的代码各不相同但又逻辑相似造成代码冗余。所以就有了库函数,在需要使用这些简单函数时,直接调用。这样就使得开发效率得到提升同时代码更标准化。
所以为了支持可移植性和提高程序的效率,C语言的基础库中提供了一系列类似的库函数。
C语言常用的库函数
- IO函数(输入输出相关函数)
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
那么如何学习库函数呢?
我们要学会查询工具的使用:
www.cplusplus.com
MSDN(Microsoft Ddveloper Network)
http://en.cppreference.com
第三个学习网站如果想看中文的将前面的en换成zh即可。
接下来简单说两个库函数
1.strcpy
利用www.cplusplus.com这个网站我们可以查阅到库函数的说明与讲解,这里需要大家略懂一些英文,在功能介绍栏 Copy string,意思是拷贝字符串,这个函数里面需要两个参数,第一个是目标空间,第二个是源头,意思就是将源头的字符串拷贝给目标空间。看下面代码以及输出:
//strcpy--字符串拷贝复制函数('\0'也会拷贝过去)
#include<string.h>//头文件
int main()
{
char arr1[20] = "xxxxxxxxx";//目标空间
char arr2[] = "hello";//源头
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
2.memset
对这个函数的解释是将缓冲区设置为指定的字符,将某一块内存中的内容全部设置为指定的值。这个函数有三个参数,void *memset(void *s, int ch, size_t n),它的功能是将s中从当前位置后面的n个字节用 ch替换并返回 s 。看下面代码和输出:
//memset - 内存设置
//void* memset ( void * ptr, int value, size_t num );
//将ptr中从当前位置后面的num个字节用value替换并返回ptr
// value--- 替换的字符(不能是字符串) size_t num---改多少个字节
#include<string.h>//头文件
int main()
{
char arr[] = "hello bit";//xxxxx bit
char *ret = memset(arr, 'x', 5);
printf("%s\n", ret);
return 0;
}
3.自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数。不一样的是这些都是我们自己来设计的,这就有很大的发挥空间。
函数的组成:
ret_type fun_name(参数类型 参数名,....)
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
接下来我们通过例题来熟悉自定义函数
//例1:写一个函数找出两个数的较大值
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);//输入两个数
//求2个数的较大值
int max = get_max(a, b);//函数调用
printf("max = %d\n", max);
return 0;
}
下面一题我们就需要注意了,看下面代码和输出:
//例2:写一个函数可以交换两个整型变量的内容
void Swap(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
//函数
Swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
为什么两个值没有进行交换呢?我们进行一下调试可以发现,a与x的地址,b与y的地址都不相同,所以x,y和a,b在空间上是独立的,这里只是将x和y的值交换了,而没有操作a和b的权利。
所以我们得出的结论是:实参a和b,传给形参x,y的时候,形参将是实参的一份临时拷贝,改变形参变量x,y,是不会影响实参a和b。
那么怎么修改代码才能完成交换呢?如下,我们可以将a,b的地址传过去,利用指针变量接收,这样就可以间接对a,b进行操作。
void Swap2(int* px, int* py)
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b); //传址调用
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们来通过调试看看
我们可以看到a与px的地址是相同的,b与py的地址是相同的,所以这样在函数中操作px与py就相当于操作a和b,这样我们就可以进行两数的交换了。
4.函数的参数
函数的参数分为实际参数和形式参数
实际参数(实参)
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,他们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了,因此形式参数只在函数中有效。
下面这张图更直观一点
从上面两道例题我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
5.函数的调用
通过上方第二个例题我们可以知道
函数的调用也分为两种:传值调用和传址调用。
传值调用
函数的形参和实参分别占有不同的内存块(两者内存地址不同),对形参的修改不会影响实参。
下图是传值调用,上方例2的错误做法。
传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
下图是传址调用,例2的正确做法
6.函数的嵌套调用和链式访问
嵌套调用
函数与函数之间是允许嵌套调用的,但不允许嵌套定义。
例如下方代码:
int main()
{
//嵌套定义是不支持的 - 错误
void test()
{
printf("hehe\n");
}
return 0;
}
这种写法是错误的,因为函数与函数之间都是平等的,不允许在一个函数内部定义另一个函数。(main函数虽是主函数,但本质也是函数。)
嵌套调用举例:
void test3()
{
printf("我是小帅哥\n");
}
void test2()
{
test3();//调用test3
}
int main()
{
test2();
return 0;
}
链式访问
把一个函数的返回值作为另外一个函数的参数。
例如:
#include<stdio.h>
#include<string.h>
int main()
{
int len = strlen("abc");
printf("len = %d\n",len);//平常写法
//链式访问
printf("%d\n",strlen("abc"));
char arr1[20] = "xxxxxx";
char arr2[20] = "abc";
//链式访问
printf("%s\n", strcpy(arr1, arr2));
return 0;
}
运行结果如图:
请看下方代码:
int main()
{
printf("%d",printf("%d",printf("%d",43)));
}
运行结果是什么呢?是不是认为打印43?
其实答案是4321。
其实printf函数是有返回值的:
printf函数返回的值是字符个数,我们解释一下代码,第一个printf打印的是printf("%d",printf("%d",43))的返回值,第二个printf打印的是printf("%d",43)的返回值,第三个printf打印的是43,首先打印43,43是两个字符个数,所以第二个printf打印的是2,2是一个字符,所以第一个printf打印的是1。
运行结果如图:
7.函数的声明和定义
函数声明:
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但具体是不是存在,无关紧要。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
函数定义:
函数的定义是指函数的具体实现,交代函数的功能实现。
函数的声明与定义一般有两种方式,看下方代码我们来进行了解:
//第一种写法:
#include <stdio.h>
int Add(int x,int y) //函数定义
{
int z = x+y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b); //函数调用
printf("%d\n", ret);
return 0;
}
//第二种写法:
#include <stdio.h>
int Add(int x, int y);//函数声明
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b); //函数调用
printf("%d\n", ret);
return 0;
}
int Add(int x,int y) //函数定义
{
int z = x+y;
return z;
}
从代码我们可以看出,两者区别就是,一个有函数声明,一个没有函数声明。而这两种方式,我们一般采用第一种方式的写法,把函数定义放在主函数前面,这样可以省略函数声明部分。第二种写法一般是在教科书上的写,把函数定义放在主函数后面,这样就需要进行函数的声明,因为编译器是从上往下进行扫描的,若没有函数声明,当我们扫到Add函数时,编译器就会报出警告,说Add函数未定义,因为上方没有扫描到有关Add函数的相关信息,所以这时候就需要函数的声明。
8.函数递归
什么是递归:
C语言中,函数自己调用它自己,这种调用过程称为递归。递归的主要思想在于:把大事化小
可以使用循环的地方通常可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁,但效率却没有循环高。
递归的两个必要条件:
- 存在限制条件,当满足这个限制条件的时候,递归便不在继续。
- 每次递归调用之后越来越接近这个限制条件。
演示递归:
我们通过一个程序示例,来学习什么是递归。看下方代码:
//接受一个整型值(无符号),按照顺序打印它的每一位。例如:输入1234,输出1 2 3 4
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
print(num);
return 0;
}
在main函数中调用print()函数,这次调用称为“第1级递归”。然后print()调用自己,这次调用称为“第2级递归”。接着第2级递归调用第3级递归,以此类推。
假设我们输入:1234
则递归的形式如下:
print(1234)
print(1234)可以拆分为:print(123) 4
print(123)可以拆分为:print(12) 3 4
print(12) 可以拆分为:print(1) 2 3 4
那我们如何得到每位的数字的?我们可以这样:
1234%10 = 4
1234/10=123 123%10 = 3
123/10=12 12%10 = 2
12/10=1 1%10 = 1
这样我们就可以得到我们每一位的数字啦。
当我们知道这些后,为了进一步深入研究递归时发生了什么,我们用图画进行讲解。请看下图:
我们把每次递归函数拿出来一一分析,就可以看明白:
递归的基本原理:
第1,每级函数调用都有自己的变量。也就是说,第1级的n和第2级的n不同,所以程序创建了4个单独的变量,每个变量名都是n,但是它们的值各不相同。当程序最终返回print()的第1级调用时,最初的n仍然是它的初值1234。
第2,每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。程序必须按照逐级返回递归,从某级print()返回上一级的print()。
第3,递归函数中位于递归调用之前的语句,均按被调函数的顺序执行。
第4,递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行。就比如上方代码中的 printf("%d ",n%10); 语句,它的执行顺序是第4级、第3级、第2级、第1级。
第5,虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。程序按照顺序执行函数中的代码,而递归函数就相当于又从头开始执行函数的代码。除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句。实际上,递归有时可用循环来代替,循环有时也能用递归来代替。
第6,递归函数必须包含能让递归调用停止的语句。通常递归函数都使用if或者其他等价的判断条件,在函数形参满足这个限制条件的时候,递归便不在继续。为此,每次递归调用的形参都要使用不同的值,来接近这个限制条件。例如上方程序中的print(n/10)。
好了,通过上方的讲解,我们大概可以了解函数递归的概念了,接下来我们来两道例题熟悉一下递归的使用吧!
例1.编写函数不允许创建临时变量,求字符串的长度。
//1.编写函数不允许创建临时变量,求字符串的长度。
#include <stdio.h>
int my_strlen(char* s)
{
//方法一:循环
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
//方法二:递归
if (*s != '\0')
{
return 1 + my_strlen(s + 1);
}
else
return 0;
}
int main()
{
//求字符串长度
char arr[10] = "abcdef";
//数组名arr是数组首元素的地址---char*
int len = my_strlen(arr);//6
printf("%d\n", len);
return 0;
}
虽然我们还没讲指针,但我们先来初步了解一下
字符指针+1:向后跳1个字节
char* p;
p+1 -->向后跳一个字节
这上面的两种方法都可以求出字符串的长度,这里为了能比较循环和递归的区别,就全部写出来啦,注意别看错了哦。下面的题也是如此,我会把循环的方法和递归的方法都进行编写。
例2.求n的阶乘(不考虑溢出)
在做这题之前我们了解一下求n的阶乘的递归公式
//2.求n的阶乘(不考虑溢出)
int Fac1(int n)//方法一:循环(迭代)
{
int i = 0;
int ret1 = 1;
for (i = 1; i <= n; i++)
{
ret1 *= i;
}
return ret1;
}
int Fac(int n)//方法二:递归
{
if (n <= 1)
return 1;
else
return n * Fac(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
//求n的阶乘
int ret1 = Fac1(n);
int ret = Fac(n);
printf("ret1 = %d\n", ret1);
printf("ret = %d\n", ret);
return 0;
}
例3.求第n个斐波那契数(不考虑溢出)
什么是斐波那契数列?
1 1 2 3 5 8 13 21 34 55…
从第三个数开始,每个数等于它前面两个数之和。
递归公式
//3.求第n个斐波那契数
int Fib1(int n)//方法一:递归(不建议)
{
if (n <= 2)
return 1;
else
return Fib1(n - 1) + Fib1(n - 2);
}
int Fib(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;
scanf("%d", &n);
int ret1 = Fib1(n);//方法一:递归
int ret = Fib(n);//方法二:循环
printf("ret1 = %d\n", ret1);
printf("ret = %d\n", ret);
return 0;
}
当我们去用递归的方法来做这道题时,我们运行代码输入一个比较大的值时,代码运行需要花费大量时间,才能得出结果。应为该函数使用了双递归,即函数每一级递归都要调用本身两次。这就暴露了一个问题,因为函数在递归过程中,会发现,变量的数量呈指数增长,有很多值是重复大量的计算!这样就会很快就消耗计算机的大量内存,很可能导致程序崩溃。
所以本例说明:在使用递归要特别注意,尤其是效率优先的程序。
也可以看出,循环的效率比递归的效率要高。