C语言的
函数(Function)是程序的基本构建块,用于封装一段可重用的代码,完成特定任务。函数可以提高代码的模块化、可读性和复用性。

函数包括库函数和自定义函数 函数也被成为子程序,就是⼀个完成某项特定的任务的一小段代码
我们前⾯内容中学到的 printf 、 scanf 都是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要学会就能直接使⽤了。有了库函数,⼀些常⻅的功能就不需要程序员⾃⼰实现了,⼀定程度提升了效率;同时库函数的质量和执⾏效率上都更有保证。
库函数是在标准库中对应的头⽂件中声明的,所以库函数的使⽤,务必包含对应的头⽂件,不包含是可能会出现⼀些问题的。
#include <stdio.h>
#include <math.h>
int main()
{
double d = 16.0;
double r = sqrt(d);
printf("%lf\n", r);
return 0;
}C标准库的函数按功能分类在不同的头文件(.h)中,使用时需先包含对应头文件:
头文件 | 主要功能 | 常用函数示例 |
|---|---|---|
<stdio.h> | 标准输入输出 | printf, scanf, fopen, fgets |
<string.h> | 字符串处理 | strcpy, strlen, strcat, strcmp |
<math.h> | 数学运算 | sin, cos, sqrt, pow |
<stdlib.h> | 内存管理、随机数、类型转换 | malloc, free, rand, atoi |
<time.h> | 时间和日期处理 | time, clock, strftime |
<ctype.h> | 字符分类和转换 | isalpha, tolower, isdigit |
自定义函数由 函数名、参数列表、返回类型和函数体构成
返回类型 函数名(参数列表) {
// 函数体(代码逻辑)
return 返回值; // 可选,取决于返回类型
}, 分隔。
int add(int a, int b) { // 返回类型:int | 函数名:add | 参数:int a, int b
return a + b; // 返回计算结果
}//函数定义后,可以通过 `函数名 + 参数` 调用:
int result = add(3, 5); // 调用 add(),传入 3 和 5
printf("3 + 5 = %d\n", result); // 输出: 3 + 5 = 8//无返回值函数的调用
greet(); // 调用 greet(),输出 "Hello, World!"//指针参数的调用
int x = 10, y = 20;
swap(&x, &y); // 传入地址,交换 x 和 y 的值
printf("x=%d, y=%d\n", x, y); // 输出: x=20, y=10如果函数定义在调用之后,需要先声明(告诉编译器函数的存在):
#include <stdio.h>
// 函数声明(原型)
int add(int a, int b);
int main() {
int sum = add(3, 5); // 调用 add()
printf("Sum: %d\n", sum);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}#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); //17
//输出
printf("%d\n", r);
return 0;
}在上⾯代码中,我们把第17行调用Add函数时,传递给函数的参数a和b,称为实际参数,简称实参。
实际参数就是真实传递给函数的参数。
在上⾯代码中,第2⾏定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称形参。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化
当函数被调用时,形参在
栈(Stack)中分配独立的内存空间。 函数执行结束后,形参的内存自动释放。实参的内存空间在调用前已存在(可能是全局变量、栈变量或堆内存)。 实参的内存生命周期由定义它的作用域决定(如函数结束释放栈变量)。
在值传递时,实参的值会被拷贝给形参,二者占用不同内存空间。 在指针传递时,实参和形参共享同一内存地址(通过指针间接访问)。
特性 | 形参 | 实参 |
|---|---|---|
内存位置 | 栈(函数调用时分配) | 由定义位置决定(栈/堆/全局) |
生命周期 | 函数执行期间 | 依赖原作用域 |
修改是否影响实参 | 值传递:否;指针传递:是 | 直接修改自身 |
本质 | 函数的局部变量 | 调用时传入的具体数据 |
在C语言中,数组作为函数参数传递是一个重要且需要特别注意的概念。下面我将从多个方面详细讲解数组作为函数参数的使用方法、原理和注意事项。
// 写法一:使用数组形式声明
void func(int arr[], int size) {
// 函数体
}
// 写法二:使用指针形式声明
void func(int *arr, int size) {
// 函数体
}C语言中数组作为函数参数传递时,实际上传递的是数组首元素的地址,而不是整个数组的副本。
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5); // arr在这里退化为指向首地址的指针
return 0;
}
void printArray(int a[], int size) {
// 实际上a是一个指针,不是数组
}由于数组参数退化为指针,函数内部无法直接获取数组的实际大小 因此,通常需要额外传递数组大小作为参数。
void printSize(int arr[]) {
printf("%zu\n", sizeof(arr)); // 输出指针大小(如8字节),不是数组大小
}
int main() {
int a[10];
printf("%zu\n", sizeof(a)); // 输出40(假设int为4字节)
printSize(a); // 输出8(64位系统指针大小)
return 0;
}如果不希望函数修改数组内容,可以使用const限定符
void printArray(const int arr[], int size) {
for(int i = 0; i < size; i++) {
printf("%d ", arr[i]);
// arr[i] = 0; // 编译错误,不能修改const数组
}
}void wrongFunc(int arr[]) {
int size = sizeof(arr)/sizeof(arr[0]); // 错误!结果是1或2(指针大小/元素大小)
}必须显式传递数组大小作为额外参数:sizeof(a)在main函数中仍然是完整的数组大小
// 正确写法
void correctFunc(int arr[], size_t size) {
for(size_t i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int a[5] = {1, 2, 3, 4, 5};
correctFunc(a, sizeof(a)/sizeof(a[0])); // 在调用处计算大小
return 0;
}这里我们需要知道数组传参的⼏个重点知识:
• 函数的形式参数要和函数的实参个数匹配
// 正确:形参和实参个数匹配
void printArray(int arr[], int size) { ... }
int main() {
int a[5] = {1, 2, 3, 4, 5};
printArray(a, 5); // 两个实参:数组名 + 数组大小
return 0;
}• 函数的实参是数组,形参也是可以写成数组形式的
// 以下两种写法等价
void func1(int arr[]) { ... } // 数组形式(推荐用于直观性)
void func2(int *arr) { ... } // 指针形式(推荐用于明确本质)• 形参如果是⼀维数组,数组⼤⼩可以省略不写
// 以下三种声明完全等效
void funcA(int arr[]) { ... } // 省略大小
void funcB(int arr[10]) { ... } // 写了大小(但无效)
void funcC(int *arr) { ... } // 直接写指针• 形参如果是⼆维数组,⾏可以省略,但是列不能省略
// 正确:列数必须明确
void printMatrix(int mat[][4], int rows) { ... }
// 错误:列数未指定
void wrongFunc(int mat[][]) { ... } // 编译报错!• 数组传参,形参是不会创建新的数组的
void modifyArray(int arr[]) {
arr[0] = 100; // 修改会影响实参的数组
}
int main() {
int a[3] = {1, 2, 3};
modifyArray(a);
printf("%d", a[0]); // 输出100,原数组被修改
return 0;
}• 形参操作的数组和实参的数组是同⼀个数组
void clearArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] = 0; // 直接修改原始数组
}
}
int main() {
int data[5] = {1, 2, 3, 4, 5};
clearArray(data, 5); // data数组被清空
return 0;
}#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;
}
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");
}另外在这里强调一下 数组
首地址和首元素地址是一个概念
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使用的注意事项。
嵌套调⽤就是函数之间的互相调⽤,每个函数就像⼀个乐⾼零件,正是因为多个乐⾼的零件互相⽆缝的配合才能搭建出精美的乐⾼玩具,也正是因为函数之间有效的互相调⽤,最后写出来了相对⼤型的程序。 禁止函数嵌套定义 下面我们用一段代码来展示
计算某年某月某日有多少天
is_leap_year():根据年份确定是否是闰年get_days_of_month():调⽤is_leap_year确定是否是闰年后,再根据月计算这个⽉的天数#include <stdio.h>
// 判断闰年函数
int IsLeapYear(int year) {
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
return 1;
}
else {
return 0;
}
}
// 获取月份天数函数
int get_days_of_month(int year, int month) {
int arr[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 修正数组顺序
int day = arr[month];
// 如果是闰年且2月,天数加1
if (IsLeapYear(year) && month == 2) {
day += 1;
}
return day;
}
int main() {
int y = 0;
int m = 0;
printf("请输入年份和月份:");
scanf("%d %d", &y, &m); // 使用标准scanf
int d = get_days_of_month(y, m);
printf("%d年%d月有%d天\n", y, m, d);
return 0;
}所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。
#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;
}printf是打印屏幕上的字符个数
我们就第一个printf打印的是第二个printf的返回值,第二个printf打印的是第三个printf的返回值。 第三个printf打印43,在屏幕上打印2个字符,再返回2 第二个printf打印2,在屏幕上打印1个字符,再放回1 第一个printf打印1 所以屏幕上最终打印:4321
声明: 告诉编译器"这个函数存在"(放在.h头文件) 定义: 实际实现函数功能(放在.c源文件) 一般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的定义与功能的实现放在源文件(.c)当中
调用函数时候记得包含头文件————用"头文件名.h"
project/
├── main.c # 主程序
├── utils.h # 函数声明(头文件)
└── utils.c # 函数定义在讲解关键字之前,我们先讲讲
作用域和生命周期
可见范围。void func() {
int x = 10; // 局部变量,只在func内可见
if (1) {
int y = 20; // 只在if块内可见
}
// y 这里不可用
}int global = 100; // 全局变量,从定义处到文件末尾都可见
void func1() {
global++; // 可以访问
}
void func2() {
global--; // 可以访问
}static int file_scope = 50; // 只在当前.c文件可见
void func() {
file_scope = 60; // 可以访问
}
// 其他文件无法访问file_scope存活的时间。void func() {
int auto_var = 10; // 函数调用时创建,函数结束时销毁
}int global_var; // 程序启动时创建,结束时销毁(初始化为0)
static int static_var; // 同上,但作用域受限
void func() {
static int local_static = 0; // 只会初始化一次,之后就保持值
local_static++;
}void func() {
int *p = malloc(sizeof(int)); // 手动分配
*p = 100;
free(p); // 手动释放
}void func() {
int *p = malloc(sizeof(int)); // 手动分配
*p = 100;
free(p); // 手动释放
}我们先来看下面的两端代码
#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的test函数中的局部变量i是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。
#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;
}代码2中,我们从输出结果来看,i的值有累加的效果,其实 test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
static修饰局部变量改变了局部变量的生命周期,生命周期的改变本质上就是改变了变量的存储类型 本来局部变量是存储在内存的栈区的,但是被
static修饰后存储到了静态区。 存储在静态区的变量的生命周期和全局变量的生命周期是一样,直到程序结束,变量才销毁,内存才收回
未来⼀个变量出了函数后,我们还想保留值,等下次进入函数继续使用,就可以使用
static修饰。
//test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d",g_val);
return 0;
}
//add.c
int g_val = 2018;使用建议:如果⼀个全局变量,只想在所在的源⽂件内部使用,不想被其他文件发现,就可以使用static 修饰。
static对函数也同样适用
extern是C语言中用于声明变量或函数的外部链接性的关键字,它告诉编译器"这个标识符的定义在其他文件中。
extern int globalVar; // 声明globalVar在其他文件中定义extern void someFunction(); // 等同于 void someFunction();在一个源文件中定义变量:
// file1.c
int globalVar = 10; // 定义在另一个源文件中使用:
// file2.c
extern int globalVar; // 声明
void foo() {
printf("%d\n", globalVar); // 使用
}// globals.h
extern int globalVar;extern const int MAX_SIZE; // 声明在别处定义的const变量递归是C语言函数学习中一个绕不开的话题 递归就是函数自己调用自己
int main() {
printf("hehehehe\n");
main();
return 0;
}
#include <stdio.h>
int Fact(int n) {
if (n == 0) {
return 1;
}
if (n != 0) {
return n * Fact(n - 1);
}
}
int main() {
int n = 0;
scanf_s("%d", &n);
int ret = Fact(n);
printf("%d", ret);
return 0;
}Fact函数在运行可能会涉及一些运行内存的开销 在C语言中每⼀次函数调⽤,都需要为本次函数调用在内存的栈区,申请⼀块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。 函数不返回,对应的栈帧空间就一直占用;所以如果函数在调用中出现函数递归的话,每一次递归函数调用都会开辟属于自己栈帧空间。直到递归不再继续,开始回归,才开始释放栈帧空间 所以如果采⽤函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
函数的主要内容就到此为止了,下一章我们将结合知识点,编写一款扫雷游戏