目录
(4)写一个函数,每调用一次这个函数,就会将 num 的值增加1
函数是C语言的基本单位,在C语言程序中发挥着极其重要的作用
一、函数的定义与分类
1.定义
在维基百科中,函数的定义叫做子程序。
(1)一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
(2)一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
2.分类
(1)库函数:C语言内部提供的函数。
(2)自定义函数:自我发挥写出的函数。
二、库函数
1.库函数存在的意义
我们在编写C语言代码的时候,总会频繁地使用一些功能:
比如:将信息按照一定的格式打印到屏幕上(printf)、在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)、在编程是我们也计算,总是会计算n的k次方这样的运算(pow)……
像上面的这些基本的功能,在编写程序时经常会用到。所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
2.库函数的学习和使用
库函数的使用不需要专门去记,我们可以通过查找了解它们的使用方式。
这里推荐一个网站和一个应用程序
(2)msdn
通过这些方式,我们可以查找到它们的信息,例如:函数名、形式参数、需要的头文件和返回值等必要的信息。
这些工具的语言都是英文,在学习编程的工程中我们需要学习英文,保证以后在第一时间可以了解计算机的最新技术。
三、自定义函数
1.自定义函数的组成
自定义函数由程序员自主设计,和普通的函数一样有函数名、返回类型、形式参数等。
基本结构如下:
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
2.示例
(1)写一个函数找出两个整数的最大值
#include<stdio.h>
int islarge(int a, int b)
{
if (a>=b)
{
return a;
}
else
{
return b;
}
}
//上述为实现程序的函数
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int c = islarge(a, b);
printf("%d", c);
return 0;
}
//输入:10 20
//输出:20
(2)写一个函数交换两个整型变量的内容
错误示范:
#include<stdio.h>
void swap(int a,int b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(a, b);
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
//输入:10 20
//输出:
//交换前:a=10,b=20
//交换后:a=10,b=20
//
正确程序:
#include<stdio.h>
void swap(int* pa, int* pb)
{
int temp = 0;
temp = *pa;
*pa = *pb;
*pb = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
//输入:10 20
//输出:
//交换前:a=10,b=20
//交换后:a=20,b=10
//
这个程序我先不讲错在哪里,到后面形参的部分再详细解释。
四、函数的参数
1. 实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
在调用函数时,它们都必须有确定的值,以便把这些值传送给形参。
2.形式参数(形参)
形式参数是指函数名后括号中的变量。
形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。因此形式参数只在函数中才有效。
下面是函数在处理数据时的处理思路:
#include<stdio.h>
int islarge(int a, int b)
//int是返回类型,括号里的int a和int b
{
if (a>=b)
{
return a;
}
else
{
return b;
}
}
//上述为实现程序的函数
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);//输入a,b的值
int c = islarge(a, b);//islarge有两个实参a和b,定义变量c接收islarge函数的返回值
printf("%d", c);
return 0;
}
形参实例化之后其实相当于实参的一份临时拷贝。
五、函数的调用
1.传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
所以,我们在不改变函数实参的时候可以使用传值调用。
比如,我们写一个程序计算两个整数的和:
#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 c= add(a,b);
printf("%d\n",c);
return 0;
}
在这个程序中,我们只是使用a和b进行操作,而没有改变a和b的数值等属性,这时我们就可以使用传值调用,再将操作得到的值返回。
2.传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
#include<stdio.h>
void swap(int* pa, int* pb)
{
int temp = 0;
temp = *pa;
*pa = *pb;
*pb = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
在这个程序中,我们改变了a和b的数值,这时我们就需要使用传址调用,因为在传值调用中形参的改变是不会影响实参的。
3.错误讲解
讲到这里,我们讲一讲上面使用传值调用交换数值的程序错在哪里:
#include<stdio.h>
void swap(int a,int b)//返回类型为void表示不返回,此处的int a与int b表示形式参数和它们的类型
{
int temp = 0;//定义一个临时变量
temp = a;//把a的值赋给temp
a = b;//把b的值赋给a
b = temp;//把temp的值赋给b,完成交换操作
//注意,因为形参只是实参的一份临时拷贝,在整个函数中我们改变的只是实参,出函数后形参被销毁无法改变实参
}
int main()
{
int a = 0;//创建变量a
int b = 0;//创建变量b
scanf("%d %d", &a, &b);//输入数值
printf("交换前:a=%d,b=%d\n", a, b);//展示
swap(a, b);//交换函数,将a,b传进去
printf("交换前:a=%d,b=%d\n", a, b);//实参依旧是a和b的原始值,没有达到我们的目的
return 0;
}
打个比方:就好像老师在练习册上留作业,你确实是写了,就是写在了你同学的练习册上。虽然确实做了正确的事,但是做完了也没什么用,你的作业本依旧是空的。(PS:偷把别人作业写了,阻止他学习,内卷的高级境界)
传址调用的程序传递的是实参的地址,这是实参的本质属性。
#include<stdio.h>
void swap(int* pa, int* pb)//返回类型为void表示不返回,此处的int* pa与int* pb表示形式参数和它们的类型
{
int temp = 0;//定义临时变量
temp = *pa;//用地址找到实参a并赋给temp
*pa = *pb;
//把用地址找到的实参b赋给用地址找到的实参a
*pb = temp;//用地址找到实参b并赋给temp
//跳出函数时,被销毁的形参只是两个指针变量,此时实参的交换已经完成
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a, &b);//传入地址
printf("交换前:a=%d,b=%d\n", a, b);
return 0;
}
这次,我们也干同样的事情,比如写作业。但这次我们定位到了你自己的作业本上,就可以实现写作业的任务。
4.练习
(1)写一个函数可以判断一个数是不是素数
函数1:
int isprime(int x)//这个形参用于接收需要判断的数字
{
int i = 2;
for (i=2; i<x; i++)//从2到这个数字减一逐一试除
{
if (x%i == 0)//如果有能除开的就表明它不是素数,返回0
{
return 0;
}
}
return 1;//在除完所有的数字均除不开时,为素数返回1
}
这个程序是可以改进的
比如说,4×4=16,而2×8=16或8×2=16也成立,16×1=16或1×16=16依旧成立。
我们不难看出,被乘数和乘数一定会有一个大于等于这个积开根号,一个小于等于这个积开根号,那么我们只需要试除到根号下x就完全可以判断一个数字的是否为素数。
函数2:
#include<math.h>
int isprime(int x)
{
int i = 2;
for (i=2; i<=sqrt(x); i++)//sqrt表示对参数开平方
{
if (x%i == 0)
{
return 0;
}
}
return 1;
}
(2)写一个函数判断一年是不是闰年
判定条件: 对于整百的年份,闰年必定是400的倍数 ;对于不是整百的闰年,闰年是4的倍数
函数1:
int isleap(int year)
{
if (year % 400 == 0)
{
return 1;
}
if (year%4==0)
{
if (year % 100 != 0)
{
return 1;
}
}
return 0;
}
我们把这两个条件集成一下,得到函数2
函数2:
#include<stdio.h>
isleap(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
//((year是4的倍数)并且(year不是100的倍数))或者(year是400的倍数)
{
return 1;
}
else
{
return 0;
}
}
(3)写一个函数,实现一个整形有序数组的二分查找
int search(int arr[], int a, int sz)//形参为数组、需要查找的整数、数组的元素个数
{
int left = 0;
int right = sz - 1;
int mid = 0;
while (left <= right)
{
mid = left + (right - left) / 2;//找中间的元素
if (arr[mid] > a)//中间元素大于查找值,就从右缩小一半的范围
{
right = mid-1;//可以使用--mid,不推荐
}
else if (arr[mid] < a)//中间元素小于查找值,就从左缩小一半的范围
{
left = mid+1;//可以使用++mid,不推荐
}
else
{
return mid;//找到了,返回下标
}
}
if (left>right) //正常情况下不会出现
{
return -1;//找不到,返回-1
}
}
(4)写一个函数,每调用一次这个函数,就会将 num 的值增加1
#include<stdio.h>
void test(int* p)//在主程序内定义一个变量储存调用的次数,因为需要改变变量的值,所以进行传址调用
{
printf("hehe\n");
(*p)++;//解引用找到变量再加1,注意这个括号不能忘
//否则,*p++就表示每次这个指针先向后移动4个字节,然后解引用
}
六、函数的嵌套调用和链式访问
1.函数的嵌套调用
函数可以根据需要进行相互调用。
#include<stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
这是每一个初学者都会写的代码,我们先调用了main函数,然后在main函数的内部又调用了printf函数,这就是嵌套调用。
2.函数的链式访问
我们为了减少不必要变量的定义,可以直接把一个函数的返回值作为另一个函数的参数。
#include<string.h>
#include<stdio.h>
int main()
{
char arr[20] = "abcdef";
printf("%d", strlen(arr));
return 0;
}
strlen函数的返回值变成了printf函数的参数,这就把这两个函数像锁链一样串联起来,也就是链式访问。
3.链式访问的经典例题
这个程序的输出是什么?
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
答案:4321
printf这个函数的返回值是它打印字符的个数,首先进入最外层的printf函数
这层函数需要第二层函数printf(“%d”, printf(“%d”, 43))的返回值
而第二层的printf函数又需要第三层函数printf(“%d”, 43)的返回值
在执行完第三层的printf(“%d”, 43)函数后,返回打印字符的个数2
printf(“%d”, printf(“%d”, 2))
第二层得到返回值2,打印2,而此时第二层函数也返回它打出的字符的个数1
printf(“%d”, 1)
最后打印1,也就形成了4321的输出结果
七、函数的定义和声明
1.函数的定义
(1)函数的定义是指函数的具体实现,交待函数的功能实现。相当于我们平常创建自定义函数的步骤。
(2)函数不能嵌套定义
错误的定义方法:
int add(int x,int y)//加法函数
{
return x + y;
int sub(int x, int y)//这个减法函数被嵌套定义在了加法函数内部,这种写法是错的
{
return x - y;
}
}
正确的定义方法:
int add(int x,int y)//加法函数
{
return x + y;
}
int sub(int x, int y)//减法函数
{
return x - y;
}
对于函数来讲,数数平等,不能搞特权。
2.函数的声明
函数的声明
- 函数的声明主要的目的在于告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。
- 但是这个函数具体存在不存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
3.程序的分块化编写
我们在写代码的时候可能会想:我把所有的代码写在一个源文件中,这样找起来不就方便了吗。
其实,这样的习惯对日后程序的开发是不利的。
我们的社会是有各自的分工的,当我们在开发一个程序的时候,我们往往只需要负责一个大的工程中的部分内容,比如一个人去写主程序,一个人写函数等等,而我们将工程的各个部分分开就可以更快地快找到bug并对应修复。
这样,当我们写一个函数时,就需要这样的文件分配:
- 函数声明——头文件.h
- 函数定义——函数实现的源文件.c
每一个函数都可以分成这两个文件编写,也可以几个函数写在两个文件中。
4.函数的声明和定义为什么不写在同一个.c文件内
这里涉及到一个代码加密的问题,我会补充。
八、函数递归与迭代
1.函数递归的定义与条件
(1)递归的定义
程序调用自身的编程技巧称为递归( recursion)。表示一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
(2)递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
2.讲解练习
接受一个整型值(无符号),按照顺序打印它的每一位
例如:输入:1234,输出 :1 2 3 4
#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;
}
(1)思想层面:大事化小
我们如果想要得到一个数字的每一位,就需要我们先%10得到最后一位,后/10除去最后一位,因为/10最后一位为余数,可以继续向前查找,直到这个数字成为一个一位数停止程序(因为如果这里是个一位数,a/10的值就是0,我们并不想打印0的每一位),所以在这里我们定义一个函数print(),它可以按顺序打印每一个值。
分步解决就是这样:
print(1234)
print(123) 4
print(12) 3 4
(2)实践讲解
print(1234);//这个函数从上到下,先递进后回归
//1234大于9,进入if语句,第一层
print(1234)
{
if(n>9)//n=1234,满足条件,进入if
{
print(123);
}
printf("%d ", n%10);//第一层,a%10=4
}
//print(123)展开,n=123满足条件,继续进入下一层
print(123)
{
if(n>9)//a/10=123,满足条件,进入if
{
print(12);
}
printf("%d ", n%10);//第二层,a%10=3
}
//print(12)展开,a/10=1此时不满足条件,不会继续进入下一层的if语句
print(12)
{
if(n>9)//n=12,不满足条件,不进入if
{
print(1);
}
printf("%d ", n%10);//第三层,a%10=2
}
print(1)
{
if(n>9)//n=1,不满足条件,不进入if
{
print(0);
}
printf("%d ", n%10);//第三层,a%10=1
}
递归的“递”此时已经完成,我们将这个代码整理一下,查看它时如何“归”的
print(1234)
{
{
{
{
printf("%d ",n%10);//第四层,a%10=1
}
printf("%d ", n%10);//第三层,a%10=2
}
printf("%d ", n%10);//第二层,a%10=3
}
printf("%d ", n%10);//第一层,a%10=4
}
//代码从第四层开始向外执行,故可以实现数字的按位打印
//输出:1 2 3 4
3.函数的递归与迭代
(1)什么是迭代
迭代实际上就是重复,如果只讨论我们比较熟悉的程序设计操作,迭代在程序中就表示循环。
(2)函数递归和迭代的优缺点
函数递归中我们一层一层调用函数,它的优点是所需代码量少,简洁。但缺点主要有两个,一方面,大量重复的计算拖慢了程序的运行速度;另一方面,函数每一次被调用的时候都需要在栈区开辟相应的空间,当递归过深时可能会出现栈溢出。(栈区的空间已经被用完了,程序无法继续进行了)
当我们使用迭代时,循环不需要大量调用函数,重复的计算会少很多,这个程序的运行速度会加快不少,只是这个程序的代码量会大很多。(下面这个程序不是很明显,但也确实更短)
程序:应用递归求斐波那契数列的第n项
斐波那契数列:1 1 2 3 5 8 13 …(规律:第一二项为1,后一项等于前两项的和)
递归程序:
#include<stdio.h>
int fib(int m)
{
int ret = 0;
if (m<=2)
{
ret = 1;//第一二项为1
}
else
{
ret = fib(m - 1) + fib(m - 2);//三项及三项以后,后一项等于前两项的和
}
return ret;
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d",fib(n));
return 0;
}
迭代程序:
#include<stdio.h>
int fib(int m)
{
if (m < 2)//前两项为1
{
return 1;
}
else//后两项为前两项之和
{
int i = 0;
int a = 1;
int b = 1;
int c = 0;
for (i=m; i>2; i--)
{
c = a + b;
a = b;//把原来的第二个数变成新计算中的第一个数
b = c;//把算出的结果变为新计算的第二个数
}
return c;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d",fib(n));
return 0;
}