嵌入式Linux学习 C语言(Day06)函数核心知识点详解 + 对应练习及答案
一、函数的核心概念
函数又称为接口,是 C 语言中对一段实现独立功能的代码进行封装的语法形式,是模块化编程的核心载体。
二、函数的核心作用
- 模块化编程:将复杂的程序拆分为多个功能独立的小模块,每个模块由一个函数实现,降低代码的复杂度和维护成本,让程序结构更清晰。
- 增强代码复用性:实现某一功能的函数可在程序中多次调用,无需重复编写相同代码,提升开发效率,减少代码冗余。
三、函数的定义
基本语法
返回值类型 函数名(参数列表) {
// 函数体:实现函数具体功能的语句集合
功能实现代码;
// 有返回值时,需通过return返回对应类型数据
return 结果值;
}
函数定义的四大核心组成:函数名、参数列表、返回值类型、函数体,缺一不可(无参数 / 无返回值需显式声明)。
1. 函数名
- 函数的入口标志,程序调用函数时通过函数名定位,其本质是函数在内存中的首地址。
- 属于 C 语言标识符的一种,命名需遵循标识符规则:
- 由字母、数字、下划线组成;
- 数字不能作为开头;
- 区分大小写,建议见名知意(如
get_sum、is_leap_year)。
2. 参数列表
表面意义
获取函数实现功能所需的已知条件 / 原始数据,相当于函数的 “输入”。
核心特性
- 个数无限制:可无参数(写
void或空)、单个参数、多个参数(多个参数用,分隔); - 本质是局部变量:参数仅在函数体内部有效,函数执行结束后会被释放;
- 定义格式:
类型名 变量名(如int a, float b)。
3. 返回值类型
- 函数执行完成后向调用者反馈的结果类型,结果通过
return关键字返回; - 支持 C 语言所有数据类型(
int、float、char、数组、指针等),无返回值时需显式声明为 void; - 注意:若省略返回值类型,C 语言默认返回值类型为 int(不推荐省略,降低代码可读性)。
return的作用:① 返回结果给调用者;② 立即结束函数执行(后续代码不再运行)。
4. 函数体
- 包裹在
{}中的语句集合,是实现函数核心功能的代码块; - 函数体中可包含变量定义、表达式、循环、分支、甚至调用其他函数;
- 若无具体功能(仅占位),可写
;(空函数),如void test() {}。
四、函数的声明
基本概念
函数声明是告诉编译器函数的存在,说明函数的返回值类型、函数名、参数列表,让编译器在调用函数时能做语法校验,不包含函数体。
基本语法
返回值类型 函数名(参数列表); // 末尾必须加分号
关键注意事项
- 声明位置:建议全局声明(写在所有函数之前,如 main 函数上方),也可在函数内部声明(仅在当前函数内有效);
- 声明时机:必须在函数调用之前声明,否则编译器会报 “函数未定义” 错误;
- 声明与定义的匹配:函数声明的返回值类型、函数名、参数列表(类型 + 个数 + 顺序) 必须与函数定义完全一致;
- 简化声明:参数列表中可只写类型,省略变量名(如
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)中的a、b),是 “形式上的参数”,仅在函数体内部有效; - 实参:函数调用时传递给参数列表的值(如
add(1,2)中的1、2),是 “实际的参数”,可以是常量、变量、表达式等; - 传递规则:实参的类型、个数、顺序必须与形参一致,否则会出现语法错误或隐式类型转换导致的逻辑错误。
调用示例
#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. 多文件编程的优势
- 功能解耦:核心功能与主逻辑分离,便于单独修改、测试某一功能;
- 代码复用:mylib.c 中的函数可被多个主文件调用(如其他项目需要闰年判断,直接复用 mylib.h 和 mylib.c);
- 隐藏实现细节:调用者只需看头文件的函数声明(知道 “怎么用”),无需关注源文件的实现(不用知道 “怎么写”);
- 团队协作:多个开发者可同时开发不同文件,避免代码冲突。
七、函数实战练习(必做)
练习 1:实现四则运算接口函数
需求说明
- 分别定义加法
add()、减法sub()、乘法mul()、除法dev()函数,实现两个整数的四则运算; - 除法函数需做除零校验(除数为 0 时,提示错误并返回 0);
- 编写 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:统计所有水仙花数
需求说明
- 水仙花数定义:三位数,其各位数字的立方和等于该数本身(如
153 = 1³ + 5³ + 3³); - 分步实现:① 定义函数判断一个数是否为水仙花数;② 定义函数计算一个数的各位立方和;③ 在 main 函数中遍历所有三位数,统计并输出所有水仙花数。
解题思路
- 三位数的范围:
100 ~ 999,通过 for 循环遍历; - 分解三位数的各位:百位
num/100、十位(num/10)%10、个位num%10; - 立方和计算:
百位³ + 十位³ + 个位³; - 水仙花数判断:立方和 == 原数,则为水仙花数。
参考实现
#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. 函数与栈区的核心关联(重点)
函数的调用和执行全程依赖栈区,栈区采用 “先进后出” 的栈式管理方式:
- 函数调用时,编译器在栈区为函数分配栈帧:存储形参、局部变量、函数调用的返回地址;
- 函数执行期间,所有形参和局部变量的操作都在栈帧中进行;
- 函数执行完成(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. 函数的内存管理核心注意事项
- 栈区变量不要返回地址,避免野指针;
- 堆区动态分配的内存,使用后必须用 free 释放,否则会导致内存泄漏;
- static 修饰的局部变量虽生命周期长,但仅在当前函数内有效(作用域不变);
- 全局变量尽量少用:会占用全局 / 静态区内存,且易导致程序耦合度高,引发数据冲突。
对应练习
10 道 C 语言函数趣味练习题(初学者友好)
- 函数版:问好小助手
写一个函数 sayHello(),不带参数无返回值,调用时输出:
你好!欢迎学习C语言~

- 求两数之和(带参函数)
写函数 add(int a, int b),返回两数之和。
主函数输入两个数,调用函数输出结果。

- 判断奇偶数函数
写函数 isEven(int n),是偶数返回 1,奇数返回 0。
主函数输入数字,输出 “偶数” 或 “奇数”。

- 求最大值函数
写函数 max(int a, int b),返回较大的数。
再扩展:写 max3(int a,int b,int c) 返回三个数最大值。

- 打印星号金字塔(无返回值函数)
写函数 printStars(int n),打印 n 层高的金字塔:

- 数字反转函数
写函数 reverse(int n),返回数字反转后结果。
如输入 123 → 返回 321。

- 成绩等级判断函数
写函数 getLevel(int score):
90~100 → 'A'
80~89 → 'B'
70~79 → 'C'
其他 → 'D'

- 素数判断函数(超经典)
写函数 isPrime(int x),是素数返回 1,否则 0。
主函数输出 1~100 之间的素数。

- 计算 n 的阶乘函数
写函数 fact(int n),返回 n!
如 fact (5)=120。

- 小游戏:猜数字辅助函数
写函数 check(int ans, int g):
猜大了返回 1
猜小了返回 -1
猜对返回 0
主函数循环猜数字直到猜对。

更多推荐



所有评论(0)