文章目录
-
- 1. 函数的概念
- 2. 库函数
-
- 2.1 标准库、库函数、头文件
- 2.2 库函数的使用方法
-
- 2.2.1 函数原型
- 2.2.2 功能
- 2.2.3 头文件包含
- 2.2.4 实践
- 2.2.5 库函数文档的一般格式
- 3. 自定义函数
-
- 3.1 函数的语法形式
- 3.2 函数的举例
- 4. 形参和实参
-
- 4.1 实参
- 4.2 形参
- 4.3 实参和形参的关系
- 5. return语句
- 6. 数组做函数参数
- 7. 嵌套调用和链式访问
-
- 7.1 嵌套调用
- 7.2 链式访问
- 8. 函数的声明和定义
-
- 8.1 单个文件
- 8.2 多个文件
- 8.3 static 和 extern
-
- 8.3.1 作用域和生命周期
- 8.3.2 static 和 extern 的用途
- 8.3.3 static 修饰局部变量
- 8.3.4 static 修饰全局变量
- 8.3.5 static 修饰函数
1. 函数的概念
数学中我们见过函数(如一次函数 y=kx+b),给任意 x 可得到对应 y 值。
C语言中的函数(又称子程序),是完成某项特定任务的小段代码,有特殊写法和调用规则。 C语言程序由无数个函数组合而成,即一个大的计算任务可分解为若干个小函数完成,函数的复用性能提升了软件的开发效率。
C语言中函数主要分为两类:
- 库函数
- 自定义函数
2. 库函数
2.1 标准库、库函数、头文件
C语言标准仅规定语法规则,不提供库函数。
国际标准ANSI C定义了常用函数标准,即标准库。
编译器厂商根据标准库实现的函数称为库函数。我们之前学习的printf、scanf均为库函数,库函数无需程序员自行实现,直接学习使用即可,其质量和执行效率更有保障。
库函数按功能划分,声明在不同头文件中。
库函数相关头文件参考:https://zh.cppreference.com/w/c/header,涵盖数学、字符串、日期等各类功能,无需一次性全部掌握,可逐步学习。
2.2 库函数的使用方法
库函数的学习和查看工具丰富,常用的有:
- C/C++官方链接:https://zh.cppreference.com/w/c/header
- 中文版:https://cppreference.cn/w/
- cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/
以sqrt函数为例,详细说明库函数的使用:
2.2.1 函数原型
double sqrt (double x);
- sqrt:函数名
- x:函数参数,需传递一个double类型的值
- double:返回值类型,函数计算结果为double类型
2.2.2 功能
计算平方根,返回参数x的平方根。
2.2.3 头文件包含
库函数需包含对应头文件才能使用,sqrt函数的头文件为<math.h>。
2.2.4 实践
#include <stdio.h>
#include <math.h>
int main()
{
double d = 16.0;
double r = sqrt(d);
printf("%lf\\n", r);
return 0;
}
运行结果:
4.000000
2.2.5 库函数文档的一般格式
3. 自定义函数
自定义函数能给程序员更多创造性,是编程中更为重要的部分。
3.1 函数的语法形式
自定义函数与库函数形式一致,语法如下:
ret_type fun_name(形式参数)
{
// 函数体
}
- ret_type:函数返回类型,若无需返回值则为void
- fun_name:函数名,需根据功能起有意义的名称
- 形式参数:可以无参数(void),有参数需说明参数个数、每个参数的类型、形参的名字
- 函数体:用{}括起,是实现函数功能的核心代码
可将函数类比为小型加工厂:输入原材料(形式参数),经加工(函数体代码),产出产品(返回值)。 
3.2 函数的举例
需求:写一个加法函数,完成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;
}
简化版本
int Add(int x, int y)
{
return x + y;
}
- 函数的参数部分要交代清楚:参数个数、每个参数的类型、形参的名字。
- 函数的参数、返回类型、函数名可根据实际需求灵活设计。
4. 形参和实参
在函数使用过程中,参数分为实际参数(实参)和形式参数(形参)。
4.1 实参
调用函数时传递给函数的参数称为实参(实际参数)。例如上述加法函数中,main函数里调用Add(a, b)时,a和b就是实参,是真实传递给函数的值。
4.2 形参
定义函数时,函数名后括号中的参数称为形参(形式参数)。例如加法函数Add(int x, int y)中的x和y,仅在函数被调用时才会申请内存空间(即形参实例化),用于存放实参传递的值;若函数未被调用,形参不会真实存在。
4.3 实参和形参的关系
实参是传递给形参的值,但二者是独立的内存空间。
通过调试该代码可以看到,形参x和y确实得到了实参a和b的值,但是x和y的地址和a和b的地址是不同的,即形参是实参的一份临时拷贝。
5. return语句
return语句是函数中重要的关键字,使用注意事项如下:
6. 数组做函数参数
当在函数中需要操作数组时,要将数组作为参数传递给函数。
数组传参的重点:
示例:写一个函数将一个整型数组的内容全部置为-1,再写一个函数打印数组的内容。
#include <stdio.h>
// 函数set_arr:将整型数组所有元素置为-1
void set_arr(int arr[], int sz)
{
int i = 0;
for(i = 0; i < sz; i++)
{
arr[i] = –1;
}
}
// 函数print_arr:打印数组内容
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);
print_arr(arr, sz);
return 0;
}
7. 嵌套调用和链式访问
7.1 嵌套调用
函数之间的互相调用称为嵌套调用,就像乐高零件配合搭建出复杂模型,函数通过嵌套调用能实现大型程序的功能。
示例:计算某年某月的天数
#include <stdio.h>
// 函数is_leap_year:判断是否为闰年
int is_leap_year(int y)
{
if(((y%4==0)&&(y%100!=0))||(y%400==0))
return 1;
else
return 0;
}
// 函数get_days_of_month:计算某月的天数
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];
// 嵌套调用is_leap_year函数
if (is_leap_year(y) && m == 2)
day += 1; // 闰年2月加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;
}
注意:函数不能嵌套定义,只能嵌套调用。
7.2 链式访问
将一个函数的返回值作为另一个函数的参数,像链条一样串起函数,称为链式访问。
示例1:简化字符串长度打印
#include <stdio.h>
#include <string.h>
int main()
{
// 链式访问:strlen的返回值作为printf的参数
printf("%zu\\n", strlen("abcdef"));
return 0;
}
示例2:多层printf链式调用
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
8. 函数的声明和定义
8.1 单个文件
一般我们在使用函数的时候,将函数定义写在函数调用之前。 示例:写一个函数判断一年是否是闰年。
#include <stido.h>
// 函数定义
// 函数is_leap_year:判断一年是不是闰年
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;
}
这种场景下编译没有出现问题。
那如果我们希望将函数定义放在函数调用后,如下:
#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;
}
代码在VS2022上编译,会出现下面的警告信息。
这是因为C语言编译器对源代码进行编译的时候,从第1行往下扫描的,当遇到第8行is_leap_year函数调用的时候,并没有发现前面有is_leap_year的函数定义。
解决方法:在调用函数之前声明函数。函数声明需交代清楚返回类型、函数名和参数类型,语法为ret_type fun_name(参数类型 参数名),参数名可省略。
#include <stdio.h>
// 函数声明
int is_leap_year(int y); //参数名y可省略:int is_leap_year(int)
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;
}
规则:函数调用需满足“先声明后使用”,函数定义本身也是一种特殊声明,因此定义在调用之前可无需额外声明。
8.2 多个文件
企业开发中,代码常按功能拆分到多个文件,一般规则如下:
- 头文件(.h):存放函数声明、类型声明;
- 源文件(.c):存放函数实现。
示例:多文件实现加法函数 add.h
// 函数声明
int Add(int x, int y);
add.c
// 函数定义
int Add(int x, int y)
{
return x + 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;
}
运行结果: 
8.3 static 和 extern
8.3.1 作用域和生命周期
作用域:变量/函数的可用代码范围。
生命周期:变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。
8.3.2 static 和 extern 的用途
static和extern都是C语言中的关键字。
static 可以用来:
- 修饰局部变量
- 修饰全局变量
- 修饰函数。
extern 是用来声明外部符号的,如果一个全局的符号在A文件中定义的,在B文件中想使用,就可以使用 extern 进行声明,然后使用。
8.3.3 static 修饰局部变量
对比代码1和代码2,理解 static 修饰局部变量的意义。
代码1(无static):
#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(有static):
#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
代码1test函数中的局部变量i是每次进入test函数会创建变量(生命周期开始,申请内存)并赋值为0,然后++,再打印,出函数的时候销毁变量(生命周期结束,收回内存)。 代码2test函数中的i创建好后,被static修饰后存储在静态区,生命周期与程序一致,出函数的时候是不会销毁的,重新进入函数也就不会重新创建变量,直接上次累积的数值继续计算。 结论:static修饰局部变量改变了变量的生命周期(从“局部作用域”变为“程序运行期间”),本质是改变了变量的存储类型(从内存的“栈区”存储到了“静态区”),存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样了,只有程序结束,变量才销毁,内存才回收。但作用域不变的。
使用建议:若变量出函数后需保留值,下次进入函数继续使用,可使用static修饰。
8.3.4 static 修饰全局变量
对比代码1和代码2,理解 static 修饰全局变量的意义。
代码1(无static):
add.c
int g_val = 20; // 定义全局变量
test.c
#include <stdio.h>
extern int g_val; // 声明外部全局变量
int main()
{
printf("%d\\n", g_val);
return 0;
}
输出结果:
20
代码2(有static):
add.c
static int g_val = 20; // static修饰全局变量
test.c
#include <stdio.h>
extern int g_val; // 声明外部全局变量
int main()
{
printf("%d\\n", g_val);
return 0;
}
编译结果: 
代码1正常运行,全局变量默认具有“外部链接属性”,只要适当声明就可跨文件使用。 代码2编译报错,因为全局变量变为了“内部链接属性”,仅文件内部可访问。 结论:static修饰全局变量改变了变量的访问权限(从“可跨文件可访问”变为“仅文件内部可访问”),本质是改变了变量的连接属性(从“外部链接属性”变为“内部链接属性”),变量变为 “内部链接属性”就只能在自己所在的源文件内部使用了,其他源文件即使声明了也无法正常使用。
使用建议:如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用 static 修饰。
8.3.5 static 修饰函数
对比代码1和代码2,理解 static 修饰函数的意义。 代码1(无static): add.c
int Add(int x, int y)
{
return x+y;
}
test.c
#include <stdio.h>
extern int Add(int x, int y); // 声明外部函数
int main()
{
printf("%d\\n", Add(2, 3));
return 0;
}
输出结果:
5
代码2(有static): add.c
static int Add(int x, int y) //static修饰函数
{
return x+y;
}
test.c
#include <stdio.h>
extern int Add(int x, int y); // 声明外部函数
int main()
{
printf("%d\\n", Add(2, 3));
return 0;
}
编译结果: 
代码1正常运行,函数默认具有“外部链接属性”,只要适当声明就可跨文件使用。 代码2编译报错,因为函数 变为了“内部链接属性”,仅文件内部可访问。 结论:static修饰函数后,其“外部链接属性”变为“内部链接属性”,只能在本源文件内使用,其他文件无法调用。 使用建议:若函数仅需在所在源文件内部使用,不想被其他文件调用,可使用static修饰。
网硕互联帮助中心





评论前必须登录!
注册