概述
函数
目录
- 函数是什么
- 库函数
- 自定义函数
- 函数参数
- 函数调用
- 函数的嵌套调用和链式访问
- 函数的声明和定义
- 函数递归
1.函数是什么
数学里的函数f(x)=2*x+1
但是C语言中的函数是什么样的呢?在维基百科中的定义为子程序。
在计算机科学中,子程序,是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
C语言中函数的分类
1.库函数
2.自定义函数
2.库函数
库函数是什么呢?
库函数(Library function)是将函数封装入库,供用户使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include<>加到里面就可以了。
为什么会有库函数呢?
1.我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
2.在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
3.在编程的过程中我们会频繁的做一些字符串的比较工作(strcmp)
为了支持可移植性和提高代码的效率,所以C语言的基础库中提供了一系列类似的库函数。
C语言常用的库函数都有:
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
简单说两个库函数
1.strcpy
利用MSDN我们可以查阅到库函数的说明与讲解,这里需要大家略懂一些英文,MSDN上面说Copy a string,意思是拷贝一个字符串,这个函数里面需要两个参数,第一个是目的地,第二个是来源,意思就是将来源的字符串拷贝给目的地。看下面代码以及输出:
//strcpy字符串拷贝函数
#include<string.h>//头文件
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world";
strcpy(arr1, arr2);
printf("%s", arr1);//打印arr1这个字符串 %s - 以字符串形式打印
return 0;
}
2.memset
对这个函数的解释是将缓冲区设置为指定的字符,将某一块内存中的内容全部设置为指定的值。这个函数需要三个参数,void *memset(void *s, int ch, size_t n),将s中当前位置后面的n个字节用 ch替换并返回 s 。看下面代码和输出:
//memset - 内存设置
#include<string.h>//头文件
int main()
{
char arr[] = "hello world";
memset(arr, 'x', 5);
printf("%sn", arr);
return 0;
}
我们如何学会使用库函数呢?
我们要学会查询工具的使用:
MSDN(Microsoft Developer Network)
www.cplusplus.com
http://en.cppreference.com
第三个学习网站如果想看中文的将前面的en换成zh即可。
3.自定义函数
自定义函数和库函数一样,有函数名,返回类型和函数参数。不一样的是自定义函数是我们自己设计的,发挥空间很大。
写一个函数找出两个数的较大值
//函数的实现
int get_max(int x,int y)
{//返回类型//函数名//传入参数类型
return x>y?x:y;//返回较大值
}
int main()
{
int a=10;
int b=20;
//函数的调用
int max=get_max(a,b);//将返回的较大值放入max中
printf("%dn",max);
return 0;
}
写一个函数将两个变量的值交换
//函数返回类型的地方写出:void,表示这个函数不返回任何值,也不需要返回
void Swap1(int x,int y)
{
int temp=0;
temp=x;
x=y;
y=temp;
}
int main()
{
int a=10;
int b=20;
//写一个函数交换2个整形变量的值
printf("交换前:a=%d b=%d",a,b);
Swap1(a,b);
printf("交换后:a=%d b=%d",a,b);
return 0;
}
然而没有交换成功,为什么这个函数没有完成任务呢?
实践上a和b在传进函数时,创建了一份新的空间x和y,这里只是将x和y的值交换了,而没有操作a和b的权利,a和b与x和y是独立的空间,**下图时调试监视的值。**可以看到a与x的地址不一样,b与y的地址不一样。
那么怎么才能完成交换呢?
int main()
{
int a=10;//4个字节的空间
int *pa=&a;//pa是一个指针变量
*pa=20;
printf("%d",a);//20
return 0;
}
在上面代码中我们可以通过*pa找到a,将a的值改为20,那么我们交换函数就可以这样写:
#include<stdio.h>
void Swap2(int* pa,int* pb)
{
int temp=0;
temp=*pa;//*pa是通过pa找到a
*pa=*pb;
*pb=temp;
}
int main()
{
int a=10;
int b=20;
//写一个函数交换2个整形变量的值
printf("交换前:a=%d b=%d",a,b);
Swap2(&a,&b);
printf("交换后:a=%d b=%d",a,b);
return 0;
}
我们将a和b的地址传进去,那么函数里面操作的就是a和b了。
可以看到此时将地址传进去,pa和a的地址一样,pb和b的地址一样
4.函数的参数
函数调用:
函数定义:
在Swap1和Swap2函数调用中的a和b为实际参数,在Swap1和Swap2函数定义中的x,y,pa,pb都为形式参数。
实际参数
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类
型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
例:
int a=10;
int b=20;
int max=get_max(a,get_max(3,5));//将返回的较大值放入max中
get_max(3,5)获得5,然后int max=get_max(a,5)。
形式参数
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配
内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在
函数中有效。形参实例化之后相当于是实参的一份临时拷贝。
5.函数调用
传值调用
函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。下图是传值调用:
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操
作函数外部的变量。下图是传址调用:
练习
写一个判断素数的函数
#include<stdio.h>
int is_prime(int n)
{
int j=0;
for(j=2;j<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;
}
写一个判断闰年的函数
#include<stdio.h>
//is_leap_year
//如果是闰年返回1
//不是闰年返回0
int is_leap_year(n)
{
//return ((y%4==0&&y%100!=0)||(y%400==0))
if((n%4==0&&n%100!=0)||(n%400==0))
{
return 1;
}
return 0;
}
int main()
{
int y=0;
for(y=1000;y<=2000;y++)
{
if(is_leap_year(y)==1)
{
printf("%d ",y);
}
}
return 0;
}
写一个函数,实现二分有序查找
#include<stdio.h>
//int binary_search(int *a,int k,int s)
int binary_search(int a[],int k,int s)
{
int left=0;
int right=s-1;
while(left<=right)
{
int mid=(left+right)/2;
if(a[mid]>k)
{
right=mid-1;
}
else if(a[mid]<k)
{
left=mid+1;
}
else
{
return mid;
}
}
return -1;//找不到了
}
int main()
{
int arr[]={1,2,3,4,5,6,7,8,9,10};
int key=7;
int sz=sizeof(arr)/sizeof(arr[0]);
//找到了就返回位置
//找不到返回-1
//数组arr传参实际上传递的不是数组本身
//仅仅传过去了数组首元素的地址
int ret = binary_search(arr,key,sz);
if(-1==ret)
{
printf("找不到n");
}
else
{
printf("找到了,下标是:%dn",ret);
}
return 0;
}
数组arr传参实际上传递的不是数组本身
仅仅传过去了数组首元素的地址
注意:数组传参时不能在函数里面计算数组的元素个数。
写一个函数,每调用一次这个函数,就会将num的值增加1
#include<stdio.h>
void Add(int *p)
{
(*p)++;
}
int main()
{
int num = 0;
//每调用一次这个函数,就会将num的值增加1
Add(&num);
printf("%dn",num);//1
Add(&num);
printf("%dn",num);//2
Add(&num);
printf("%dn",num);//3
return 0;
}
6.函数的嵌套调用和链式访问
嵌套调用
int test3()
{
printf("hahan");
}
int test2()
{
test3();//调用test3
}
int main()
{
test2();
return 0;
}
链式访问
把一个函数的返回值作为另外一个函数的参数。
#include<stdio.h>
#include<string.h>
int main()
{
int len = strlen("abc");
printf("%dn",len);
//链式访问
printf("%dn",strlen("abc"));
return 0;
}
代码打印如下图:
int main()
{
printf("%d",printf("%d",printf("%d",43)));//打印4321
}
打印如下图:
printf函数返回的值是字符个数,我们解释一下代码,第一个printf打印的是printf("%d",printf("%d",43))的返回值,第二个printf打印的是printf("%d",43)的返回值,第三个printf打印的是43,首先打印43,43是两个字符个数,所以第二个printf打印的是2,2是一个字符,所以第一个printf打印的是1。
7.函数的声明和定义
函数声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关
紧要。 - 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
int main()
{
int a=10;
int b=20;
int c=Add(a,b);
printf("%dn",c);
return 0;
}
int Add(int x,int y)
{
return x+y;
}
会出现警告,如下图:
编译器从前往后扫描,当看到Add时,编译器没见过Add,就会警告,这时就需要声明一下
int main()
{
int a=10;
int b=20;
//函数声明一下 - 告知
int Add(int,int);
int c=Add(a,b);
printf("%dn",c);
return 0;
}
int Add(int x,int y)
{
return x+y;
}
这时就不会报警告了。
函数的定义
函数的定义是指函数的具体实现,说明函数的功能实现。
我们通常在test.h中放置函数的声明
在test.c的中放置函数的实现
8.函数递归
什么是递归?
函数自己调用自己就是递归,它通常把一个大型复杂的问题层层转换为一个与原问题相似的规模较小的问题来求解,递归思想只需要少量程序
int main()
{
printf("hahan");
main();
return 0;
}
main函数自己调用自己
程序会死循环,自己调用自己,最后会栈溢出,下面我们会有栈溢出的讲解
练习1
接受一个整形值,按照顺序打印它的每一位。例如:输入1234,输出1234.
1234%10 = 4
1234/10=123 123%10 = 3
123/10=12 12%10 = 2
12/10=1 1%10 = 1
1/10=0
%10是算的是个位数
递归怎么做?
把大事化小
print(1234)
print(1234)可以拆分为:print(123) 4
print(123)可以拆分为:print(12) 3 4
print(12) 可以拆分为:print(1) 2 3 4
void print(unsigned int n)
{
if(n>9)
{
print(n/10);//将1234拆成123
}
printf("%d ",n%10);
}
int main()
{
unsigned int num = 0;
scanf("%d",&num);//1234
//递归-自己调用自己
print();//print函数可以打印参数部分数字的每一位
return 0;
}
递归的两个必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件
但是满足这两个条件,不一定正确。
void test(int n)
{
if(n<10000)
{
test(n+1);
}
}
int main()
{
test(1);
return 0;
}
此时程序会崩,栈溢出,如下图的异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDckVqFp-1618739053573)(C:Users15191107746AppDataRoamingTyporatypora-user-imagesimage-20210418100021065.png)]
递归栈溢出的解释:
内存分配空间如图:
我们来单独看看栈区:
事实上,函数调用的参数是通过栈空间来传递的,而递归调用,只有走到最后的结束点后函数才能依次退出,而未到达最后的结束点之前,占用的栈空间一直没有释放,如果递归调用次数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,导致程序的异常退出。
写递归代码的时候需要注意:
1.不能死递归,都有跳出条件,每次递归逼近跳出条件
2.递归层次不能太深
推荐一个网站
www.stackoverflow.com
相当于程序员的知乎!
练习2
编写函数不允许创建临时变量,求字符串长度
#include<stdio.h>
#include<string.h>
int main()
{
char arr[]="abc";
//['a']['b']['c']['