函数
函数的定义:
函数,又称为子程序,是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的影藏。这些代码通常被集成为软件库。
函数的分类:
库函数:
早期的C语言是没有库函数的,在实际生活中往往会存在很多问题:代码冗余;开发效率低;不标准;
把常用的一些功能实现成函数,集成为库,由C语言直接提供。从而C语言标准,就可以规定库函数的标准;
C语言常用的库函数:IO函数,字符操作函数,内存操作函数,时间/日期函数,数学函数,其他库函数等;
注:使用库函数,必须包含#include对应的头文件
案例一:
strcpy函数:字符串拷贝函数
原型:extern char* strcpy(char* dest, char* src);
功能:把从src地址开始且含有NULL结束符的字符串赋值到以dest开始的地址空间,返回dest(地址中存储的为复制后的新值);
要求:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串;
int main()
{
char arr1[] = "abcdef";//a b c d e f \0,注意:\0也会拷贝过来
char arr2[20] = {0};
//能把arr1中的abcdef拷贝到arr2中
strcpy(arr2,arr1);
printf("%s\n",arr2);
return 0;
}
运行结果:
案例二:
memset函数:内存填充函数
sizeof()函数的返回值类型是size_t,而size_t是unsigned int型;
void* memset(void* s, int ch, size_t n); (int ch可以是char或int);
将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作, 其返回值为指向S的指针;
int main()
{
char arr[] = "hello csdn";
//设置内存的时候是以字节为单位的
//每个字节的内容都是一样的value
memset(arr,'X',6);//XXXXXXcsdn
//memset(arr+1, 'X', 5);//hXXXXXcsdn,arr+1:表示从第二个字节开始
printf("%s\n",arr);
return 0;
}
运行结果:
自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数
函数格式:
//函数组成:
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
函数的参数:
实参:真实传给函数的参数;实参可以是常量,变量,表达式,函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参;
形参:形式参数是指函数名后括号中的变量,因为形参只有在函数被调用的过程中才实例化(分配内存);形参在函数调用完之后就会自动销毁,因此形式参数只在函数中有效;
案例分析:
写一个函数交换两个整型变量的内容
代码一:
void swap(int m, int n)
{
int temp = m;
m = n;
n = temp;
//printf("swap m = %d,n = %d\n", m, n);
}
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;
}
运行结果:
从运行结果可知,在调用swap函数之后,a和b的值在交换前后并未发生变化,说明该函数并不能完成两个变量的内容交换。
通过调试来一探究竟:
a.在从主函数进入被调函数swap之前,通过键盘输入a和b的值,并查看其对应的地址分别为:
b.在进入被调函数swap之初,通过观察发现,a和b的值分别赋给了m和n,此时m和n的分别为3和6:
c.将swap内部的函数执行完毕之后,可以发现m和n的值已经发生了互换,但是并没有改变a和b的值,此时a和b的值依旧是3和6:
d.通过查看变量各自的地址空间可以发现,此时a和b对应的地址空间与m和n对应的地址空间是完全不相同的:
e.虽然被调函数swap中m和n的值发生了互换,但是并没有影响到主函数中a和b的值;究其原因可以发现,a和b所占用的内存空间与m和n所占用的内存空间是完全不同的;因此,对m和n的内存空间上的值进行修改修改并不能影响到对应的主函数中的a和b的值。
代码二:
void swap(int* x, int* y)
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b);
printf("交换前:a=%d,b=%d\n",a,b);
swap(&a,&b);//按F11进入swap内部进行调试
printf("交换后:a=%d,b=%d\n",a,b);
return 0;
}
运行结果:
分析:
此时,a和b的值在交换前后发生了变化,通过调试可以发现:
a.在从主函数进入被调函数swap之前,a和b对应的值及其地址分别为:
b.在进入被调函数swap之初,此时变量x和y的值分别等于对应的a和b所占内存的地址;同时,在变量x和y中存放的值分别为3和6:
c.待swap函数执行完毕返回主函数时,我们可以发现,x和y中存放的值均已发送变化,同时a和b的值也相应的发生了交换:
总结:
在传值时:
当实参传给形参的时候,形参是实参的一份零时拷贝,对形参的修改不会影响实参;
函数参数在传递的时候,都是传原数据的副本,也就是说,swap内部使用的a和b只是最初始a和b的一个副本而已;
所以无论在swap函数内部对a和b做任何改变,都不会影响初始的a和b的值
在传地址时:
实参和形参使用的是同一内存的空间
C语言里,参数传递都是值传递。也就是说,我们所认为的传指针也是传值,只不过它的值是指针;
如果想要改变入参内容,则需要传该入参的地址(指针和引用都是类似的作用),通过解引用修改其指向的内容
函数的调用:
传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种函数调用的方式;这种传参方式可以让函数和函数外边的变量建起真正的联系,也就是函数内部可以直接操作函数外部的变量
练习:
案例一:
写一个函数:判断一个数是不是素数
#include<math.h>
int is_prime(int n)
{
//2到n-1试除
//2到sqrt(n)试除
//1不是素数
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
{
return 0;//不是素数
}
}
return 1;//是素数
}
int main()
{
//打印100-200之间的素数
int i = 0;
for (i = 100; i <= 200; i++)
{
//判断i是否为素数
if (is_prime(i) == 1)
{
printf("%d ",i);
}
}
return 0;
}
运行结果:
案例二:
写一个函数判断一年是不是闰年
int is_leap_year(int y)
{
return (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0));
}
int main()
{
//1000-1050年的闰年
int y = 0;
for (y = 1000; y <= 1050; y++)
{
//判断y是不是闰年
if (is_leap_year(y) == 1)
{
printf("%d ",y);
}
}
printf("\n");
return 0;
}
运行结果:
案例三:
写一个函数,实现一个整型有序数组的二分查找,找到了就返回下标,找不到就返回-1
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
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 = 0;
scanf("%d",&k);//输入要查找的元素
int sz = sizeof(arr) / sizeof(arr[0]);
int ret=binary_search(arr,k,sz);
if (-1 == ret)
{
printf("找不到\n");
}
else
{
printf("找到了,下标是:%d\n",ret);
}
return 0;
}
运行结果:
错误案例:
int binary_search(int arr[], int k)//int arr[]本质是个int* arr
{
//数组在传参的时候,传递的不是整个数组,传递的是数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);//1,4/4=1,arr是个指针
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
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 = 0;
scanf("%d", &k);//输入要查找的元素
//int sz = sizeof(arr) / sizeof(arr[0]);
int ret = binary_search(arr, k);
if (-1 == ret)
{
printf("找不到\n");
}
else
{
printf("找到了,下标是:%d\n", ret);
}
return 0;
}
运行结果:
总结:当数组作为参数传参时,这里实际是同名的指针,而不是整个数组,拷贝进去的只是数组元素的首地址,传进去的不是值而是变量的地址,如果传递整个数组的话,对内存来说将会浪费很大的空间,这里不管数组多大都只传递数组的首地址,所以数组传参是传址。
案例四:
写一个函数,每调用一次这个函数,就会将num的值增加1
void test(int *p)
{
*p = *p + 1;//(*p)++
}
int main()
{
int num = 0;
test(&num);
printf("%d\n",num);//1
test(&num);
printf("%d\n", num);//2
return 0;
}
运行结果:
函数的嵌套调用:
函数可以嵌套调用,但是不能嵌套定义
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;
}
函数的链式访问:
把一个函数的返回值作为另外一个函数的参数
int main()
{
//int len = strlen("abcdef");
//printf("len=%d\n",len); //strlen计算字符串的长度时,遇到‘\0’停止计数,不会统计‘\0’;而sizeof统计‘\0’(在计算字符数组时)
printf("%d\n",strlen("abcdef"));
//printf函数的返回值是打印的字符的个数
printf("%d",printf("%d",printf("%d",43)));//4321:第一次打印43 第二次打印2(因为包含4和3两个字符) 第三次打印1(因为只显示了一个字符2),
printf("\n");
return 0;
}
运行结果:
总结:printf()函数也是有返回值的,一般是返回它所输出的数据的字符数目,如果printf()函数执行失败,就会返回一个负数;
大多情况下的程序是不需要处理printf()函数的返回值的,一般是在检测printf()函数是否执行成功的时候会来处理查看这个返回值
函数的声明:
告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在,函数声明决定不了;
函数的声明一般出现在函数的使用之前,要满足先声明后使用;
函数的声明一般要放在头文件中的
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
当我们运行的时候,可以发现编译器发出了一个警告:warning C4013: “Add”未定义;假设外部返回 int,说明这是一种不规范的写法。函数的声明一般出现在函数的使用之前,要满足先声明后使用
改进:
//形参的名字可以省略
int Add(int x, int y);//或者int Add(int,int);
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
函数的定义:
函数的定义是指函数的具体实现,交代函数的功能实现;
一般情况下,函数的声明和定义是分开的
函数的声明一般是放在头文件add.h里的:
函数的定义一般是放在源文件add.c里的:
在主函数test.c中,要想使用该函数的话,需要包含对应的头文件:#include"add.h"
(一般情况,个人写的头文件往往要用“ ”,比如:#include"add.h";调用库函数时,头文件用<>,比如:#include<stdio.h>)
函数递归:
什么是递归?程序调用自身的编程技巧称为递归;只需少量的程序就可描述出解决过程所需要的多次重复计算,大大减少了程序的代码量;
递归的主要思考方式在于:把大事化小
int main()
{
printf("hehe\n"); // warning C4717: “main”: 如递归所有控件路径,函数将导致运行时堆栈溢出
main();
return 0;
}
递归的必要条件:
存在限制条件,当满足这个限制条件的时候,递归便不再继续;
每次递归调用之后越来越接近这个限制条件
案例一:
接收一个整型值(无符号),按照顺序打印它的每一位
void print(int n)
{
if (n > 9)
print(n/10);
printf("%d ",n%10);
}
int main()
{
unsigned int num = 0;
scanf("%d",&num);//1234
print(num);//print函数可以把num的每一位按顺序打印出来
printf("\n");
return 0;
}
运行结果:
分析:a.从主函数开始,当从键盘输入1234后,开始第一次的print函数调用;第一次调用print函数时,此时的n=1234。由于n>9成立,所以进入if判断并进行第二次的print函数调用;
b.第二次print函数调用时的参数是:n/10,也就是n=1234/10=123,此时的n>9依旧成立;那么此刻则要进入if判断并进行第三次的print函数调用;
c.第三次print函数调用时的参数是:n/10,也就是n=123/10=12,此时的n>9依旧成立;那么此刻则进入if判断并进行第四次的print函数调用;
d.第四次print函数调用时的参数是:n/10,也就是n=12/10=1,此时的n<9,则执行printf("%d “,n%10);打印输出1,那么第四次的print函数彻底执行完毕,则返回到第三次print函数调用中的print(n/10)语句;
e.第三次的print函数中的n=12,在if判断语句中的print(n/10)执行完毕后,则执行printf(”%d “,n%10);打印输出2,那么第三次的print函数彻底执行完毕,则返回到第二次print函数调用中的print(n/10)语句;
f.第二次的print函数中的n=123,在if判断语句中的print(n/10)执行完毕后,则执行printf(”%d “,n%10);打印输出3,那么第二次的print函数彻底执行完毕,则返回到第一次print函数调用中的print(n/10)语句;
g.第一次的print函数中的n=1234,在if判断语句中的print(n/10)执行完毕后,则执行printf(”%d ",n%10);打印输出4,那么第一次的print函数彻底执行完毕,则返回到主函数中的print(num),此时的函数递归调用彻底结束。
案例二:
编写函数不允许创建临时变量,求字符串的长度
#include<string.h>
//带临时变量
int my_strlen(char* str)
{
int count = 0;//统计字符的个数
while(*str != '\0')//*是解引用的意思
{
count++;
str++;//str是指针,存放地址
}
return count;
}
//不带临时变量
int my_strlen(char* str)
{
if (*str != '\0')
{
return 1 + my_strlen(str+1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = "abcdef";//a b c d e f \0
//char* str = arr;
//int len = strlen(arr);
int len = my_strlen(arr);//不包含\0,读取到\0则停止
printf("%d\n",len);
return 0;
}
运行结果:
注意:当递归到一定限度时会出现Stack overflow——栈溢出,所以递归调用时,要加上一定的限制条件
**递归的缺点:**递归调用,占用空间大;递归太深,易发生栈溢出;可能存在重复计算
递归与迭代:
迭代:利用变量的原值推算出变量的一个新值,如果递归是自己调用自己的话,迭代就是A不停的调用B。
迭代与普通循环的区别:迭代时,循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值。
递归与普通循环的区别:循环是有去无回,而递归则是有去有回(因为存在终止条件)。
在循环的次数较大的时候,迭代的效率明显高于递归。递归中一定有迭代,但是迭代中不一定有递归。能用迭代的不用递归,递归调用函数,浪费空间,并且递归太深容易造成堆栈的溢出。
案例一:
求n的阶乘(不考虑溢出)
//递归版本
int fac(int n)
{
if (n <= 1)
return 1;
else
return n * fac(n-1);
}
int main()
{
int n = 0;
scanf("%d",&n);
int ret = fac(n);
printf("%d\n",ret);
return 0;
}
运行结果:
案例二:
求第n个斐波那契数
斐波那契数列:1 1 2 3 5 8 13 21 34 55…(前两个数相加等于第三个数)
fib(n):n<=2,1;n>2,fib(n-1)+fib(n-2)
//递归版本
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n-2);
}
int main()
{
int n = 0;
scanf_s("%d",&n);
int ret = fib(n);
printf("%d\n",ret);
return 0;
}
运行结果:
通过以上两个案例,我们发现:
在使用fac函数求10000的阶乘(不考虑结果的正确性),程序会崩溃;
在使用fib这个函数的时候求第50个斐波那契数字的时候会特别消耗时间
为什么呢?
我们发现fib函数在调用的过程中很多计算其实在一直重复。
如果我们把代码修改一下:
int count = 0;//全局变量
int fib(int n)
{
if (n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int sum = fib(n);
printf("%d\n", sum);
printf("count=%d\n", count);
return 0;
}
运行结果:
通过运行结果可知,在计算第30个斐波那契数的时候,第3个斐波那契数以及被计算了317811次,这说明fib函数在调用的过程中很多计算其实在一直重复。
那如何解决上述的问题?
将递归改写成非递归;
使用static对象替代nonstatic 局部对象。在递归函数设计中,可以使用static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic 对象的开销,而且static 对象还可以保存递归调用的中间状状态并且可为各个调用层所访问。
那如何对案例一二进行修改以提升程序的运行效率呢?在这里我们主要考虑用迭代的方法代替递归。
案例一改进版:
int fac(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
return ret;
}
int main()
{
int n = 0;
scanf("%d",&n);
int ret = fac(n);
printf("%d\n",ret);
return 0;
}
案例二改进版:
int fib(int 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_s("%d",&n);
int ret = fib(n);
printf("%d\n",ret);
return 0;
}
提示:
1.许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰;
2.但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些;
3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时的开销。