一、函数的核心概念

函数又称为接口,是 C 语言中对一段实现独立功能的代码进行封装的语法形式,是模块化编程的核心载体。

二、函数的核心作用

  1. 模块化编程:将复杂的程序拆分为多个功能独立的小模块,每个模块由一个函数实现,降低代码的复杂度和维护成本,让程序结构更清晰。
  2. 增强代码复用性:实现某一功能的函数可在程序中多次调用,无需重复编写相同代码,提升开发效率,减少代码冗余。

三、函数的定义

基本语法

返回值类型 函数名(参数列表) {
    // 函数体:实现函数具体功能的语句集合
    功能实现代码;
    // 有返回值时,需通过return返回对应类型数据
    return 结果值; 
}

函数定义的四大核心组成:函数名参数列表返回值类型函数体,缺一不可(无参数 / 无返回值需显式声明)。

1. 函数名

  • 函数的入口标志,程序调用函数时通过函数名定位,其本质是函数在内存中的首地址
  • 属于 C 语言标识符的一种,命名需遵循标识符规则:
    • 字母、数字、下划线组成;
    • 数字不能作为开头
    • 区分大小写,建议见名知意(如get_sumis_leap_year)。

2. 参数列表

表面意义

获取函数实现功能所需的已知条件 / 原始数据,相当于函数的 “输入”。

核心特性
  • 个数无限制:可无参数(写void或空)、单个参数、多个参数(多个参数用,分隔);
  • 本质是局部变量:参数仅在函数体内部有效,函数执行结束后会被释放;
  • 定义格式:类型名 变量名(如int a, float b)。

3. 返回值类型

  • 函数执行完成后向调用者反馈的结果类型,结果通过return关键字返回;
  • 支持 C 语言所有数据类型(intfloatchar、数组、指针等),无返回值时需显式声明为 void
  • 注意:若省略返回值类型,C 语言默认返回值类型为 int(不推荐省略,降低代码可读性)。
  • return的作用:① 返回结果给调用者;② 立即结束函数执行(后续代码不再运行)。

4. 函数体

  • 包裹在{}中的语句集合,是实现函数核心功能的代码块;
  • 函数体中可包含变量定义、表达式、循环、分支、甚至调用其他函数;
  • 若无具体功能(仅占位),可写;(空函数),如void test() {}

四、函数的声明

基本概念

函数声明是告诉编译器函数的存在,说明函数的返回值类型、函数名、参数列表,让编译器在调用函数时能做语法校验,不包含函数体

基本语法

返回值类型 函数名(参数列表); // 末尾必须加分号

关键注意事项

  1. 声明位置:建议全局声明(写在所有函数之前,如 main 函数上方),也可在函数内部声明(仅在当前函数内有效);
  2. 声明时机:必须在函数调用之前声明,否则编译器会报 “函数未定义” 错误;
  3. 声明与定义的匹配:函数声明的返回值类型、函数名、参数列表(类型 + 个数 + 顺序) 必须与函数定义完全一致;
  4. 简化声明:参数列表中可只写类型,省略变量名(如int add(int, int);),但定义时必须写变量名。

示例

// 函数声明
int add(int a, int b); 
int main() {
    // 函数调用(声明后可正常调用)
    int res = add(1,2); 
    return 0;
}
// 函数定义
int add(int a, int b) {
    return a + b;
}

五、函数的调用

基本语法

// 无返回值调用
函数名(传递的值); 
// 有返回值调用,用变量接收结果
变量名 = 函数名(传递的值); 

调用的核心原理

函数一旦被调用,程序的执行流程会跳转到函数定义的内存地址处执行函数体;当函数执行完成(遇到return或函数体结束),执行流程会返回到调用函数的下一行代码,继续向下执行。

关键概念:实参和形参

  • 形参:函数定义 / 声明时参数列表中的变量(如add(int a, int b)中的ab),是 “形式上的参数”,仅在函数体内部有效;
  • 实参:函数调用时传递给参数列表的值(如add(1,2)中的12),是 “实际的参数”,可以是常量、变量、表达式等;
  • 传递规则:实参的类型、个数、顺序必须与形参一致,否则会出现语法错误或隐式类型转换导致的逻辑错误。

调用示例

#include <stdio.h>
// 函数声明
int sub(int a, int b);
int main() {
    int x = 10, y = 3;
    // 有返回值调用,变量接收结果
    int res1 = sub(x, y); 
    // 有返回值调用,表达式作为实参
    int res2 = sub(x+2, y*3); 
    // 无返回值函数调用示例
    printf("res1=%d, res2=%d\n", res1, res2); 
    return 0;
}
// 函数定义
int sub(int a, int b) {
    return a - b;
}

输出结果res1=7, res2=3

六、函数的多文件编程(工程化开发核心)

在实际项目开发中,不会将所有代码写在一个文件中,而是通过多文件拆分实现功能解耦,核心是 “头文件(.h)+ 源文件(.c)” 的配合,以下以日历小项目为例讲解。

1. 多文件编程的问题背景

原始日历程序(06cal.c)的痛点:

  • 功能重复:闰年判断逻辑写了 3 次,月份天数计算逻辑写了 2 次,代码冗余;
  • 无模块化:所有代码按执行顺序写,流水账式结构,维护难度大;
  • 可读性差:核心功能与主逻辑混杂,难以快速定位问题。

2. 多文件拆分方案

将日历程序拆分为 3 个文件,各司其职,实现声明与定义分离、功能与主逻辑分离

文件名称 文件类型 核心作用 具体实现
mylib.h 头文件 函数声明、宏定义、头文件包含 声明 mylib.c 中定义的所有函数(如闰年判断、月份天数计算),让调用者无需查看 mylib.c 的实现细节即可调用函数
mylib.c 源文件 函数定义(核心功能实现) ① 定义is_leap_year(int year):判断是否为闰年;② 定义get_days(int year, int month):计算指定年月的天数
06cal.c 主源文件 程序主逻辑、函数调用 包含 mylib.h 头文件,调用其中声明的函数,实现日历的整体打印逻辑

3. 多文件编程的关键语法

(1)头文件包含

在源文件中通过#include "头文件名.h"包含自定义头文件(系统头文件用<>,自定义头文件用""),如 06cal.c 和 mylib.c 中都需写:

#include "mylib.h"
(2)头文件保护(防止重复包含)

头文件被多次包含时会导致 “函数重声明” 错误,需添加头文件保护机制,有两种写法:

写法 1:#ifndef + #define + #endif(通用,兼容所有编译器)
// mylib.h
#ifndef __MYLIB_H__ // 若未定义该宏
#define __MYLIB_H__ // 定义该宏

// 函数声明
int is_leap_year(int year);
int get_days(int year, int month);

#endif // 结束条件编译
写法 2:#pragma once(简洁,主流编译器支持)
// mylib.h
#pragma once // 仅包含一次该头文件

// 函数声明
int is_leap_year(int year);
int get_days(int year, int month);
(3)编译与运行

多文件程序需将所有源文件一起编译,如 Linux 下 gcc 编译命令:

gcc 06cal.c mylib.c -o cal
./cal # 运行生成的可执行文件

4. 多文件编程的优势

  1. 功能解耦:核心功能与主逻辑分离,便于单独修改、测试某一功能;
  2. 代码复用:mylib.c 中的函数可被多个主文件调用(如其他项目需要闰年判断,直接复用 mylib.h 和 mylib.c);
  3. 隐藏实现细节:调用者只需看头文件的函数声明(知道 “怎么用”),无需关注源文件的实现(不用知道 “怎么写”);
  4. 团队协作:多个开发者可同时开发不同文件,避免代码冲突。

七、函数实战练习(必做)

练习 1:实现四则运算接口函数

需求说明
  1. 分别定义加法add()、减法sub()、乘法mul()、除法dev()函数,实现两个整数的四则运算;
  2. 除法函数需做除零校验(除数为 0 时,提示错误并返回 0);
  3. 编写 main 函数,实现交互逻辑:读入两个整型数,再读入一个运算符,根据运算符调用对应函数,输出运算结果。
输入输出示例
输入:1 100 -
输出:1 - 100 = -99

输入:10 0 /
输出:错误!除数不能为0
10 / 0 = 0

输入:5 6 *
输出:5 * 6 = 30
参考实现
#include <stdio.h>
// 函数声明
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int dev(int a, int b);

int main() {
    int num1, num2, res;
    char op;
    // 读入两个整数和一个运算符
    scanf("%d %d %c", &num1, &num2, &op);
    // 根据运算符调用对应函数
    switch(op) {
        case '+': res = add(num1, num2); break;
        case '-': res = sub(num1, num2); break;
        case '*': res = mul(num1, num2); break;
        case '/': res = dev(num1, num2); break;
        default: printf("错误!无效的运算符\n"); return 1;
    }
    // 输出结果
    printf("%d %c %d = %d\n", num1, op, num2, res);
    return 0;
}

// 加法函数
int add(int a, int b) {
    return a + b;
}
// 减法函数
int sub(int a, int b) {
    return a - b;
}
// 乘法函数
int mul(int a, int b) {
    return a * b;
}
// 除法函数(含除零校验)
int dev(int a, int b) {
    if(b == 0) {
        printf("错误!除数不能为0\n");
        return 0;
    }
    return a / b;
}

练习 2:统计所有水仙花数

需求说明
  1. 水仙花数定义:三位数,其各位数字的立方和等于该数本身(如153 = 1³ + 5³ + 3³);
  2. 分步实现:① 定义函数判断一个数是否为水仙花数;② 定义函数计算一个数的各位立方和;③ 在 main 函数中遍历所有三位数,统计并输出所有水仙花数。
解题思路
  1. 三位数的范围:100 ~ 999,通过 for 循环遍历;
  2. 分解三位数的各位:百位num/100、十位(num/10)%10、个位num%10
  3. 立方和计算:百位³ + 十位³ + 个位³
  4. 水仙花数判断:立方和 == 原数,则为水仙花数。
参考实现
#include <stdio.h>
// 函数声明:计算一个数的各位立方和
int get_cube_sum(int num);
// 函数声明:判断一个数是否为水仙花数
int is_narcissus(int num);

int main() {
    int count = 0; // 统计水仙花数的个数
    printf("所有水仙花数:\n");
    // 遍历所有三位数
    for(int i = 100; i <= 999; i++) {
        if(is_narcissus(i)) {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n水仙花数的总个数:%d\n", count);
    return 0;
}

// 定义:计算各位立方和
int get_cube_sum(int num) {
    int bai = num / 100;
    int shi = (num / 10) % 10;
    int ge = num % 10;
    return bai*bai*bai + shi*shi*shi + ge*ge*ge;
}

// 定义:判断是否为水仙花数(是返回1,否返回0)
int is_narcissus(int num) {
    // 先判断是否为三位数
    if(num < 100 || num > 999) {
        return 0;
    }
    // 立方和等于原数,则为水仙花数
    return get_cube_sum(num) == num ? 1 : 0;
}

输出结果

所有水仙花数:
153 370 371 407 
水仙花数的总个数:4

八、C 语言内存管理(函数进阶核心,非常重要)

函数的执行与内存紧密相关,C 语言的内存空间按功能划分为栈区、堆区、全局 / 静态区、常量区、代码区,不同区域的变量有不同的生命周期和作用域,函数的形参、局部变量、返回值等都与内存区域密切相关。

1. 内存分区整体结构

C 程序运行时,操作系统为其分配的内存空间分为 5 个核心区域:

内存区域 核心存储内容 生命周期 分配 / 释放方式 关键特性
代码区 程序的二进制执行代码(函数体、指令) 程序运行期间一直存在 操作系统自动分配 / 释放 只读、共享
常量区 字符串常量、const 修饰的全局常量 程序运行期间一直存在 操作系统自动分配 / 释放 只读,不可修改
全局 / 静态区 全局变量、static 修饰的局部 / 全局变量 程序运行期间一直存在 操作系统自动分配 / 释放 初始化默认值为 0
栈区 函数的形参、局部变量、函数调用的返回地址 函数执行时分配,函数结束后立即释放 编译器自动分配 / 释放 空间小(几 M)、速度快
堆区 动态分配的内存(如 malloc、calloc 申请的内存) 手动申请,手动释放(free) 程序员通过函数手动分配 / 释放 空间大(几 G)、灵活,易内存泄漏

2. 函数与栈区的核心关联(重点)

函数的调用和执行全程依赖栈区,栈区采用 “先进后出” 的栈式管理方式:

  1. 函数调用时,编译器在栈区为函数分配栈帧:存储形参、局部变量、函数调用的返回地址;
  2. 函数执行期间,所有形参和局部变量的操作都在栈帧中进行;
  3. 函数执行完成(return / 结束),编译器自动释放该函数的栈帧:形参和局部变量被销毁,内存空间归还给栈区,其值不再有效。
经典坑点:返回局部变量的地址
#include <stdio.h>
int* test() {
    int a = 10; // 局部变量,存储在栈区
    return &a;  // 返回栈区变量的地址
}
int main() {
    int* p = test(); // 接收地址
    printf("%d\n", *p); // 未定义行为:栈帧已释放,地址指向的内存无效
    return 0;
}

问题分析:test 函数执行结束后,其栈帧被释放,变量 a 的内存被回收,返回的地址成为 “野指针”,访问该地址会导致未定义行为(可能输出随机值、程序崩溃)。解决方案:若需返回函数内变量的地址,需将变量定义在全局 / 静态区(static 修饰)或堆区(malloc 动态分配)。

3. 静态变量(static)与函数

static 修饰函数内的局部变量时,变量的存储区域从栈区转移到全局 / 静态区,其生命周期变为 “程序运行期间一直存在”,仅在函数第一次调用时初始化一次,后续调用会保留上一次的结果。

示例:统计函数被调用的次数
#include <stdio.h>
void count_call() {
    static int count = 0; // 静态局部变量,全局/静态区,仅初始化一次
    count++;
    printf("函数已被调用%d次\n", count);
}
int main() {
    count_call(); // 输出:函数已被调用1次
    count_call(); // 输出:函数已被调用2次
    count_call(); // 输出:函数已被调用3次
    return 0;
}

4. 函数的内存管理核心注意事项

  1. 栈区变量不要返回地址,避免野指针;
  2. 堆区动态分配的内存,使用后必须用 free 释放,否则会导致内存泄漏;
  3. static 修饰的局部变量虽生命周期长,但仅在当前函数内有效(作用域不变);
  4. 全局变量尽量少用:会占用全局 / 静态区内存,且易导致程序耦合度高,引发数据冲突。

对应练习

10 道 C 语言函数趣味练习题(初学者友好)

  1. 函数版:问好小助手

写一个函数 sayHello(),不带参数无返回值,调用时输出:

你好!欢迎学习C语言~

  1. 求两数之和(带参函数)

写函数 add(int a, int b),返回两数之和。

主函数输入两个数,调用函数输出结果。

  1. 判断奇偶数函数

写函数 isEven(int n),是偶数返回 1,奇数返回 0。

主函数输入数字,输出 “偶数” 或 “奇数”。

  1. 求最大值函数

写函数 max(int a, int b),返回较大的数。

再扩展:写 max3(int a,int b,int c) 返回三个数最大值。

  1. 打印星号金字塔(无返回值函数)

写函数 printStars(int n),打印 n 层高的金字塔:

  1. 数字反转函数

写函数 reverse(int n),返回数字反转后结果。

如输入 123 → 返回 321。

  1. 成绩等级判断函数

写函数 getLevel(int score):

90~100 → 'A'

80~89 → 'B'

70~79 → 'C'

其他 → 'D'

  1. 素数判断函数(超经典)

写函数 isPrime(int x),是素数返回 1,否则 0。

主函数输出 1~100 之间的素数。

  1. 计算 n 的阶乘函数

写函数 fact(int n),返回 n!

如 fact (5)=120。

  1. 小游戏:猜数字辅助函数

写函数 check(int ans, int g):

猜大了返回 1

猜小了返回 -1

猜对返回 0

主函数循环猜数字直到猜对。


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐