函数::
函数的概述
1.函数的定义
维基百科中,对函数的定义是子程序。在计算机科学中,子程序是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定任务,而且,相较于其他代码,具备相对的独立性,C语言是由函数组成的,我们写的代码都是由主函数 main()开始执行的。函数是C语言程序段基本模块,是用于完成任务的程序代码单元。
2.函数的分类
函数的分类
从函数定义的角度看,函数可分为系统函数即库函数和用户定义函数两种:
1.系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
2.自定义函数:用以满足用户的专门需要。
为什么会有库函数呢
我们知道在学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf),比如,在编程的过程中,我们会频繁的做一些字符串的拷贝工作(strcpy)或在我们进行编程计算时,总是会计算n的k次方这样的运算(pow)。C语言常用的库函数还有IO函数、字符串操作函数、字符操作函数、内存操作函数、时间日期函数、数学函数等等,但是使用库函数时,必须包含对应的头文件。
3.函数的作用
函数的作用
一方面,函数的使用可以省去重复代码的编写,降低代码重复率。另一方面,函数可以让程序更加模块化,从而有利于程序的阅读修改和完善。假如我们编写一个实现以下功能的程序:读入一行数字;对数字进行排序;找到它们的平均值;打印出一个柱状图。如果我们把这些操作直接写在主函数里,这样可能会给用户感觉代码会有点凌乱。但,假如我们使用函数,这样可以让程序更加清晰、模块化。
函数的使用:产生随机数
当调用函数时,需要关心5要素:
头文件:包含指定的头文件
函数名字:函数名字必须和头文件声明的名字一样
功能:需要知道此函数能干什么后才调用
参数:参数类型需匹配
返回值:根据需要接收返回值
代码语言:javascript复制#include<stdio.h>
int main()
{
float list[50]={0};
//这里只是举例,函数还没有实现
readlist(list,50);
sort(list,50);
average(list,50);
bargraph(list,50);
return 0;
}
代码语言:javascript复制#include <time.h>
time_t time(time_t *t);
功能:获取当前系统时间
参数:常设置为NULL
返回值:当前系统时间, time_t 相当于long类型,单位为毫秒
#include <stdlib.h>
void srand(unsigned int seed);
功能:用来设置rand()产生随机数时的随机种子
参数:如果每次seed相等,rand()产生随机数相等
返回值:无
#include <stdlib.h>
int rand(void);
功能:返回一个随机数值
参数:无
返回值:随机数
代码语言:javascript复制#include<stdio.h>
#include <time.h>
#include <stdlib.h>
int main()
{
time_t tm = time(NULL);//得到系统时间
srand((unsigned int)tm);//随机种子只需要设置一次即可
int r = rand();
printf("r = %dn", r);
return 0;
}
4.自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数,但是不一样的是这些都是我们自己来设计,这给程序员一个很大的发挥空间。
函数的组成
代码语言:javascript复制retype fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
paral 函数参数
代码语言:javascript复制 #include<stdio.h>
//写一个函数找出两个整数中的最大值
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 m = get_max(a, b);
printf("%dn", m);
}
代码语言:javascript复制 #include<stdio.h>
错误的版本
//写一个函数交换两个整型变量的内容
void Swap1(int x,int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前数据a=%d,b=%d", a, b);
Swap1(a, b)
//a和b是实参 x和y是形参 当实参传递给形参的时候 形参是实参的一份临时拷贝 对形参的修改不会影响实参
printf("交换后数据a=%d,b=%d", a, b);
return 0;
正确的版本
void Swap2(int* px, int* py)
{
int z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前数据a=%d,b=%d", a, b);
Swap2(&a, &b);
printf("交换后数据:a=%d,b=%d", a, b);
return 0;
}
上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1 ,num2 和传给 Swap2 函数的 &num1 , &num2 是实际参数。Swap1 函数在调用的时候,x, y 拥有自己的空间,同时拥有了和实参一模一样的内容。因此,形参实例化之后其实相当于实参的一份临时拷贝。选择传值调用还是传址调用 看改变形参的值对调用函数后所求结果是否改变,不变传值,改变传址。
函数的定义
1.函数的定义格式
代码语言:javascript复制返回类型 函数名(形式参数列表)
{
数据定义部分;
执行语句部分;
}
函数定义的一般格式:
2.函数框架
函数名
理论上函数是可以随意起名字,但最好起的名字见名知意,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面有个圆换号(),代表这个为函数,不是普通的变量名。
形参和实参
在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值。在定义函数时,指定的形参必须是类型加变量的形式。另外,在定义函数时指定的形参可有可无,根据函数的需要来设计,如果没有形参,圆括号内容为空,或写一个void关键字。
- 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。
- 实参出现在主调函数中,进入被调函数后,实参也不能使用。
- 实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参。
- 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放。
- 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
void max(int a = 10, int b = 20) // error, 形参不能赋值
{
}
right, 类型 变量
void max(int a, int b)
{
}
error, 只有类型,没有变量
void max(int, int)
{
}
error, 只有变量,没有类型
int a, int b;
void max(a, b)
{
}
没形参, 圆括号内容为空
void max()
{
}
没形参, 圆括号内容为void关键字
void max(void)
{
}
函数体
花括号{}里的内容即为函数体的内容,这里为函数功能的实现过程,这和以前写的代码没太大区别,以前我们把代码写在main()函数里,现在只是把这些写到别的函数里。
返回值
函数的返回值是通过函数中的return语句获得的,return后面的值也可以是一个表达式。但不能一次返回多个类型的数据。
(1尽量保证return语句中表达式的值和函数返回类型是同一类型。
代码语言:javascript复制int max() // 函数的返回值为int类型
{
int a = 10;
return a;// 返回值a为int类型,函数返回类型也是int,匹配
}
(2)如果函数返回类型和return语句中表达式不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据可以自动进行类型转换。
代码语言:javascript复制double max() // 函数的返回值为double类型
{
int a = 10;
return a;// 返回值a为int类型,它会转为double类型再返回
}
注意:如果函数返回的类型和return语句中表达式的值不一致,而它又无法进行类型转换,程序则会报错。
(3)return语句的另一个作用为中断return所在的执行函数,类似于break中断循环,switch语句一样。
代码语言:javascript复制int max()
{
return 1;// 执行到,函数已经被中断,所以下面的return 2无法被执行到
return 2;// 没有执行
}
(4)如果函数带返回值,return语句后面必须跟着一个值,如果函数没有返回值,函数名字的前面必须有一个void关键字,这时候,我们写代码时也可以通过return中断函数(也可以不用),只是这时,return语句后不带内容。
代码语言:javascript复制void max()// 最好要有void关键字
{
return; // 中断函数,这个可有可无
}
3.函数的声明和定义
函数的声明
如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。所谓函数声明,就是在函数尚未定义的情况下,事先将函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译器能正常运行。
注意:一个函数只能被定义一次,但可以声明多次。
代码语言:javascript复制#include <stdio.h>
int max(int x, int y); // 函数的声明,分号不能省略
int max(int, int); // 另一种方式
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函数的调用
printf("num_max = %dn", num_max);
return 0;
}
// 函数的定义
int max(int x, int y); // 函数的声明,分号不能省略
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函数的调用
printf("num_max = %dn", num_max);
return 0;
}
// 函数的定义
int max(int x, int y)
{
return x > y ? x : y;
}
函数的声明和定义的区别:
(1)定义是指对函数功能的确立,包括指定函数名,函数类型,形参及其类型,函数体,它是一个完整的独立的函数单位。
(2)声明的作用则是把函数的名字,函数类型以及形参的个数,类型和顺序(不包括函数体),通知编译系统,以便在对包含函数调用的语句进行编译时,据此进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。
4.函数的嵌套调用和链式访问
嵌套调用
函数和函数之间可以根据实际的需求进行组合,也就是互相调用。但函数虽然可以嵌套调用但不可以嵌套定义。
代码语言:javascript复制#include <stdio.h>
void new_line()
{
printf("hehen");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i )
{
new_line();
}
}
int main()
{
three_line();
return 0; }
链式访问
前提条件:函数必须具有返回值。
核心原理:你的函数的返回值类型作为我的函数的参数。
代码语言:javascript复制#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));strcat的返回值类型是char* strlen的参数也是const char*
printf("%dn", ret);
return 0;
}
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
程序运行结果为4321
函数的调用
1.函数的执行流程
代码语言:javascript复制#include <stdio.h>
void print_test()
{
printf("this is for testn");
}
int main()
{
print_test(); // print_test函数的调用
return 0;
}
(1)进入main函数
(2)调用print_test()函数
a.它会在main()函数之前寻找有没有一个“print_test”的函数定义;
b.如果找到,接着检查函数的参数,这里调用函数时没有传参,函数定义也没有传参,参数类型匹配。
c.开始执行print_test()函数,这时候,main()函数里面的执行会停在print_test()这一行代码,等待print_test()函数的执行。
(3)print_test函数执行完,main()才会继续往下执行,执行到return 0,程序执行完毕。
2.无参和有参函数的调用
无参函数调用
如果是调用无参函数,则不能加上”实参“,但括号不能省略。
代码语言:javascript复制/ 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right, 圆括号()不能省略
test(250); // error, 函数定义时没有参数
return 0;
}
有参函数调用
(a)如果实参列表含多个实参,则各参数之间用逗号隔开。
代码语言:javascript复制// 函数的定义
void test(int a, int b)
{
}
int main()
{
int p = 10, q = 20;
test(p, q); // 函数的调用
return 0;
(b)实参与形参的个数应相等,类型相匹配(相同或兼值赋容)。实参与形参按顺序对应,一对一的传递数据。
(c)实参可以是常量,变量或表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。所以,这里的变量是在圆括号()外面定义好赋好值的变量。
代码语言:javascript复制// 函数的定义
void test(int a, int b)
{
}
int main()
{
// 函数的调用
int p = 10, q = 20;
test(p, q); // right
test(11, 30 - 10); // right
test(int a, int b); // error, 不应该在圆括号里定义变量
return 0;
}
无参及有参函数的函数返回值
(a)如果函数定义没有返回值,函数定义时不能写void关键字,调用函数时也不能接收函数的返回值。
代码语言:javascript复制// 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right
void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方
int a = test(); // error, 函数定义根本就没有返回值
return 0;
}
(b)如果函数定义有返回值,这个返回值我们根据用户需要可用可不用,但是,假如我们需要使用这个函数返回值,我们需要定义一个匹配类型的变量来接收。
代码语言:javascript复制// 函数的定义, 返回值为int类型
int test()
{
}
int main()
{
// 函数的调用
int a = test(); // right, a为int类型
int b;
b = test(); // right, 和上面等级
char *p = test(); // 虽然调用成功没有意义, p为char *, 函数返回值为int, 类型不匹配
// error, 必须定义一个匹配类型的变量来接收返回值
// int只是类型,没有定义变量
int = test();
// error, 必须定义一个匹配类型的变量来接收返回值
// int只是类型,没有定义变量
int test();
return 0;
}
3.传值调用和传址调用
传值调用
函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式,这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
二者选择
相较于上面的另一种方式:只要是想通过函数改变main函数中局部变量的值就采用传址调用 由于指针是可以直接改变局部变量内存块中所对应的值,因此不用返回值 返回值类型为void,不需要则采用传值调用。
4.函数递归
递归的定义
程序调用自身的编程技巧称为递归。递归做为一种算法在程序设计语言中广泛应用,一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
递归的策略
只需少量的程序就可描述出解题过程中需要的多次重复计算,大大的减小了程序的代码量,递归的主要思考方式在于把大事化小。
递归的思路方式
1.先想问题的第一步搞清楚能否拆成一个常量和调用自己的函数来求解。
2.无返回值,在递归过程中找出口。有返回值,另有出口。
3,当函数递归到最后一步时,返回的是什么就是递归的出口。
递归的两个必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
函数递归求解斐波那契数列的缺陷
在使用fib这个函数的时候,如果我们要计算第50个斐波那契数字的时候特别耗费时间,使用factorial函数求10000的阶乘,程序会崩溃。
缺陷的产生原因
fib函数在调用的过程中很多计算其实一直在重复。在调试factorial函数的时候,如果参数比较大,那就会报错:stack overflow(栈溢出)这样的信息。系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者死递归,这样会导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象称之为栈溢出。
代码语言:javascript复制递归如果没有出口会导致栈溢出 递归的层次太深也会出现栈溢出
void test(int n)
{
if (n < 10000)
{
test(n 1);
}
}
int main()
{
test(1);
return 0;
}
如何改进
1.将递归改写成非递归。
2.使用static对象替代非static局部对象。在递归函数设计中,可以使用static对象替代非static局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且分为各个调用层所访问。
函数调用习题训练
1.写一个函数判断一个数是不是素数
代码语言:javascript复制#include<stdio.h>
#include<math.h>
#include<stdbool.h>
//代码1
int main()
{
int i = 0;
int count = 0;
for (i = 100; i <= 200; i )
{
//判断i是否为素数 是素数就打印
//用2到i-1的数字来试除i
int flag = 1;//flag是1表示是素数
int j = 0;
for (j = 2; j <= i-1; j )
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag == 1)
{
count ;
printf("%d ", i);
}
}
printf("ncount = %dn", count);
return 0;
}
//优化
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i =2)
{
//判断i是否为素数 是素数就打印
//m = a * b;a和b中一定有一个数是小于等于 sqrt(m)
int flag = 1;//flag是1表示是素数
int j = 0;
for (j = 2; j <= sqrt(i); j )
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag == 1)
{
count ;
printf("%d ", i);
}
}
printf("ncount = %dn", count);
return 0;
}
//代码2 函数版
int is_prime(int n)
{
int j = 0;
for (j = 2; j <= sqrt(n); j )
{
if (n % j == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i =2)
{
if (is_prime(i))
{
printf("%d ", i);
count ;
}
}
printf("ncount = %dn", count);
return 0;
}
//代码3 布尔类型
bool is_prime(int n)
{
int j = 0;
for (j = 2; j <= sqrt(n); j )
{
if (n % j == 0)
{
return false;
}
}
return true;
}
int main()
{
int i = 0;
for (i = 100; i <= 200; i )
{
if (is_prime(i))
{
printf("%d ", i);
}
}
return 0;
}
2.写一个函数判断一年是不是闰年
代码语言:javascript复制#include<stdio.h>
闰年的判断规则:1.能被4整除并且不能被100整除是闰年
2.能被400整除是闰年
//代码1
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year )
{
if (year % 4 == 0)
{
if (year % 100 != 0)
{
printf("%d ", year);
}
}
if (year % 400 == 0)
{
printf("%d ", year);
}
}
return 0;
}
//优化版
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year )
{
if (year % 4 == 0 && year % 100 != 0)
{
printf("%d ", year);
}
else if (year % 400 == 0)
{
printf("%d ", year);
}
}
return 0;
}
//简化版
int main()
{
int year = 0;
for (year = 1000; y <= 2000; year )
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
{
printf("%d ", year);
}
}
return 0;
}
//代码2 函数版
int is_leap_year(int y)
{
if (((y % 4) == 0 && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year )
{
if (is_leap_year(year))
{
printf("%d ", year);
}
}
return 0;
}
3.写一个函数,实现一个整型有序数组的二分查找
代码语言:javascript复制 int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = right - 1;
int mid = left (right - left) / 2;
while (left <= right)
{
if (arr[mid] < k)
{
left = mid 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid;
};
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
int ret = binary_search(arr, k, sz);
if (ret == -1)
{
printf("找不到n");
}
else
{
printf("找到了,下标是:%dn", ret);
}
return 0;
}
4.写一个函数,每调用一次这个函数,就会将变量的值加一
代码语言:javascript复制#include<stdio.h>
void Add(int* p)
{
(*p) ;
}
int main()
{
int num = 0;
Add(&num);
printf("%dn", num);
Add(&num);
printf("%dn", num);
Add(&num);
printf("%dn", num);
}
函数递归习题训练
1.接受一个整型值(无符号),按照顺序逆序分别打印它的每一位
代码语言:javascript复制#include<stdio.h>
接受一个整型值 倒序打印它的每一位
int main()
{
unsigned int num = 0;
scanf("%u", &num);
while (num)
{
printf("%d ", num % 10);
num /= 10;
}
return 0;
}
接受一个整型值 正序打印它的每一位
//递归的实现
// print(1234)
// print(123) 4
// print(12) 3 4
//print(1) 2 3 4
//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;
}
2.编写函数不允许创建变量,求字符串的长度
代码语言:javascript复制#include<stdio.h>
模拟实现strlen
int my_strlen(char* str)
{
int count = 0;
while (*str != '