1. 函数的概念
C语言的函数是⼀个完成某项特定的任务的⼀⼩段代码。这段代码是有特殊的写法和调⽤⽅法的。
C语⾔的程序其实是由⽆数个⼩的函数组合⽽成的,也可以说:⼀个⼤的计算任务可以分解成若⼲个较⼩的函数(对应较⼩的任务)完成。同时,⼀个函数如果能完成某项特定任务的话,这个函数也是可以复⽤的,提升了开发软件的效率。
在C语⾔中我们⼀般会⻅到两类函数:1、库函数;2、⾃定义函数
2. C语言中函数的分类
2.1 库函数
为什么会有库函数?
1、我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上( printf 函数)。
2、在编程的过程中我们会频繁的做一些字符串的拷贝工作( strcpy 函数)。
像上面我们描述的基础功能,它们不是业务性的代码,而且我们在开发的过程中每个程序员都可能用的到。C语言之所以有库函数,就是为了提供一些常用的功能和操作,以便开发人员能够更快速、高效地编写代码。库函数可以帮助我们完成一些常见的任务,例如输入输出、字符串处理、数学运算、内存管理等。它们是预先编写好的可重用代码片段,可以被开发者直接调用和使用,避免了重复编写相同功能的代码。通过使用库函数,开发者能够节省时间和精力,并提高代码的可读性和可维护性。此外,库函数还能够提供一些底层的系统功能和操作,使得开发者能够更加灵活地控制程序的行为。
注:库函数的使用必须包含#include 对应的头文件!!!
如何学习库函数?
这里推荐两个学习的网站:cppreference、cplusplus
这里我们以cplusplus 为例:
这是C的库,在左边的部分可以很快的找到我们曾经用过的函数。那么我将以 memset 函数为例,带你来学习库函数。
memset 这个函数包含在 (string.h) 这个头文件里面,这个函数需要一个指针(地址)、一个整形及一个无符号整型,返回类型为指针。
void* memset ( void* ptr, int value, size_t num );
该函数的功能为:
Fill block of memory: Sets the first num bytes of the block of memory pointed by ptr to the specified value (interpreted as an unsigned char).
翻译过来的意思是:
填充内存块:将 ptr 指向的内存块前 num 个字节设置为特定值(无符号字符)。
ptr: Pointer to the block of memory to fill.
(ptr:指向需要填充的内存)
value: Value to be set. The value is passed as an int, but the function fills the block of memory using the unsigned char conversion of this value.
(value:要设置的值。该值作为整型传递,但该函数使用此值的无符号字符填充内存块)
num:
Number of bytes to be set to the value.
size_t is an unsigned integral type.
(数字:要设置为该值的字节数。size_t 是无符号整型)
Return Value:
ptr is returned.
(返回值:ptr)
实例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "hello swust";
memset(arr+6, 'x', 5);
printf("%s\n", arr);
return 0;
}
运行结果:
2.2 自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
编程中更重要的是需要自己进行自定义函数。自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。
函数的组成:
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
实例:写一个函数可以交换两个整形变量的内容。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void exchange(int* pa, int* pb)
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:%d,%d\n", a, b);
exchange(&a, &b);
printf("交换前后:%d,%d\n", a, b);
return 0;
}
运行结果:
3. 函数的参数
在函数使⽤的过程中,把函数的参数分为:实际参数(实参)和形式参数(形参)。
例如下面这段代码:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x+y;
return z;
}
int main()
{
int a = 0;
int b = 0;
//输⼊
scanf("%d %d", &a, &b);
//调⽤加法函数,完成a和b的相加
//求和的结果放在r中
int r = Add(a, b);
//输出
printf("%d\n", r);
return 0;
}
3.1 实际参数(实参)
在上⾯代码中,第2~7⾏是 Add 函数的定义,有了函数后,再第17⾏调⽤ Add 函数的。我们把第17⾏调⽤Add函数时,传递给函数的参数 a 和 b ,称为实际参数,简称实参。实际参数就是真实传递给函数的参数。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参。
3.2 形式参数(形参)
在上⾯代码中,第2⾏定义函数的时候,在函数名Add 后的括号中写的 x 和 y ,称为形式参数,简
称形参。
为什么叫形式参数呢?实际上,如果只是定义了Add 函数,⽽不去调⽤的话, Add 函数的参数 x
和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在
函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。
3.3 实参和形参的关系
虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各⾃是独⽴的内存空
间。
在调试的可以观察到, x 和 y 确实得到了 a 和 b 的值,但是 x 和 y 的地址和 a 和 b 的地址是不⼀样的,所以我们可以理解为形参是实参的⼀份临时拷贝。
4. return 语句
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项。
1、return 后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式的结果。
2、return 后边也可以什么都没有,直接写 return 也可以不写。 这种写法适合函数返回类型是 void 的情况。
3、return 返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
4、return 语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
5、如果函数中存在 if 等分⽀的语句,则要保证每种情况下都有 return 返回,否则会出现编译错误。
5. 函数的调用
5.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
例如:写一个函数可以交换两个整形变量的内容。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Swap(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
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;
}
运行结果:
可以看出,把参数传进去, a、b 两值却没有发生交换。如何进行修改呢?接下来就不得不采用传址调用。
5.2 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Swap(int* px, int* py)
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
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;
}
运行结果:
实参中,将 a , b 的地址给传了过去,形参又用 px 和 py 两个指针存储了 a , b 的地址,接着用 * 操作符找到了 a , b 的地址并修改了里面的内容。
6. 函数的嵌套调用和链式访问
6.1 嵌套调用
嵌套调⽤就是函数之间的互相调⽤,每个函数就像⼀个乐⾼零件,正是因为多个乐⾼的零件互相⽆缝
的配合才能搭建出精美的乐⾼玩具,也正是因为函数之间有效的互相调⽤,最后写出来了相对⼤型的
程序。
假设我们计算某年某⽉有多少天?,如果要函数实现,可以设计2个函数:
1、is_leap_year():根据年份确定是否是闰年
2、get_days_of_month():调⽤is_leap_year确定是否是闰年后,再根据⽉计算这个⽉的天数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
int get_days_of_month(int y, int m)
{
int days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[m];
if (is_leap_year(y) && m == 2)
day += 1;
return day;
}
int main()
{
int y = 0;
int m = 0;
scanf("%d %d", &y, &m);
int d = get_days_of_month(y, m);
printf("%d\n", d);
return 0;
}
运行结果:
这⼀段代码,完成了⼀个独⽴的功能。代码中反应了不少的函数调⽤:
1、main 函数调⽤ scanf 、 printf 、 get_days_of_month。
2、get_days_of_month 函数调⽤ is_leap_year。
未来的稍微⼤⼀些代码都是函数之间的嵌套调⽤,但是函数不能嵌套定义。
6.2 链式访问
链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来。
比如下面这段代码:
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
运行结果:
这个为什么打印的结果是4321呢?关键在于明⽩ printf 函数的返回值。
printf函数的返回值是打印在屏幕上的字符的个数。
在上面的例子中,我们第⼀个printf打印的是第⼆个printf的返回值,第⼆个printf打印的是第三个
printf的返回值。
第三个printf打印43,在屏幕上打印2个字符,再返回2。
第⼆个printf打印2,在屏幕上打印1个字符,再返回1。
第⼀个printf打印1。
所以屏幕上最终打印:4321。
7. 函数的声明和定义
函数的声明:
顾名思义,就是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
函数的定义:
指函数的具体实现,交待函数的功能实现。
7.1 单个文件
⼀般我们在使⽤函数的时候,直接将函数写出来就使⽤了。
⽐如:我们要写⼀个函数判断⼀年是否是闰年。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//判断⼀年是不是闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if(r == 1)
{
printf("闰年\n");
}
else
{
printf("⾮闰年\n");
}
return 0;
}
上述代码中,下面这一部分为函数的定义。
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0)) || (y%400==0))
{
return 1;
}
else
{
return 0;
}
}
而下面这一段为函数的调用。
int r = is_leap_year(y);
这种场景下函数的定义在函数调⽤之前,没啥问题。
但是如果我们将函数的定义放在函数的调⽤后边:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if (r == 1)
{
printf("闰年\n");
}
else
{
printf("⾮闰年\n");
}
return 0;
}
int is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
这个代码在VS2013上编译,会出现下⾯的警告信息:
为什么会出现警告呢?
这是因为C语⾔编译器对源代码进⾏编译的时候,从第⼀⾏往下扫描的,当遇到第7⾏的is_leap_year
函数调⽤的时候,并没有发现前⾯有is_leap_year的定义,就报出了上述的警告。
把怎么解决这个问题呢?
就是函数调⽤之前先声明⼀下is_leap_year这个函数,声明函数只要交代清楚:函数名,函数的返回类型和函数的参数。如:int is_leap_year(int y);这就是函数声明,函数声明中参数只保留类型,省略掉名字也是可以的。代码变成这样就能正常编译了。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_leap_year(int y);
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if (r == 1)
{
printf("闰年\n");
}
else
{
printf("⾮闰年\n");
}
return 0;
}
int is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
函数的调⽤⼀定要满足:先声明后使⽤;
函数的定义也是⼀种特殊的声明,所以如果函数定义放在调⽤之前也是可以的。
7.2 多个文件
⼀般在企业中我们写代码时候,代码可能⽐较多,不会将所有的代码都放在⼀个⽂件中;我们往往会根据程序的功能,讲代码拆分放在多个⽂件中。
⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在原⽂件(.c)⽂件中。
比如:
add.c
//函数的定义
int Add(int x, int y)
{
return x+y;
}
add.h
//函数的声明
int Add(int x, int y);
test.c
#include <stdio.h>
#include "add.h"
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
运行结果:
有了函数的声明和函数定义的理解,我们写代码不仅更加⽅便,更重要的是能够使代码实现模块化,大大提升效率。