函数(function),用于完成某项特定任务的一小段代码,也称之为“子程序”。C语⾔中提供了两种函数:库函数和自定义函数。
一、库函数
(1)标准库与头文件
C语言标准规定了C语言的各种语法规则,约定了一些常用函数的标准,组成了标准库。
这些约定的函数标准C语言本身是没有去实现的,是由不同的编译器厂商根据ANSI提供的C语言标准在编译器中提供这些函数的具体实现,这些函数就被称为库函数。如printf、scanf、strlen、sqrt函数等。
这些库函数根据功能的划分,都在不同的头文件中进行了声明。即在使用库函数时,需要包含对应的头文件。
库函数的相关头文件:https://zh.cppreference.com/w/c/header
(2)学习和查看工具
C/C++官⽅的链接:https://legacy.cplusplus.com/reference/clibrary/
(3)实例分析
#include <stdio.h> /* printf 所需的头文件 */
#include <math.h> /* sqrt 所需的头文件 */
int main ()
{
double param, result;
param = 1024.0;
result = sqrt (param); // sqrt函数, 开方
printf ("%f\n", result ); // 输出:32.000000
return 0;
}
二、自定义函数
(1)创建语法及使用
由程序员按照一定语法规则自己创建,实现某一特定功能的函数。语法规则如下:
ret_type fun_name(形式参数)
{
// 函数体
}
ret_type:用来表示函数返回结果的类型,返回结果可以为空,返回类型为void
fun_name:用来表示函数的名称,即函数名
( ):小括号内部放入形式参数(形参),可以为空
{ }:大括号内部放入函数体,实现该函数功能的具体计算
例:自定义一个加法函数,实现两个整型变量的相加
#include <stdio.h>
// 自定义Add函数,用于计算两个整数的相加
int Add(int x, int y) // int 返回结果为整型;Add 函数名; x,y为形式上的参数,简称 形参
{
return x+y; // 返回计算结果,为整型
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b); // 输⼊两个整数
// 调⽤Add函数,完成a和b的相加,并将返回结果赋值给 r
int r = Add(a, b); // a,b为真实传递给Add函数的参数,为实际参数,简称 实参
printf("%d\n", r);
return 0;
}
(2)形参与实参
实参:真实传递给函数的参数,位于函数调用时小括号内的参数,如上述代码中的int r=Add(a,b),这里的a,b即为真实传递给Add函数的参数,为实际参数,简称实参。
形参:形式上的参数,位于函数定义时小括号内的参数,如上述代码中的int Add(int x,int y),Add 函数的参数x和y只是形式上存在的,不会向内存申请空间,不会真实存在的,为形式参数,简称形参。形参只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。
关系:形参和实参都有自己独立的内存空间,即它们的地址是不相同的,在参数传递过程中,只是将值进行了传递,其地址是不变的。
(3)return 语句
在设计函数时,会经常使用到return语句,表示返回的意思。
- return 语句后可为⼀个数值,也可为⼀个表达式,若为表达式则先执行表达式,再返回表达式的结果,其后也可什么都没有,适用函数返回类型是void的情况。
return 10; // 数值
return a+b; // 表达式
return; // 函数返回类型为 void
- return 返回的值类型和函数返回的值类型不⼀致,系则统会自动将返回的值隐式转换为函数的返回类型。
int test(){
return 3.14; // return 返回一个浮点数,而函数返回类型为整型,则会将 3.14 强制转换为 3
}
- return 语句执行后,函数就彻底返回,后边的代码不再执行。
- 若函数中存在 if 等分支句,则要保证每种情况下都有return返回,否则会出现编译错误。
int test(){
int a = 0;
if(a==1)
return 1; // 当a不等于1的时候,函数就不知道返回什么,出现报错
}
- C语言中,return语句只能返回一个值,不能返回多个值,如数组不可返回,但Java语言可以。
(4)数组传参
在设计函数时,有时会将数组作为参数传递给函数,在函数内部对数组进行操作。
如:设计两个函数,⼀个将整型数组的内容全部置为-1;⼀个打印重置后数组的内容。
一维数组的使用:
#include <stdio.h>
// 重置函数
void set_arr(int arr[], int len)
{
for (int i = 0; i < len; i++) {
arr[i] = -1;
}
}
// 打印函数
void print_arr(int arr[], int len)
{
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
// 打印函数
print_arr(arr, len);
// 重置函数
set_arr(arr, len);
// 打印函数
print_arr(arr, len);
return 0;
}
二维数组的使用:
#include <stdio.h>
// 重置函数
void set_arr(int arr[2][3], int r, int c)
{
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
arr[i][j] = -1;
}
}
}
// 打印函数
void print_arr(int arr[2][3], int r, int c)
{
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
printf("%d ",arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[2][3] = {1,2,3,4};
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
// 打印函数
print_arr(arr, 2, 3);
printf("-------------------\n"); // 分割符
// 重置函数
set_arr(arr, 2, 3);
// 打印函数
print_arr(arr, 2, 3);
return 0;
}
注意事项:
- 函数的形参和实参个数要匹配,即个数相同
- 函数的实参是数组,形参也是可以写成数组形式的
- 形参若为⼀维数组,数组大小可省略不写;若为⼆维数组,行可省略,但列不能省略
- 数组传参,形参是不会创建新的数组,且形参操作的数组和实参的数组是同⼀个数组
(5)嵌套调用与链式访问
嵌套调用,即函数之间的互相调用,可以在函数内部调用函数。
如:假设我们计算某年某月有多少天?,若要函数实现,可以设计2个函数:
- is_leap_year():根据年份确定是否是闰年
- get_days():调用is_leap_year确定是否是闰年后,再根据月份计算这个月的天数
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
// 判断闰年
int is_leap_year(int y)
{
if ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)
{
return 1; // 是闰年,返回 1
}
else
{
return 0; // 不是闰年,返回 0
}
}
// 计算天数
/* 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月
* 闰年: 31 29 31 30 31 30 31 31 30 31 30 31
* 非闰年: 31 28 31 30 31 30 31 31 30 31 30 31
*/
int get_days(int y,int m)
{
int days[13]= { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[m];
if (is_leap_year(y) && m == 2) // 调用is_leap_year()函数判断是否为闰年
day += 1; // 为闰年,且为2月时天数加 1
return day;
}
int main()
{
int year = 0;
int month = 0;
printf("请输入年份与月份:");
scanf("%d %d", &year,&month);
printf("%d 年 %d 月有 %d 天!\n", year, month, get_days(year, month));
return 0;
}
链式访问,即将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。如:
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d\n", strlen("abc")); // strlen()函数,返回字符串长度; 输出结果: 3
return 0;
}
#include <stdio.h>
int main()
{
// printf()函数成功打印后返回屏幕上的字符个数
printf("%d", printf("%d", printf("%d", 43))); // 输出结果: 4321
return 0;
}
注:printf函数返回的是成功打印在屏幕上的字符个数。
printf(“%d”, 43),返回值为 2;printf(“%d”, printf(“%d”, 43)),返回值为 1;
故printf(“%d”, printf(“%d”, printf(“%d”, 43))),在屏幕上输出结果为 4321。
三、函数的声明与定义
在C语言中,对于变量,需要先声明再使用;同理,对于函数,也需要先声明再使用。
对于单个文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", Add(a,b));
return 0;
}
int Add(int x, int y)
{
return x + y;
}
如,上述代码中,运行程序后会出现警告信息,但不影响程序执行的结果。之所以会出现警告信息,是因为编译器是从上往下扫描执行代码的,在调用Add函数时还未扫描到Add函数的定义(声明),就会出现警告,但最后扫描到Add函数后,程序的执行结果不会有影响。
解决警告信息方法:在main()函数前先声明函数,包括函数名,函数的返回类型和函数的参数。语法如下:
int Add(int x,int y); // 声明Add函数
注:函数的定义也是⼀种特殊的声明,若函数定义放在调用之前也是可以的,就不需要重复声明了。
对于多个文件:函数的声明、类型的声明放在头文件(.h)中,函数的实现放在源文件(.c)中。
源文件在调用自己写的函数时,需包含头文件,进行函数的声明,语法为
#include "Add.h" // 头文件放在双引号中
四、static 与 extern
static 和 extern 都是C语言中的关键字。
作用域(scope):是一个程序设计概念,通常来说,⼀段程序代码中所用到的名字并不总是有效(可用)的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
1.局部变量的作用域是变量所在的局部范围。即一个大括号{ }里定义的变量(局部变量)只能在该大括号{ }里(局部范围)使用,出了该大括号{ }就无法使用了。
2.全局变量的作用域是整个工程。即在整个项目代码中,全局变量都可以使用。
生命周期:指变量创建(申请内存)到变量销毁(收回内存)之间的⼀个时间段。
1.局部变量的生命周期:进入作用域生命周期开始,出作用域生命周期结束。
2.全局变量的生命周期:整个程序的生命周期。
extern是用来声明外部符号的,如果⼀个全局的符号是在A文件中定义的,在B文件中想使用,就可以使用extern进行声明,然后使用。
static是静态的的意思,可以修饰局部变量、全局变量、和函数。
static 修饰 局部变量:
两种代码的分析:
代码1:该test函数中的局部变量i是每次进入test函数先创建变量(生命周期开始)并赋值为0,然后++,再打印,出函数的时候变量生命周期就会结束(释放内存)。
代码2:test函数中的局部变量i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
结论:static 修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,局部变量本来是存储在内存的栈区的,但是被static修饰后存储到了内存的静态区,此时局部变量的生命周期就与程序的生命周期⼀样了,只有程序结束,变量才销毁,内存才回收,但作用域不变。
注:局部变量一般存储在内存的栈区,全局变量存储在内存的静态区。
static 修饰 全局变量:⼀个全局变量被static修饰,会使得这个全局变量就只能在本源文件内使用,不能在其他源⽂件内使用。本质原因是全局变量默认是具有外部链接属性的,在外部的⽂件中想使用,只要适当的声明就可以使用;但是全局变量被static修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源⽂件内部使用,其他源⽂件,即使声明了,也是无法正常使用。
static 修饰 函数:与修饰全局变量相似,一个函数本身在整个工程代码的所有文件中都可以使用,但被static修饰后的函数就只能在其所在的源文件内部使用,其它文件就不能使用该函数。本质是因为函数默认具有外部链接属性,使得函数在整个工程中只要适当的声明就可以被使用。但是被static修饰后变成了内部链接属性,使得函数只能在自己所在的源文件内部使用。
五、代码隐藏
⼀般在企业中写代码的时候,代码可能比较多,不会将所有的代码都放在⼀个⽂件中,往往会根据程序的功能,将代码拆分放在多个文件中。⼀般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现放在源文件(.c)中。
隐藏过程:右键项目名----->选择属性----->修改配置类型为静态库(.lib)----->点击应用,然后确定
原理:将头文件和源文件编译成静态库形式,静态库里为二进制编码,从而实现隐藏。
应用确定后,重新生成一下解决方案,就会在我们项目里的Debug文件中生成.lib的静态库文件了。
.lib的静态库文件默认在项目下的Debug文件中。
外人使用时,只需将静态库(.lib)和头文件给到他即可,无需展示源代码,从而保护自己的代码。
外人使用过程:
1.将所给的静态库(.lib)和头文件放到自己的项目文件中,复制粘贴即可
2.在自己的项目文件中,选择“头文件”,然后添加“现有项”,选择第一步粘贴进项目里的头文件即可
3.在源文件中引入头文件和静态库即可使用。
#include "Add.h" // 引入头文件
#pragma comment(lib,"add.lib") // 导入静态库
六、代码练习
一、实现一个函数is_prime,判断一个数是不是素数,并利用该函数打印100到200之间的素数。
二、实现一个函数判断year是不是闰年。
三、实现一个函数,打印乘法口诀表,口诀表的行数和列数自己指定。
四、创建一个整形数组,完成对数组的操作:1.实现函数init() 初始化数组为全0;2.实现print()打印数组的每个元素3.实现reverse() 函数完成数组元素的逆置。
参考代码:https://gitee.com/zuiltd/c-language/commit/e5f0b441bf6452982a1236938d59807bf07abe8e