文章目录
0、总结
1、概念
函数:是一个自带声明和语句的一小段代码。
可以利用函数把程序划分成小块,便于人们理解和修改程序。函数可以复用,提升开发软件的效率。
在C语言中,一般会见到两类函数:
- 库函数
- 自定义函数
2、库函数
标准库:C语言的国际标准ANSI C规定了一些常用函数的标准。
库函数:不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。
注意,C语言标准中规定了C语言的各种语法规则,并不提供库函数。
查阅库函数文档链接:
-
1、C/C++官方链接:C 标准库头文件 - cppreference.com
例1:查阅sqrt
// 查阅文档可知以下:
// sqrt 是函数名字
// x 是函数的参数,表示调用sqrt函数需要传递一个double类型的值
// double 是返回值类型,表示函数计算的结果是double类型的值
double sqrt (double x);
库函数文档的一般格式如下:
- 1、函数原型
- 2、函数功能介绍
- 3、参数和返回类型说明
- 4、代码举例
- 5、代码输出
- 6、相关知识链接
3、自定义函数
1、语法形式
自定义函数和库函数是一样的,语法如下:
ret_type fun_name(形式参数)
{
}
ret_type
:函数返回类型fun_name
:函数名- 括号中放的是:形式参数
- {}括起来的是:函数体
可以把函数想象为一个工厂,工厂需要输入各种原材料,经过工厂加工输出产品,那么函数也是一样的,函数一般会输入一些值(可以是0个,也可以是多个),经过函数的计算,得出输出的结果。
例1:写一个加法函数,完成2个整型变量的加法操作。
#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);
int r = Add(a, b);
printf("%d\n", r);
return 0;
}
输入:
3 4
输出:
7
Add函数可以简化为:
int Add(int x, int y)
{
return x + y;
}
2、形参和实参
函数使用的过程中,函数的参数可分为形参和实参。
分析代码:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = Add(a, b);
printf("%d\n", r);
return 0;
}
在上面代码中,第2~5行是Add
函数的定义,有了函数后,在第12行调用Add
函数。
实参(实际参数):在第12行调用Add
函数时,传递给函数的参数a
和b
。
实际参数就是真实传递给函数的参数。
形参(形式参数):在第2行定义函数时,在函数名Add
后的括号中写的x
和y
。
如果只定义
Add
函数而不去调用,Add
函数的参数x
和y
只是形式上存在的,不会向内存申请空间,所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程称之为形参的实例化。
3、形参和实参的关系
观察代码的调试:
可以总结:
- 形参和实参是完全不同的内存空间
- 形参是实参的一份临时拷贝
- 形参的修改不会影响实参
4、return语句
在函数的设计中,经常出现return
语句,注意事项如下:
return
后边可以是一个数值,也可以是一个表达式,如果是表达式先执行表达式,再返回表达式的结果。return
后边可以什么都没有,直接写return;
,这种写法适合函数返回类型void
的情况。return
返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。return
语句执行后,函数就彻底返回,后边的代码不再执行。- 如果函数中存在
if
等分支的语句,则要保证每种情况下都有return
返回,否则会出现编译错误。
5、数组做函数参数
在使用函数时,有时会遇见数组作为参数传递给函数,然后在函数内对数组进行操作。
例1:写一个函数对一个整型数组的内容全部设置为-1,再写一个函数打印数组的内容。
#include <stdio.h>
void set_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
arr[i] = -1;
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
set_arr(arr, sz); // 设置数组内容为-1
print_arr(arr, sz);// 打印数组内容
return 0;
}
输出:
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1
问题:为什么要对函数传数组长度,不能在函数内求数组长度吗?
#include <stdio.h>
void print_arr(int arr[])
{
int z = sizeof(arr) / sizeof(arr[0]);
printf("%d\n",z);
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr(arr);
return 0;
}
输出:
2
从代码的结果来看,函数内求不出数组的长度。
这是因为在
arr
定义为int [10]
类型时,它明确表示了一个包含10个整数的数组,当这个数组作为参数传递给函数时,类型发生了变化,不再是原始的数组类型,而是退化为了int *
类型,即一个指向int
类型数据的指针。这种类型退化意味着,在函数内部接收到的参数仅仅是一个指向数组首元素的指针,而不再包含关于原数组大小(即10个元素)的信息,函数内部无法通过指针来推断求原始数组的长度。
结果为2,是
sizeof(int*) / sizeof(int) = 8 / 4 = 2
。注:一个指针的大小,在64位是8字节,在32位是4字节。(1字节 = 8位)
以上总结:
-
函数的形参和函数的实参个数匹配。
-
函数的实参是数组,形参也可以写成数组形式的。
-
形参如果是一维数组,数组大小可以省略不写。
-
形参如果是二维数组,行可以省略,列不能省略。
-
数组传参,形参不会创建新的数组。
-
形参操作的数组和实参的数组是同一个数组。
6、嵌套调用
嵌套调用:函数之间的互相调用。
例1:计算某年某月有多少天?用函数实现。
// is_leap_year() 根据年份确定是否是闰年
// get_days_of_month() 根据月计算这个月的天数
#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;
}
输入:
2024 2
输出:
29
以上代码,main
函数调用了scanf
、printf
、get_days_of_month
,get_days_of_month
函数调用is_leap_year
。
注意,在一个函数的内部不能直接定义另一个函数,即函数不能嵌套定义。
7、链式访问
链式访问:将一个函数的返回值作为另一个函数的参数,像链条一样将函数串起来。
例1:
#include <stdio.h>
int main()
{
printf("%d\n", strlen("abcdef")); // 链式访问
return 0;
}
输出:
6
例2:
#include <stdio.h>
int main()
{
// printf函数返回的是打印在屏幕上的字符的个数。
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
输出:
4321
4、函数的声明和定义
1、单个文件
例1:写一个函数判断一年是否是闰年。
#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;
}
输入:
2023
输出:
非闰年
以上代码,第2行~第8行是函数的定义,第13行是函数的调用。
如果把函数的定义放在函数的调用后边,如下:
#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;
}
输入:
2024
输出:
闰年
-
第2行代码是函数的声明。声明中交代清楚:函数名、函数的返回类型、函数的参数。
-
函数声明中参数可以只保留类型,省略掉名字。
-
函数的定义也是一种特殊的声明,如果函数定义放在调用之前也是可以的。
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 c = Add(a, b);
printf("%d\n", c);
return 0;
}
输出:
30
此外,多文件书写不仅逻辑清晰,还能方便多人协同。
例如,开发一个计算器,找四个人分别负责加法功能、减法功能、乘法功能、除法功能。如图:
3、static和extern
static
和extern
都是C语言中的关键词。
1、static
是静态的意思,可以用来:
- 修饰局部变量
- 修饰全局变量
- 修饰函数
2、extern
是用来声明外部符号的。
讲解static
和extern
之前,有必要先讲作用域和生命周期。
1、作用域(scope):限定这个名字的可用性的代码范围。
- 局部变量的作用域是变量所在的局部范围。
- 全局变量的作用域是整个工程(项目)。
2、生命周期:变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。
- 局部变量的生命周期:进入作用域变量创建,生命周期开始,出作用域生命周期结束。
- 全局变量的生命周期:整个程序的生命周期。
例1:static修饰局部变量:
// 代码1
#include <stdio.h>
void test()
{
int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
test();
}
return 0;
}
输出:
1 1 1 1 1
// 代码2
#include <stdio.h>
void test()
{
// static修饰局部变量
static int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
test();
}
return 0;
}
输出:
1 2 3 4 5
通过以上代码,发现static
修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,原先局部变量是存储在内存的栈区,经过static
修饰后存储到静态区。存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样,只有程序结束,变量才销毁,内存才回收。但是作用域不变的。
例2:static修饰全局变量
代码1:
add.c
int g_val = 2018;
test.c
// 代码1
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
输出:
2018
代码2:
add.c
static int g_val = 2018;
test.c
// 代码2
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
运行错误
通过以上代码,一个全局变量被static
修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
因为全局变量默认是具有外部链接属性,在外部的文件想使用,只要适当的声明就可以使用。但全局变量被static
修饰后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用。其他源文件即使声明,也无法正常使用。
注:extern
是用来声明外部符号的,如果一个全局的符号在A文件中定义,在B文件中想使用,就需要extern
进行声明,然后使用。
例3:static修饰函数
代码1:
add.c
int Add(int x, int y)
{
return x + y;
}
test.c
// 代码1
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
输出:
5
代码2:
add.c
static int Add(int x, int y)
{
return x + y;
}
test.c
// 代码2
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
运行错误
通过以上代码,发现static
修饰函数和static
修饰全局变量是一模一样的。原因是因为函数默认是具有外部链接属性,使得函数在整个工程中只要适当的声明就可以使用。但函数被static修饰后就变成了内部链接属性,使得函数只能在自己所在的源文件内部使用。
完。