D5—C语言函数详解:从库函数到自定义函数的完整指南
本文系统介绍了C语言中函数的使用方法,包括库函数和自定义函数。重点讲解了函数定义、参数传递(形参与实参)、return语句、数组参数传递规则等核心概念。同时阐述了嵌套调用、链式访问等高级用法,以及多文件组织中的函数声明与定义。文章还详细解析了static和extern关键字对函数和变量的影响,包括作用域和生命周期的改变。最后总结了函数在C语言模块化编程中的关键作用,强调合理设计函数参数和返回值的重
目录
引言:理解函数的基本概念
函数是C语言程序的基本构建单元,就像乐高积木一样,通过函数的组合可以构建出复杂的程序。在C语言中,函数可以理解为完成特定任务的一小段代码,具有特殊的写法和调用方法。
函数的重要性:
-
模块化:将大任务分解为小函数
-
复用性:一次编写,多次调用
-
可维护性:便于调试和修改代码
C语言中主要有两类函数:库函数和自定义函数。
第一部分:库函数——C语言的标准工具
1.1 标准库和头文件
C语言标准规定了一些常用函数的标准,这些函数的实现由编译器厂商提供,组成了标准库。我们之前学习的 printf、scanf 就是库函数。
库函数特点:
-
已经实现好的函数
-
质量有保证
-
提升开发效率
-
需要包含对应的头文件
库函数相关头文件参考:https://zh.cppreference.com/w/c/header
1.2 库函数使用方法:以sqrt为例
学习库函数时,需要了解以下信息:
// sqrt函数原型
double sqrt(double x);
// sqrt 是函数名
// x 是函数的参数,需要传递一个double类型的值
// double 是返回值类型 - 函数计算结果
1.2.1 sqrt函数功能
-
计算平方根(Compute square root)
-
返回x的平方根(Returns the square root of x)
1.2.2 必须包含头文件
#include <stdio.h>
#include <math.h> // sqrt函数需要这个头文件
int main()
{
double d = 16.0;
double r = sqrt(d);
printf("%lf\n", r); // 输出:4.000000
return 0;
}
1.2.3 库函数文档格式
-
函数原型:函数声明
-
函数功能介绍:功能描述
-
参数和返回类型说明:详细说明
-
代码举例:使用示例
-
代码输出:预期结果
-
相关知识链接:进一步学习
第二部分:自定义函数——创造自己的工具
2.1 函数的语法形式
ret_type fun_name(形式参数)
{
// 函数体
}
函数组成要素:
-
ret_type:函数返回类型
-
fun_name:函数名
-
形式参数:函数的输入
-
函数体:实现功能的代码
函数比喻:

2.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); // a和b是实参
// 输出
printf("%d\n", r);
return 0;
}
简化版本:
int Add(int x, int y)
{
return x + y; // 直接返回表达式结果
}
第三部分:形参和实参——理解参数传递
3.1 实参(Actual Parameter)
在上面的Add函数示例中:
int r = Add(a, b); // 这里的a和b是实参
实参是实际传递给函数的参数。
3.2 形参(Formal Parameter)
int Add(int x, int y) // 这里的x和y是形参
{
return x + y;
}
形参是函数定义中的参数,只有当函数被调用时才会创建并分配内存,同时利用完后会直接销毁。
3.3 实参和形参的关系
重要结论:形参是实参的一份临时拷贝
通过调试可以观察到:
#include <stdio.h>
int Add(int x, int y) // x和y是形参
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int r = Add(a, b); // a和b是实参
// 调试观察:
// &a 和 &x 的地址不同
// &b 和 &y 的地址不同
// 证明它们在不同的内存空间
return 0;
}
关键点:
-
形参和实参各自是独立的内存空间
-
函数调用时,实参的值会拷贝给形参
-
修改形参不会影响实参(值传递)
第四部分:return语句——函数的出口
4.1 return语句的注意事项
-
返回数值或表达式:
return x + y; // 先计算表达式,再返回结果
-
void函数的return:
void PrintMessage()
{
printf("Hello\n");
return; // 可以省略
}
-
类型转换:
int Function()
{
return 3.14; // 自动转换为int类型,返回3
}
-
函数立即结束:
int FindValue(int arr[], int size, int target)
{
for(int i = 0; i < size; i++)
{
if(arr[i] == target)
return i; // 找到立即返回,后面代码不执行
}
return -1; // 未找到
}
-
分支语句必须有return:
int Absolute(int x)
{
if(x >= 0)
return x; // 分支1
else
return -x; // 分支2
// 所有分支都有return
}
第五部分:数组作为函数参数
5.1 数组参数传递的基础
#include <stdio.h>
// 设置数组所有元素为-1
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;
}
5.2 数组传参的重要规则
-
函数形参和实参个数必须匹配
-
形参可以写成数组形式:
void func(int arr[]) // 可以
void func(int *arr) // 也可以,等价
-
一维数组形参可以省略大小:
void func(int arr[]) // 正确
void func(int arr[10]) // 正确,但10会被忽略
-
二维数组形参,行可以省略,列不能省略:
void func(int arr[][5]) // 正确
void func(int arr[3][5]) // 正确,但3会被忽略
void func(int arr[][]) // 错误!列不能省略
-
数组传参的本质:
-
不会创建新的数组
-
形参操作的数组就是实参的数组
-
传递的是数组首元素的地址
第六部分:嵌套调用和链式访问
6.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 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;
}
函数调用关系:
-
main调用scanf、printf、get_days_of_month -
get_days_of_month调用is_leap_year
注意:函数可以嵌套调用,但不能嵌套定义。
6.2 链式访问:简洁的代码风格
链式访问是将一个函数的返回值作为另一个函数的参数。
传统方式:
#include <stdio.h>
#include <string.h>
int main()
{
int len = strlen("abcdef"); // 1. 获取长度
printf("%d\n", len); // 2. 打印长度
return 0;
}
链式访问:
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d\n", strlen("abcdef")); // 一步完成
return 0;
}
6.3 有趣的链式访问示例
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
输出结果:4321
解析过程:
-
printf("%d", 43)打印"43",返回2(打印了2个字符) -
printf("%d", 2)打印"2",返回1(打印了1个字符) -
printf("%d", 1)打印"1" -
最终屏幕显示:4321
关键:printf函数返回打印在屏幕上的字符个数。
第七部分:函数的声明和定义
7.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); // 直接调用
// ...
return 0;
}
情况2:函数定义在调用之后(需要声明)
#include <stdio.h>
// 函数声明
int is_leap_year(int y); // 分号结束
int main()
{
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y); // 调用
// ...
return 0;
}
// 函数定义
int is_leap_year(int y)
{
if(((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
函数声明的要点:
-
只需要函数名、返回类型、参数类型
-
参数名可以省略:
int is_leap_year(int); -
函数定义也是一种声明
-
必须遵循"先声明后使用"原则
编译错误示例:

这是因为编译器从第一行开始扫描,遇到函数调用时还没有看到函数定义。
7.2 多文件组织
在实际项目中,代码通常分布在多个文件中:
文件结构:
text
add.c - 函数定义 add.h - 函数声明 test.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); // 输出:30
return 0;
}
包含头文件的区别:
-
#include <stdio.h>:系统头文件,用尖括号 -
#include "add.h":自定义头文件,用双引号
第八部分:static和extern关键字
8.1 作用域和生命周期回顾
作用域(scope):名字的可用性范围
-
局部变量:所在局部范围
-
全局变量:整个工程
生命周期:变量创建到销毁的时间段
-
局部变量:进入作用域开始,出作用域结束
-
全局变量:整个程序的生命周期
8.2 static修饰局部变量
代码1:普通局部变量
#include <stdio.h>
void test()
{
int i = 0; // 每次调用都重新创建和初始化
i++;
printf("%d ", i);
}
int main()
{
for(int i = 0; i < 5; i++)
{
test(); // 输出:1 1 1 1 1
}
return 0;
}
代码2:static局部变量
#include <stdio.h>
void test()
{
static int i = 0; // 只初始化一次
i++;
printf("%d ", i);
}
int main()
{
for(int i = 0; i < 5; i++)
{
test(); // 输出:1 2 3 4 5
}
return 0;
}
static修饰局部变量的特点:
-
改变生命周期,不改变作用域
-
从栈区存储变为静态区存储
-
程序结束才销毁
使用场景:函数调用后需要保留值,供下次调用继续使用。
8.3 static修饰全局变量
代码1:普通全局变量
// add.c
int g_val = 2018; // 定义全局变量
// test.c
#include <stdio.h>
extern int g_val; // 声明外部全局变量
int main()
{
printf("%d\n", g_val); // 输出:2018
return 0;
}
代码2:static全局变量
// add.c
static int g_val = 2018; // static修饰
// test.c
#include <stdio.h>
extern int g_val; // 声明
int main()
{
printf("%d\n", g_val); // 链接错误!
return 0;
}
结论:
-
static修饰全局变量,使其只能在本源文件内使用
-
默认全局变量具有外部链接属性
-
static使其变为内部链接属性
使用建议:如果全局变量只想在所在源文件内部使用,用static修饰。
8.4 static修饰函数
代码1:普通函数
// 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)); // 输出:5
return 0;
}
代码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;
}
结论:
-
static修饰函数与修饰全局变量类似
-
使函数只能在本文件内部使用
-
函数默认具有外部链接属性,static使其变为内部链接属性
使用建议:如果函数只想在所在的源文件内部使用,用static修饰。
总结:函数的核心要点
1. 函数的基本概念
-
函数是完成特定任务的代码块
-
分为库函数和自定义函数
2. 函数定义和调用
-
语法:
ret_type fun_name(参数) { 函数体 } -
实参传递给形参是值传递
-
形参是实参的临时拷贝
3. 数组作为函数参数
-
传递的是数组首元素地址
-
形参数组和实参数组是同一个数组
-
需要传递数组大小参数
4. 作用域和存储期
-
static修饰局部变量:改变生命周期
-
static修饰全局变量/函数:限制作用域
-
extern声明外部符号
5. 工程化实践
-
多文件组织:.h声明,.c定义
-
函数声明先于使用
-
合理使用static实现模块封装
6. 编程技巧
-
嵌套调用构建复杂逻辑
-
链式访问简化代码
-
合理设计函数参数和返回值
函数是C语言模块化编程的基础,掌握函数的使用和设计原则,是成为合格程序员的关键一步。从简单的加法函数到复杂的多文件项目组织,函数贯穿了整个C语言编程的实践过程。
更多推荐
所有评论(0)