在C语言中,我们拥有很重要的一个概念叫做函数的概念,在这一节中,我们将对函数有一个系统性的认识,了解函数的大致逻辑,如何使用等问题,让我们一起来了解下C语言中及其重要的概念,函数吧!
在这一章中我们主要目的是回答以下问题:
1. 函数是什么
2. 库函数
3. 自定义函数
4. 函数参数
5. 函数调用
6. 函数的嵌套调用和链式访问
7. 函数的声明和定义
函数是什么
总的来说,就是为实现特定功能而封装的一个代码块代码块,这体现了函数的解耦性。
C语言中,函数可以被分为两个大的组成部分
1.库函数
2.自定义函数
库函数:
库函数就是指那些每个程序员都可能用到的,C库给我们已经编好的一些通用基础函数,就不需要我们重新开发,库函数的存在大大的提高了程序的可移植性和开发效率
库函数的查阅可以在cplusplus.com中了解
常见的库函数:
值得注意的是,在我们使用库函数时,必须要在程序上端引入#include对应的头文件,才可以对其进行使用
库函数的使用举例:
1.字符拷贝 char * strcpy ( char * destination, const char * source );
define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<windows.h>
int main(){
char src[] = "hello world!\n";
char dst[32];//在栈上开辟临时变量,未初始化,一般为随机值
printf("before:%s", dst);
strcpy(dst, src);//库函数中的字符串拷贝,将后者拷贝到前者并赋给前者
printf("after:%s", dst);
system("pause");
return 0;
}
初始dst未定义,所以才会出现上述烫烫烫烫...但是因为没有碰到字符串结束标志:\0无法结束停止,又因为在栈上开辟出的两个数组空间相邻,所以在打印完烫烫烫烫...之后继续打印出了src的内容,碰到了\0才得以结束
2.字符大小写转化
3.memset函数 void * memset ( void * ptr, int value, size_t num );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<windows.h>
int main(){
char arr[5];
memset(arr, 1, sizeof(arr));//memset是纯内存操作,与类型无关,按字节将变量初始化为1,因为char为1字节,所以可以分别赋1
for (int i = 0; i < 5; i++){
printf("%d\n", arr[i]);
}
//********************************
int arr[5];
memset(arr, 1, sizeof(arr));//因为memset是纯内存操作,所以其对int整型操作并不是对int整体4字节的对象赋值
//而是对其在4个字节的比特位上分别依次赋1,得到了二进制为0000 0001 0000 0001 0000 0001 0000 0001这个数字
for (int i = 0; i < 5; i++){
printf("%d\n", arr[i]);
}
system("pause");
return 0;
}
补充:void是空类型大小为0字节,不能直接用来定义变量,因为类型不明确,无空间,但是在linux环境下占1字节,但也不能用以定义变量
但void*可以用来定义变量(因为void*是指针类型,类型明确,拥有4个字节),该类型可以用来接收任意类型,常用于接收指针类型
以上我们所调用的函数皆为库函数,无需自行定义,直接调用即可
自定义函数:
在我们了解库函数之后边有个问题,如果库函数可以解决所有问题,那么还需要我们程序员做什么呢,此时自定义函数的重要性便可体现出来了,自定义函数的存在就是为了解决那些库函数所解决不了的问题的,自定义函数相对于库函数更加重要,他主要是用过程序员自己来进行设计返回值,函数名,形参列表以及代码块来实现的,用以解决我们遇到的各种问题,给了我们程序员充分的发挥空间。
ret_type fun_name(para1, * )
{
statement;//语句项(可以为空)
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
这便是函数的基本结构,返回值,函数名,形参列表以及代码块
注意:在一个函数中,具备函数体(有花括号{})就叫做函数的定义,没有函数体则被称为函数的声明
函数的定义必须在使用之前,否则编译器无法识别
补充:多文件的使用:
注意观察,我们创建了三个不同的文件,一个test.h文件一个test.c,以及一个main.c文件,他们的作用分别不同,他们是我们多文件编程的基础,test.h中的主要作用是对需要使用的头文件,函数,宏等进行声明;test.c中则是完成了对于各个函数的定义,负责函数的实现,类似于自定义函数库的作用,而main.c中则是完成了对于函数的调用,主函数架构等作用,多文件编程也是解耦性的一部分体现
函数的参数
在函数的使用中,存在两种不同的参数,而它们的作用以及使用也不同,为实际参数(实参)和形式参数(形参)。
上面对于这两种参数的描述略有抽象,让我们来举两个很形象的例子,来认识一下形式参数与实际参数的区别吧
#include <stdio.h>
void Swap1(int x, int y) {
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int *px, int *py) {
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp; }
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);//1,2
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);//2,1
return 0; }
上面两个程序都是用来交换两个整数,为什么第一个却没有交换成功呢?
根据概念,我们可以认识到上面Swap1和Swap2函数中的参数 x,y,px,py 都是形式参数。在main函数中传给Swap1的num1,
num2和传给Swap2函数的&num1,&num2是实际参数。
事实上在调用Swap1时,编译器仅仅是在Swap1中开辟了两个临时变量,而这两个临时变量的值正好被赋为num1,num2,相当于传入函数中时,进行形参实例化的是num1,num2的一份拷贝而已,而并非num1,num2本身,这时当函数调用完成,函数退栈,刚刚完成交换的两个拷贝也就随着函数的销毁而销毁了,并没有真正完成交换,此时再进行打印时,编译器则会自动认为是main函数中初始化时的值,所以打印出来的就是原先的值(这种值传递方式也叫传值调用),总结起来就是,交换的是拷贝,而非实际的值。
而如何解决这个问题呢?
此时便出现了我们的第二种函数调用方式,将形参类型换为指针,进行交换时直接对指针,在地址层面上对两数进行交换,传入实际参数时直接换他们的地址位置,效果便变成了真正的对这两个数进行交换,此时调用函数,当函数销毁时已经完成了对地址的交换,便可完成需求
总结:在Swap2中,我们其实是通过指针解引用的方案,在函数内直接修改了函数外main函数中a,b的值
函数的调用:
这时我们通过上述例子便可以总结出函数的调用中出现的两种调用方式,传值调用与传值调用,需要对实参直接进行操作的使用传址调用,不需要则使用传值调用。
函数调用的关键就是不论是传值传递还是传址传递都是需要形成临时变量的,只是传址传参时候形成的是指针类型,指针变量也需要开辟空间形成临时变量的,而且只有当函数调用时才发生临时变量的形成
函数的嵌套调用与链式访问
我们学习了函数的基本概念之后我们会发现,函数是可以被调用的,那么这时便出现了函数的两种调用方案
1.嵌套调用
#include <stdio.h>
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; }
我们来观察一下上面的代码段,在我们的main函数执行时,调用three_line函数,而在three_line函数内部还调用了new_line函数,那么运行时便会将两个函数都执行,这种调用方式就叫做函数得嵌套调用,值得注意的是,这种调用顺序返回时也是最后被调用的返回倒数第二个,倒数第二个返回倒数第三个...直到返回到最后一个
2.链式访问
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"world"));//我们很明显的看到,在函数strlen中调用的strcat,
//将strcat函数的返回值作为strlen函数的形参传了进去
printf("%d\n", ret);//结果为10
return 0; }
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));//结果为4321
//在我们的printf函数中,返回值是int,是显示器上显示的字符串的长度(没有\0,但是\n占用了一个长度),所以上面的代码由内而外依次返回的是2,1
return 0;
}
我们可以发现,上面两段代码中,都存在调用一个函数时,在函数的形参列表中还有其他函数,这时我们便需要从右向左,从内而外依次实现函数,将内层函数的返回值作为外层函数的形参列表,从而一层一层的向外“剥开”,这种调用方式便是链式访问
补充:链式访问的顺序,同函数的形参实例化临时变量形成的顺序一样,都是从右向左依次实现的,例如:fun(int x,int y, int z)函数中,形成临时变量的顺序为,z,y,x
函数的定义与声明
函数的声明没有函数体,函数的定义拥有函数体,定义需要在声明之前