函数是C语言的灵魂,掌握它,你就掌握了模块化编程的钥匙。本文将从概念到实战,从语法到优化,带你彻底吃透C语言函数,写出更优雅、高效的代码。
你是否曾想过,为什么C语言程序总是由一个个函数组成?为什么printf
、scanf
可以直接调用而不用自己实现?为什么有时候修改函数参数的值,外部的变量却“无动于衷”?
函数是C语言中最基础也最重要的概念之一。它就像乐高积木,是构建程序的基本模块。无论是简单的加减乘除,还是复杂的系统调用,背后都离不开函数的设计与调用。
本文将带你系统学习C语言函数的方方面面,包括:
-
函数的基本概念与分类
-
库函数的使用与自定义函数的编写
-
形参与实参的区别与关系
-
数组作为函数参数的正确用法
-
嵌套调用与链式访问的技巧
-
使用
static
和extern
控制函数的作用域
每一部分都配有可运行的代码示例、详细注释和实战场景,无论你是初学者还是希望进阶的开发者,都能从中获益。
目录
1. 函数的概念
在数学中我们已经接触过函数的概念,比如一次函数 y=kx+b(k和b为常数),给定任意x值都能得到对应的y值。
这个概念在C语言中同样适用,称为"function",有时也译为"子程序"——这个译名更能准确体现其本质。C语言的函数实际上就是为完成特定任务而编写的一段独立代码,具有特定的语法结构和调用方式。
C语言程序本质上是由众多小函数组合构建而成的。换句话说,我们可以将复杂的大型计算任务分解为若干个相对简单的函数(对应子任务)来完成。这种模块化设计不仅使程序结构更清晰,还能实现函数复用,显著提升软件开发效率。
在C语言中,函数主要分为两大类:
- 库函数
- 自定义函数
2. 库函数
2.1 标准库和头文件
C语言标准规定了语法规则,但不包含库函数。ANSI C国际标准定义了一系列常用函数的标准,称为标准库。不同编译器厂商根据这个标准实现了这些函数,即库函数。
例如我们学过的printf、scanf都属于库函数。这些现成的函数可以直接调用,避免了重复开发常见功能,既提高了效率又保证了质量和性能。
标准库按功能将库函数分门别类,声明在不同的头文件中(如数学函数、字符串处理、日期操作等)。每个头文件包含相关函数和类型的定义。学习库函数不必急于求成,可以循序渐进地掌握。详细库函数信息可参考:C标准库头文件
2.2 库函数的使用方法
常用的库函数查询工具包括:
- C/C++官方文档:https://zh.cppreference.com/w/c/header
- cplusplus.com:C library - C++ Reference
double sqrt (double x);
//sqrt 是函数名
//x 是函数的参数,表⽰调⽤sqrt函数需要传递⼀个double类型的值
//double 是返回值类型 - 表⽰函数计算的结果是double类型的值
2.2.1 功能
2.2.2 头文件包含
使用库函数时必须包含对应的标准库头文件,否则可能导致程序运行异常。这些函数声明均位于标准库的头文件中。
2.2.3 实践
#include <stdio.h>
#include <math.h>
int main()
{
double d = 16.0;
double r = sqrt(d);
printf("%lf\n", r);
return 0;
}
2.2.4 库函数文档通用格式规范
- 函数原型
- 函数功能介绍
- 参数和返回类型说明
- 代码举例
- 代码输出
- 相关知识链接
3. 自定义函数
掌握了库函数之后,我们应当重点研究自定义函数。自定义函数不仅更为关键,还能为程序员提供更大的创造空间
3.1 函数的语法形式
ret_type fun_name(形式参数)
{
}
• ret_type:函数返回类型
• fun_name:函数名称
• ( ):包含形式参数
• { }:界定函数体
我们可以将函数类比为一个微型加工厂。就像工厂需要输入原材料,经过加工才能生产出产品一样,函数也需要输入值(可以是零个或多个),经过函数内部的计算处理后得出结果。
- ret_type:表示函数返回结果的类型。当返回类型为void时,表示函数不返回任何值
- fun_name:相当于函数的标识符。就像人名便于称呼一样,有意义的函数名能更直观地体现函数功能,方便调用
- 参数列表:相当于工厂的原材料。参数可以是void,表示函数不需要参数。若有参数,需明确其类型、名称和数量
- 函数体:由大括号{}包裹的部分,包含了实现具体功能的计算过程
3.2 函数的举例
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
//输⼊
scanf("%d %d", &a, &b);
//调⽤加法函数,完成a和b的相加
//求和的结果放在r中
//to do
//输出
printf("%d\n", r);
return 0;
}
我们基于功能需求,将函数命名为Add。该函数接收两个整型参数,并返回整型计算结果。根据上述分析,我们编写函数如下:
#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;
}
函数参数需要明确说明以下几点:
- 参数数量
- 每个参数的数据类型
- 形参名称
以上示例仅为说明,实际开发中我们可以根据需求灵活设计:
- 函数名称
- 参数配置
- 返回值类型
4. 形参和实参
函数调用时,参数可分为实参和形参两种类型。
4.1 实参
在示例代码中,第2-7行定义了Add函数。随后在第17行调用了该函数。
当调用Add函数时,传递给函数的参数a和b被称为实际参数(简称实参)。实参即真正传递给函数的具体参数值。
4.2 形参
在代码中,第2行定义Add函数时,括号内的x和y被称为形式参数(简称形参)。
之所以称为"形式参数",是因为如果仅定义Add函数而不调用它,这些参数x和y仅存在于形式上,不会实际占用内存空间。只有在函数被调用时,形参才会为存储实参传递的值而申请内存空间,这个过程就是形参的实例化。
4.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;
}
调试时可以看到,x和y确实获得了a和b的值,但x、y的地址与a、b的地址不同。这表明形参实际上是实参的一份临时拷贝。
5. return 语句
Return语句使用指南
在函数设计中,return语句的使用需要注意以下要点:
- return后可跟数值或表达式,若为表达式则先执行计算再返回结果值
- 当函数返回类型为void时,可直接写
return;
不带返回值 - 若返回值类型与函数声明类型不符,系统会自动进行隐式类型转换
- 执行return后函数立即终止,后续代码不会被执行
- 对于存在条件分支的函数,必须确保所有执行路径都有返回值,否则会导致编译错误
这些规则能帮助开发者编写更规范、可靠的函数代码。
6. 数组做函数参数
在函数编程中,经常需要将数组作为参数传递给函数,并在函数内部对数组进行处理。例如,我们可以编写一个函数将整型数组的所有元素设为-1,再创建另一个函数来打印数组内容。
最基础的形式可以这样实现:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
set_arr();//设置数组内容为-1
print_arr();//打印数组内容
return 0;
}
要实现对数组内容的设置,set_arr
函数需要接收两个参数:数组本身及其元素个数。这样函数内部才能遍历数组并设置每个元素的值。同理,print_arr
函数也需要这两个参数才能正确遍历并打印数组的每个元素。
#include <stdio.h>
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;
}
以下是优化后的内容:
当数组作为参数传递给 set_arr
和 print_arr
函数时,函数设计需要注意以下几点:
- 确保函数的形式参数与实际参数个数匹配
- 若实参是数组,形参可直接使用数组形式声明
- 对于一维数组形参,可省略数组大小
- 对于二维数组形参,行数可省略但列数必须保留
- 数组作为参数传递时不会创建新数组
- 形参操作的数组与实参数组是同一数组
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");
}
7. 嵌套调用和链式访问
7.1 嵌套调用
嵌套调用指的是函数之间的相互调用。就像乐高积木的各个零件需要完美配合才能搭建出精美的作品一样,函数之间也需要通过有效的相互调用才能构建出复杂的程序。
以计算某年某月有多少天为例,我们可以设计两个函数来实现:
is_leap_year()
:判断指定年份是否为闰年get_days_of_month()
:先调用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 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 在更复杂的代码中,函数之间通常存在多层嵌套调用,但要注意函数定义本身是不能嵌套的。
7.2 链式访问
链式访问是指将一个函数的返回值直接作为另一个函数的输入参数,从而像链条一样将多个函数串联调用。例如:
#include <stdio.h>
int main()
{
int len = strlen("abcdef");//1.strlen求⼀个字符串的⻓度
printf("%d\n", len);//2.打印⻓度
return 0;
}
将代码优化为链式访问示例:直接把 strlen 的返回值作为 printf 函数的参数即可实现。
#include <stdio.h>
int main()
{
printf("%d\n", strlen("abcdef"));//链式访问
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
int printf ( const char * format, ... );
printf
函数会返回输出到屏幕的字符数量。
在上面的例子中:
- 第三个
printf
打印"43",输出2个字符并返回2 - 第二个
printf
打印2,输出1个字符并返回1 - 第一个
printf
打印1
因此程序最终输出结果为:4321
8. 函数的声明和定义
8.1 单个文件
通常情况下,我们直接定义函数即可使用。例如:编写一个判断某年是否为闰年的函数。
在以上代码中,橙色部分为函数定义,绿色部分则是函数调用。
这种函数定义先于调用的顺序是符合规范的。
#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语言编译器在编译源代码时是从上至下逐行扫描的。当扫描到第7行调用is_leap_year函数时,由于尚未发现该函数的定义,因此会报出警告。
解决方法是在函数调用前先声明is_leap_year函数。函数声明只需明确以下三点:
- 函数名
- 返回值类型
- 参数类型 例如:int is_leap_year(int y); 注意:函数声明中参数名可以省略,只保留类型也是允许的。
修改后的代码就能顺利通过编译了。
#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;
}
为确保程序正确执行,函数调用必须遵循"先声明后使用"的原则。需要注意的是,函数定义本身也属于声明的一种特殊形式,因此将函数定义置于调用语句之前同样符合该规则。
8.2 多个文件
在企业开发中,我们通常会将代码按功能模块拆分到多个文件中,而不是将所有代码都集中在一个文件里。通常的做法是:
- 将函数声明和类型定义放在头文件(.h)中
- 将函数实现放在源文件(.c)中
//函数的定义
int Add(int x, int y)
{
return x + y;
}
//函数的声明
int Add(int x, int y);
#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
static
和extern
是C语言中的两个重要关键字。
static
关键字用于:
- 修饰局部变量
- 修饰全局变量
- 修饰函数
extern
关键字则主要用于声明外部符号。
在深入理解static
和extern
之前,需要先明确两个基本概念:作用域和生命周期。
作用域(scope)指程序中变量或函数名有效的代码范围:
- 局部变量的作用域仅限于其所在的代码块
- 全局变量的作用域跨越整个工程/项目
生命周期指变量从创建(内存分配)到销毁(内存回收)的时间段:
- 局部变量的生命周期始于进入作用域,止于离开作用域
- 全局变量的生命周期等同于整个程序的运行周期
8.3.1 static 修饰局部变量:
对比代码1和代码2的执行效果,有助于理解 static 修饰局部变量的作用。
在代码1中,test函数内的局部变量i会在每次函数调用时:
- 创建变量(生命周期开始)
- 初始化为0
- 执行自增操作
- 打印当前值 函数退出时,变量生命周期结束(内存被释放)
而在代码2中,观察输出结果可以发现:
- i的值呈现出累加效果
- test函数中的i在首次创建后不会被销毁
- 后续函数调用时不会重新创建变量
- 直接基于上次的计算结果继续操作
结论:static修饰局部变量会改变其生命周期,本质上是改变了变量的存储类型。原先存储在栈区的局部变量,在被static修饰后将转移到静态区存储。静态区的变量与全局变量具有相同的特性,其生命周期与程序一致,只有当程序结束时才会被销毁并回收内存。需要注意的是,变量的作用域不会因此改变。
使用建议:若需在函数退出后保留变量值,以供下次调用时继续使用,可通过static
修饰符实现。
8.3.2 static 修饰全局变量
extern
用于声明外部符号。当一个全局符号在A文件中定义,若要在B文件中使用该符号,可以使用extern
进行声明后即可调用。
代码1可以正常编译,而代码2在编译时会出现链接错误。
结论: 使用static修饰全局变量会限制其作用域仅在当前源文件内有效,无法被其他源文件访问。这是因为全局变量默认具有外部链接属性,只要进行适当声明即可在其他文件中使用;而static修饰会将其链接属性改为内部链接,从而将可见性限制在当前文件范围内。
建议:当某个全局变量仅需在当前源文件中使用时,推荐使用static修饰以隐藏其实现细节,避免被其他文件误用。
8.3.3 static 修饰函数
static
修饰函数和全局变量具有相同效果:默认情况下函数具有全局可见性(外部链接属性),可以被整个工程访问;而使用static
修饰后,函数将变为文件作用域(内部链接属性),仅能在当前源文件内使用。
当需要限制函数仅在当前源文件内使用时,建议使用static
修饰该函数,这样能有效避免被其他文件链接调用。
✅ 结尾总结
函数是C语言编程的基石,掌握好函数的使用,不仅能提升代码的复用性和可读性,还能为后续学习更复杂的编程范式(如面向对象、函数式编程)打下坚实基础。
通过本文的学习,希望你能够:
-
✅ 理解函数的定义、声明与调用机制
-
✅ 熟练使用库函数和自定义函数
-
✅ 掌握数组、作用域等进阶用法
-
✅ 避免常见错误,写出更健壮的代码
编程是一门实践的艺术,光看不够,一定要动手去写、去调试、去优化。如果你在实践过程中遇到问题,欢迎在评论区留言交流,也欢迎关注我的专栏,获取更多C语言开发相关内容。