【20180828】【C/C++基础知识】函数的声明、定义和调用,函数的参数传递,函数与数组,函数的返回值
函数的声明、定义和调用
函数:提高可读性。实现多个功能,只需对每个功能编写程序,主程序调用所写程序即可。
以这个问题为例:
- 函数声明(有分号):
status ReadInfo(char *name, struct student stu[], int *num);
输入参数:文件名称name
输出参数:学生人数num;学生详细信息:结构数组stu
函数返回值:打开文件成功与否
需要注意的:
(1)函数名称ReadInfo:要满足标识符定义,同时与要实现的功能相关。函数名称后面跟一个小括号,表明这个不是普通变量名称,而是函数名称。小括号里面是参数。这里参数有三个,第一个是输入参数(文件名),它是字符串类型的。第二个参数是读到的学生信息(结构数组来存储)。第三个是学生人数。第二个和第三个是输出参数,所以我们必须用地址。函数返回值,我们这里是status,这个是没有定义的数据类型,实际上就是整型,用来判别文件打开成功与否。
(2)函数定义(去掉函数声明后面的分号,后面跟一个花括号,花括号里面是函数体):
(3)函数体:把输入参数转换成输出结果的语句序列。
- 函数的定义(没有分号):
函数类型 函数名() // 若函数无返回值,用void定义返回值类型
// 函数名称满足标识符定义,并且不可与关键字重复
// 函数名称后面小括号里面是参数:输入参数和输出参数,若没有参数,可为空或void
{
声明语句序列
可执行语句序列
return; // 若没有返回值,return可不跟任何值或者直接省略
}
例:
void 函数名(void)
{
声明语句序列
可执行语句序列
return;
}
void main()
{
……
}
int main()
{
……
return 0;
}
- 函数调用(有分号):
函数的执行流程:
函数定义之后,需要在主程序中直接或间接调用这个函数。main函数执行到函数调用的地方时,就会跳转到函数定义的地方,将实际参数的值直接拷贝给实参对应的参数(实参传递),然后继续执行函数体,把输入数据转换成输出结果,然后函数返回之后,又回到main函数调用的地方继续向下执行。
注意:
(1)函数生来平等,相互独立,没有高低贵贱和从属之分。
(2)函数与函数之间通过传递参数和返回值相联系。
(3)C语言中不允许函数嵌套定义:在一个函数中再定义一个函数是非法的。但函数的调用是可以嵌套调用的!
对于main函数:
(1)main函数也是函数,但它比较特殊。
(2)C语言的执行从main函数开始,一个函数可以调用其他函数,也可以被其他函数调用。
(3)调用其他函数后流程回到main函数。
(4)在main函数中结束整个程序运行。
函数:C语言中模块化编程的最小单位。(可以把每个函数看做一个模块Module)
(1)标准库函数:
(2)第三方库函数:
(3)自定义函数:
分析:
(1)每个函数需要若干个变量定义和把输入转化为输出的执行语句。
(2)每个源程序文件由编译预处理命令和若干个具有关联的函数构成。
(3)每个C语言程序由一个或多个源程序文件组成。(若干个源程序构成一个具有实际功能的C程序)
也即:一个C程序由若干个源程序构成;一个源程序由若干个函数构成;一个程序由若干个模块构成。
分析问题:自上而下(按照问题的功能)
解决问题:自下而上(一步步细化成每个模块,最后组合所有模块—复杂问题细化成一个个小的问题)
例1:计算整数n的阶乘n!
算法:n!=n*(n-1)*(n-2)*…(多个数的乘积,可能会很大,因此我们定义函数的类型是long)
/* 计算整数n的阶乘n! */ /* 我写的程序 */ #include <stdio.h> #include <stdlib.h> int n,Jn=1; int JieCheng(n,Jn) { do { Jn*=n; n--; }while(n>0); return Jn; } int main() { printf("*******************************计算整数n的阶乘n!**********************************\n请输入一个整数:\n"); scanf_s("%d",&n); JieCheng; printf("n的阶乘是%d",Jn); system("pause"); return 0; }
我的问题:
(1)实参和形参一样了,不建议这样!
(2)函数JieChen好像没有成功调用。(已解决:参数没有成功传递,需要传地址调用!)
/* 计算整数n的阶乘n! */ /* 修改后的:用了传地址调用! */ #include <stdio.h> #include <stdlib.h> int n,Jn=1; long JieCheng() { do { Jn*=n; n--; }while(n>0); return Jn; } int main() { printf("*******************************计算整数n的阶乘n!**********************************\n请输入一个整数:\n"); scanf_s("%d",&n); JieCheng(&Jn); printf("n的阶乘是%d\n",Jn); system("pause"); return 0; }
通常在函数前面会加注释:
/* 函数功能:用迭代法计算n!
函数入口参数:整型变量n表示阶乘的阶数
函数返回值:返回n!的值 */
函数的参数传递:实参完全拷贝到形参,程序执行流程转向由函数名指定的被调用函数。
请问:ReadInfo(name,student,&n); n前面为什么要加&?(试一下,自己写测试程序,比较结果)
区别:
(1)简单的传值:将函数调用语句中的实参的一份副本传给函数的形参(简单的值的传递,实参的值没有发生变化)。
(2)传地址调用:将变量的地址传递给函数的形参。形参和实参指向了同一个内存地址,对形参的操作同时影响了实参的值。
例2:传地址调用。
/* 测试程序:函数间的参数传递 */ #include <stdio.h> #include <stdlib.h> void GetNum1(int num) // 实参0传递给了形参,然后函数体内将形参修改为10,然后返回主函数继续执行,函数体执行完毕,释放形参,此时实参值未修改,还是0,因此输出是0。 { num=10; return; } int GetNum2(int *num) // 与GetNum1不同的是,GetNum2形参传递的是地址,实参将n的地址传给形参,因此形参也指向变量n,函数体内修改该地址内的值为10,执行完毕形参被释放,此时实参所指变量n的值被修改为10。 { *num=10; return 0; } int main() { int m=0,n=0; // 实参值都是0 GetNum1(m); GetNum2(&n); printf("m=%d\nn=%d\n",m,n); system("pause"); return 0; }
注意:传地址调用,主函数里面用&,函数体里面用*,否则会出错!!!
例3:交换a和b的值。
/* 交换a和b的值 */ #include <stdio.h> #include <stdlib.h> int swap(int A,int B) { int tmp; tmp=A; A=B; B=tmp; return 0; } int main() { int a=3,b=5; printf("a=%d,b=%d\n交换后的a和b的值分别为:\n",a,b); swap(a,b); printf("a=%d, b=%d\n",a,b); system("pause"); return 0; }
可见:不会报错,但功能不正确!!!
/* 交换a和b的值 */ /* 修改后的:传地址调用 */ #include <stdio.h> #include <stdlib.h> int swap(int *A,int *B) { int tmp; tmp=*A; *A=*B; *B=tmp; return 0; } int main() { int a=3,b=5; printf("a=%d,b=%d\n交换后的a和b的值分别为:\n",a,b); swap(&a,&b); printf("a=%d, b=%d\n",a,b); system("pause"); return 0; }
我的问题:
依旧是实参和形参用的相同的字母,这样会出错!!!修改之后程序运行正确!
请问:函数的参数可以传值,也可以传地址,什么时候传值,什么时候传地址???
答:函数间的参数传递,若是传值,则不会改变实参的值;若要改变实参的值,需用传地址调用!
函数与数组
例4:数组名相当于指针,因此通过数组名就可以进行双向传递,而不用加取地址符号。
/* 传递数组名可实现双向传递 */ #include <stdio.h> #include <stdlib.h> int sum(int *array,int n); int main() { int a[10]={1,2,3,4,5,6,7,8,9,10},s; s=sum(a,10); // 10是数组元素个数,也可以不传递元素个数,但要注意不要越界了! printf("%d\n",s); system("pause"); return 0; } int sum(int *array,int n) { int sum=0; for(int i=0; i<n; i++) { sum+=*array; array++; } return sum; }
请问:在函数内部修改元素,是否在main函数中也可以修改?
答:下面测试程序。只要是传地址,无论是单个元素的地址还是数组的首地址,都是双向传递。
/* 测试程序(是否是双向传递):把数组元素大于5的元素置为0 */ #include <stdio.h> #include <stdlib.h> int revise(int *array,int n, int x); // 函数的参数定义不可一起定义! int main() { int a[10]={1,2,3,4,5,6,7,8,9,10},s; s=revise(a,10,5); // 数组元素,元素个数,大于5 printf("%d\n",s); system("pause"); return 0; } int revise(int *array,int n, int x) { int sum=0; for(int i=0;i<n;i++) { if(*array>x) { *array=0; ++sum; } array++; } return sum; // 函数返回值:返回大于5的元素个数 }
用数组名做函数参数应注意:
(1)形参数组和实参数组的类型必须一致,否则将会引起错误。
(2)形参数组和实参数组的长度可以不同,因为在调用时,只传送首地址而不检查形参数组的长度。当形参数组的长度与实参数组不一致时,虽不至于出现语法错误,但程序运行结果将与实际不符,应当注意!
(3)在函数形参表中,允许不给出形参数组的长度,或用一个变量来表示数组元素的个数。
(4)多维数组也可以作为函数的参数,在函数定义时对形参数组可以指定每一维的长度,也可省去第一维的长度。
函数的返回值
函数的输出是通过指针间接修改调用函数中的变量空间达到输出的目的。
函数返回值本来就是输出。
请问:什么时候需要返回指针?
答:函数内部数据是地址,需要传递给调用函数,返回的当然是指针了!当然,这个返回地址也可以通过函数参数返回!比如:函数内动态分配空间或函数内部处理后获得的地址可以通过函数返回值获得。或者或者进行了一系列操作之后,获得地址可用函数返回值获得。
一般形式:
类型定义符 *指针型函数名(形参列表)
{
函数体
}
“类型定义符”是指返回指针指向的数据类型。
例5:实现匹配函数match:程序在输入字符串中查找一个给定的字符,如果找到,则从该字符开始打印余下的子字符串,及该字符是字符串的第几个字符;否则输出“no match found”。
分析:
match函数:两个输入参数(字符串,字符)
功能:上面题目要求的功能
问题:函数的输出是什么???打印这个功能是否应该在match函数内部完成???输入转换为输出的算法是什么?输出==打印???函数输出==在函数内部打印???
(实际上,函数的输出是把计算结果反馈给这个函数的调用者,调用者是把结果输出到屏幕显示还是作为其他函数的输入,是由调用者根据需要来决定。函数的输出一般是通过函数的返回值或者具备双向传递性质的参数来获得)。
由主调函数决定是否找到!
请问:match函数原型:输出位置?输出字符串?或者输出找到找不到?
答:输出两种结果(找到、找不到)。
2个输入参数:char *sp, char ch
1个输出参数:指向找到的位置或NULL
char *match(char c,char *sp)
/* 实现匹配函数match:程序在输入字符串中查找一个给定的字符 */ #include <stdio.h> #include <stdlib.h> #include <string.h> // strlen()函数的头文件!若没有,则会报错:strlen()未定义。 char *match(char *string,char c); // 函数声明 char *match(char *string,char c) // 定义指针型函数,因为后面是p=match,p是指针字符串,因此match函数也应该是指针型字符型函数。 { int count=0; while((c!=string[count])&&(c!='\0')) count++; if(string[count]) return(&string[count]); // 调用时是p=match,因此return的值就给了p! // 匹配后的字符串首地址给了指针p,因此主函数中获得的p就是匹配后的字符串!*p是字符ch! else return 0; } int main() // 主函数作用:定义、获取一些变量,然后调用函数,调用之后做决策/做出相应操作 { char s[100],ch,*p; // 字符型指针p:用来存放匹配后的字符串。 int posit; gets(s); // 用gets()函数这样是可以的,gets_s(s)这样子会报错:调用参数太少,需要加字符串长度!gets()函数容易造成缓冲区溢出,因此常用gets_s()函数! ch=getchar(); // 获取一个字符 p=match(s,ch); // 函数调用,注意实参和形参不可以相同,相同容易出错! if(p) { posit=strlen(s)-strlen(p)+1; // strlen()函数用来求字符串长度 printf("匹配后的字符串为%s:\n位置为%d:\n",p,posit); } else printf("No match found!\n"); system("pause"); return 0; }
如上,请问:函数的返回值可以是地址,这个地址可不可以是局部变量呢?
答:动态内存分配那节有讲,不要把局部变量的地址作为函数值返回。下面我们测试一下。
说明:可以返回全局或静态变量的地址,但绝对不能返回局部变量的地址!
/* 测试程序:局部变量是否可以作为函数的返回值返回? */ #include <stdio.h> #include <stdlib.h> int *test(); // 声明test指针型函数返回值是整型地址 int *test() { int a=5; // 定义并初始化局部变量a return(&a); // 将局部变量的地址作为函数返回值返回 } int main() // 主函数作用:定义、获取一些变量,然后调用函数,调用之后做决策/做出相应操作 { int *t,*t2; // 定义整型指针用来测试 t=test(); // 调用test函数,将返回值存放在t指针中,因此t就指向局部变量a *t=10; // 将t所指空间的值修改为10 printf("t=%p\n*t=%d\n",t,*t); // 输出*t=10 t2=test(); // 再次调用时又定义了一个局部变量,这个局部变量的地址不一定和上一次局部变量的的地址相同 // t2所指空间的值为5 printf("t=%p\n*t=%d\n",t,*t); // t的值是一个地址,因此用%p输出 // 输出*t=5,说明再次调用时局部变量覆盖了另外空间的值,造成错误! system("pause"); return 0; }
疑问:第二次调用时返回值是给了t2,为什么*t的值也改了???
补充:
(1)实参和形参:(参考:点击此处查看)
实参:“实际参数”,调用时传递给函数的实际参数,进行函数调用时,实参必须是确定的值,因此在此之前必须对其进行赋值、输入。实参只能在主调函数中使用,在函数体内部不可用。
形参:“形式参数”,它是虚拟的,在函数声明、定义时以及函数体内使用的虚拟参数,目的是用来接收调用时的实际参数。调用函数时,实参将完全拷贝给形参。形参只能在函数体内使用,主调函数内不可用。形参可有可无,但括号不可省。形参定义要一个个定义,不能一起定义;参数定义要有参数类型和参数名。
注意:实参和形参的个数、类型、顺序必须一致!
(2)gets()函数和gets_s()函数:(参考:点击此处查看)
gets()函数:gets(string),函数不检查传入字符数组的长度,易造成缓冲区溢出。
gets_s()函数:gets_s(string,n),需要两个参数,第一个参数是数组名,第二个参数是数组最大元素个数(不能省,没有第二个参数会报错:调用的参数太少)。
gets()函数 / gets_s()函数和scanf_s()函数的区别:(参考:点击此处查看)
(3)字符串指针和字符指针: