函数的参数
实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传给形参。
形式参数(形参)
形式参数是指在函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
形式参数当函数调用完成之后就自动销毁了;因此,形式参数只在函数中有效。
函数的调用
传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用:
1、传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
2、这种传参的方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
(参考swap1,swap2)
应用场景:
传值调用:如在比较大小的时候
传址调用:运用在函数内部来操作函数外部某些变量的时候;如在换值的要求上。
练习
1、写一个函数可以判断一个数是不是素数。
int is_prime(int n){
int j=0;
for(j=2;j<n;j++){
if(n%j==0)
return 0;
}
if(n%--j==1) //可省略
return 1;
}
int main(){
int i=0;
for(i=100;i<=200;i++){
if(is_prime(i)==1)
printf("%d ",i);
}
return 0;
}
if(n%--j==0)
return 0;
这里为什么不添加——
else
return 1;
假设n=9时,一开始9被 j 整除取模为1,那这样的话,9就被判断为素数,可9并不是素数。
所以不能只单独被一个数整除。
这里j<n;可以换成j<=sqrt(n);sqrt函数是什么意思,大家可以去查阅文档。
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)==1){
printf("%d ",year);
}
}
return 0;
}
函数在设计的时候功能要单一、要干净。
以上的代码具有可移植性(指这个代码在其他平台也可以使用)。
这里在补充一点:vs编译器中F10是逐过程,F11是逐语句。
【注:我们在写函数时有一个特点,先去写函数的使用方法,再去定义函数。】
3、写一个函数,实现一个整形有序数组的二分查找。
二分查找的前提:在一个有序数组中查找具体的某个数。
int binary_search(int arr[],int k,int sz){
int left=0;
int right=sz-1;
while(left<=right){
int mid=(left+right)/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=7;
int sz=sizeof(arr)/sizeof(arr[0]);
int ret=binary_search(arr,k,sz);
if(ret==-1){
printf("找不到指定数\n");
}
else{
printf("找到了,下标是:%d\n",ret);
}
return 0;
}
此代码,前面有讲过,只是这里换成函数的部分,但大致逻辑不变,如若有不会的同学,请自行翻阅前面的知识点。
【注:形参和实参的名字相同是没有问题的。】
int arr[ ]——这里这个数组可以不用指定大小,本质也不是数组。
我们知道形参是实参的一份临时拷贝,那我们在传递数组时,也会创建临时空间(数组)来接受(比如,原有1万个元素数组传参后,也建立一个一万个元素数组,这样会导致空间浪费严重)。
数组是一块连续的空间,它里面可能放很多个元素;而数组在传参的时候,并不会传整个数组,仅仅传数组第一个元素的地址。
而这时,数组名代表的是首元素的地址。
int ret=binary_search(arr,k);
且int binary_search(int arr[ ],int k)中尽管int arr[ ]写成数组的形式,但其实arr并不是数组;实际这里的arr是一个指针(指针接受地址)
所以在int sz=sizeof(arr)/sizeof(arr[0]);中其实sizeof(arr)算的是指针大小——int sz=4/4=1
简单来讲,就是不能把计算数组元素个数放在函数内部。
因此,要把int sz=sizeof(arr)/sizeof(arr[0]);放在main函数里头,然后
int ret=binary_search(arr,k);要改为int ret=binary_search(arr,k,z);
int binary_search(int arr[],int k)要改为int binary_search(int arr[],int k,int sz)
这里提一点:尽管arr传过去的是第一个元素地址,但指针在接收时,其实能看到arr数组里的内容。(通过这个地址找到原来的数组,前面有简单讲过)
4、写一个函数,每调成一次这个函数,就会将num的值增加1。
void Add(int*p){
(*p)++;
}
int main(){
int num=0;
Add(&num);
printf("num=%d\n",num);
Add(&num);
printf("num=%d\n",num);
Add(&num)
printf("num=%d\n",num);
return 0;
}
补充:“*p++”中“++”优先级比较高,是作用域“p”的,而不是“*p”。
改进方法:(*p)++让++作用域(*p)整体
有关操作符的优先级后续操作符部分会讲解。
函数的嵌套调用和链式访问
函数和函数之间可以有机的组合的。
嵌套调用
#include<stdio.h>
void new_line(){
printf("hehe\n");//new_line函数打印“hehe”
}
void three_line(){
int i=0;
for(i=0;i<3;i++){
new_line();//把new_line放进去调用
}
}
int main(){
three_line();//three_line要执行的话,就需要main函数调用
return 0;
}
打印结果为三个hehe(for循环三次封装的new_line函数)
这样,main函数调用three_line,而three_line调用new_line就叫嵌套调用。
链式访问
把一个函数的返回值作为另一个函数的参数。
如——
int main(){
int len=0;
len=strlen("abc");
printf("%d\n",len);
return 0;
}
可以把
len=strlen("abc");
printf("%d\n",len);
简化成
printf("%d\n",strlen("abc"));
像这种把原本需要多种步骤给组合成一种的(串在一起)的就叫链式访问。
举例:
int main(){
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
打印结果为43 2 1
printf函数打印的是字符个数。
最开始43,但又因是两个字符,第二次打印为2,又因2为一个字符,所以第三次为1(由里到外)
【注:这道题很经典,大家要注意】
函数的声明:
1、告诉编译器有一个函数叫什么,参数是什么,返回类型是什么;但是具体是不是存在,无关紧要。
2、函数的声明一般出现在函数的使用之前,要满足先声明后使用。
3、函数的声明一般要放在头文件中的。
函数的定义:
函数的定义是指函数的具体实现,交代函数的功能实现。
add.h源文件——函数的声明
int Add(int ,int )
test.c源文件——函数调用
int main(){
int a=10;
int b=20;
int sum=0;
sum=Add(a,b);
printf("%d\n",sum);
return 0;
}
add.c源文件——函数定义
int Add(int x,int y){
int z=0;
z=x+y;
return 0;
}
在正经工程里,一般是我们可以将函数的声明放在新建头文件里,把函数的定义(实现)放在新建源文件中,这样只需在原本的源文件中的函数调用给加上一个头文件
#include"add.h"
【注:引用库函数时的头文件用兼括号<>,用自己写的代码头文件用双引号" "】
为什么?
假设十个人要拆分任务,要写计算器,如:A负责加法,B负责减法,C负责乘法......这样分工;如果都在test.c文件里面写代码,就会复杂化。
所以,分模块写代码效果更好。
一般在新建头文件中函数声明的部分,通常会加上——
#ifndef_ADD_H_
#define_ADD_H_
int Add(int x,int y);——函数声明
#endif
这里ADD_H_一般是通过文件名来定义符号
【注:#include包含一文头文件的时候,其编译器会把<stdio.h>或将"add.h"的内容全部都拷贝郭过来,比如#include<stdio.h>会把stdio.h的内容全部拷过来】
这样你在工程里面引用了stdio.h的头文件,如果其他人也引用这样的头文件,或者大家都引用的话,那么就重复使用了;在一个工程引用了很多份,同样的代码出现了很多次。(假设#include<stdio.h>只引用一个内容)
那为了避免同一个头文件被很多人引用多次,任何一个头文件我们都会这样去设计,会在前面加上——
#ifndef_ADD_H_
#define_ADD_H_
......
#endif
#ifndef——如果没有定义过后面符号的话,那第一次判断结果就为真了,那下面就要了。
#define——定义_ADD_H_,#include里int Add(int x,int y);这行代码就包含过去。
#endif——包含结束
当我们第一次包含之后,如果有人以后又包含一次add.h的文件时候(包含第二次,第三次等时候),都会先判断#ifndef有没有被定义(如果被定义过的话,判断为假,后面的代码就不再被执行,即不参与编译)
这样就防止同一个头文件被引用多次。(只有头文件里会这样被设计)