C 语言(二)|函数:声明、调用与模块化设计
详细介绍C语言函数的定义、参数、返回值、嵌套调用、函数声明、static与extern、数组传参等核心知识点。
版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。
一、函数的基础认知
1.1 函数的定义
函数是编程中对一组实现特定功能的代码逻辑的封装与命名,是可复用的"代码模块"。
核心特点:可接收输入(参数,可选)、执行专属逻辑、返回输出(返回值,可选);定义后可在程序任意位置多次调用,无需重复编写代码,同时让程序结构更清晰、易维护。
核心总结:
- 函数是封装特定功能的可复用代码块,有专属唯一名称
- 核心逻辑:传参(输入)→ 执行操作 → 返回结果(输出),输入和输出均可省略
- 核心价值:减少代码冗余,提升程序可读性、可维护性和可调试性
对比示例:重复代码实现求和 VS 函数封装实现求和
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int sum = 0;
int a, b, c, d;
printf("请输入第1个整数:"); scanf("%d", &a); sum += a;
printf("请输入第2个整数:"); scanf("%d", &b); sum += b;
printf("请输入第3个整数:"); scanf("%d", &c); sum += c;
printf("请输入第4个整数:"); scanf("%d", &d); sum += d;
printf("所有数的和为:%d\n", sum);
return 0;
}
那如果用函数呢?
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int add(int x, int y) {
return x + y;
}
int main() {
int sum = 0;
int a, b, c, d;
printf("请输入4个整数(空格分隔):");
scanf("%d %d %d %d", &a, &b, &c, &d);
sum = add(add(add(a, b), c), d);
printf("所有数的和为:%d\n", sum);
return 0;
}
1.2 函数与main()的核心关系
main()是C程序的唯一入口,普通函数是程序的功能单元,二者为「主调-被调」关系,核心两点:
- 程序运行时,操作系统仅会先找到并执行
main(),执行流程从main()第一行开始,到main()执行结束(return或正常走完)为止 - 普通函数不会主动执行,必须在
main()内部(或main()调用的其他函数内部)被"调用",才能参与程序执行
通俗比喻:main()是厂长办公室(启动流程、分配任务),普通函数是各个车间(负责具体功能),车间不会自行开工,需等待厂长办公室的指令(调用),所有任务完成后回到厂长办公室,程序结束。
1.3 函数的分类
C语言函数按来源分为两类,是后续学习的基础分类:
- 库函数:C语言标准库提供的现成函数,无需自己定义,直接调用即可(需引入对应头文件),如
printf()、strlen() - 自定义函数:开发者根据需求自行定义的函数,实现专属功能,如上述的
add()函数
二、自定义函数核心语法
自定义函数是实现个性化功能的核心,固定格式为「函数头 + {函数体}」,二者缺一不可。
2.1 函数头组成
函数头是函数的"标识信息",必须按顺序书写:返回值类型 + 函数名 + (形参列表)
- 返回值类型:指定函数执行后返回数据的类型(如int、float),无返回值则写void
- 函数名:函数的唯一标识,调用函数时通过函数名触发
- 形参列表:函数的输入参数,无参数则写空括号(),多个参数用逗号分隔
2.2 函数体要求
必须用大括号{}包裹,即使只有一行代码,也建议保留大括号(规范写法);内部实现函数的具体专属功能,建议单一功能原则(一个函数只做一件事),语句不宜过多;可在函数体内定义局部变量、执行业务语句、书写return语句。
2.3 函数名命名规范
由字母、数字、下划线组成,首字符不能是数字;不能与C语言关键字(如int、if、for)重名,不与库函数名重复;见名知意(如求和用add/sum,求平方用square),小写为主,多个单词用下划线分隔(如sum_square)。
标准示例:
int sum_square(int a, int b) {
int s1 = a * a;
int s2 = b * b;
return s1 + s2;
}
三、函数的参数和返回值
参数是函数的"输入",返回值是函数的"输出",二者是函数与主调程序交互的核心。
3.1 函数的参数
参数分为形参和实参,传参方式为大一核心重点——值传递。
形参(形式参数)
- 定义:在函数头中声明的参数,如
add(int x, int y)中的x、y - 特点:属于函数的局部变量,仅在函数体内有效;函数调用时才分配内存,调用结束后内存立即释放;多个形参需分别指定类型(如
int x, int y,不能写int x, y)
实参(实际参数)
- 定义:调用函数时传递给形参的具体值,如
add(1, 2)中的1、2 - 要求:实参的类型、个数、顺序必须与形参一一对应
- 可以是:常量、变量、合法表达式(如
add(a+b, 3*4))
3.2 值传递的核心特性
重要结论:在函数调用时,实参将自己的值"复制"一份传递给形参,形参是独立的新变量,在函数内部修改形参不会影响实参本身。
#include <stdio.h>
void change(int n) {
n = 100; // 修改形参n
}
int main() {
int a = 10;
change(a); // 实参a传递给形参n
printf("a = %d", a); // 输出a=10,形参修改不影响实参
return 0;
}
3.3 函数的返回值
return语句的核心作用
- 终止函数调用,强制返回到主调函数继续执行
- 将函数结果(值)带回到主调函数
- 一个函数可有多条return语句,但仅会执行第一条满足条件的return
int getMax(int a, int b) {
if (a > b)
return a; // 第一条满足的return执行,后续代码不运行
return b;
}
无返回值的函数
void printMsg() {
printf("Hello C\n");
return; // 可省略,仅写return;,不能跟值
}
四、函数的调用
4.1 基本语法
// 有参函数:函数名 + (实参列表),多个实参用逗号分隔
函数名(实参1, 实参2, ...);
// 无参函数:函数名 + 空括号,括号不能省略
函数名();
4.2 调用类型
- 普通调用:主调函数直接调用被调函数,执行完被调函数后,返回主调函数的调用位置继续执行后续代码
- 嵌套调用:一个函数内部调用另一个函数,C语言允许多层嵌套调用
4.3 不同返回值类型的调用方式
有返回值函数(非void)
- 赋值调用:将函数返回值赋值给同类型变量(最常用)
int res = add(1, 2); // add返回3,赋值给res
- 表达式调用:将函数调用作为表达式的一部分,直接使用返回值
printf("和为:%d", add(1, 2)); // add返回值直接作为printf的参数
无返回值函数(void)
- 仅有一种调用方式:直接调用,单独作为一条语句,无需赋值
printMsg(); // 直接调用,执行打印功能
五、函数的嵌套调用 & 链式访问
5.1 嵌套调用
核心定义
在一个函数的执行过程中调用另一个函数,执行完被调函数的全部代码后,再回到原函数的调用位置继续执行后续代码。核心禁忌:禁止函数嵌套定义(如在main()内定义add()),只能在函数外定义、函数内调用。
示例1:基础嵌套调用
#include <stdio.h>
// 被调用函数:求一个数的平方
int square(int n) {
return n * n;
}
// 主调函数:求两数平方和,嵌套调用square
int sumSquare(int a, int b) {
int s1 = square(a);
int s2 = square(b);
return s1 + s2;
}
int main() {
int res = sumSquare(3, 4); // main调用sumSquare
printf("3²+4²=%d", res); // 输出25
return 0;
}
示例2:多层嵌套调用
#include <stdio.h>
// 步骤1:求两数之和
int add(int a, int b) {
return a + b;
}
// 步骤2:求和的2倍,嵌套调用add
int doubleNum(int a, int b) {
return add(a, b) * 2;
}
// 步骤3:求最终结果,嵌套调用doubleNum
int getResult(int x, int y, int z) {
return doubleNum(x, y) - z;
}
int main() {
printf("结果:%d", getResult(1,2,3)); // 输出3
return 0;
}
5.2 链式访问
核心定义
把一个函数的返回值,直接作为另一个函数的参数来使用,无需用临时变量接收返回值,让代码更简洁,像"链条"一样把函数串起来。
示例1:基础链式访问
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
// add(1,2)返回3,直接作为第二个add的第一个参数
int res = add(add(1, 2), 3);
printf("1+2+3=%d", res); // 输出6
return 0;
}
示例2:经典库函数链式访问
#include <stdio.h>
#include <string.h>
int main() {
// strlen("hello")返回5,直接作为printf的%d参数
printf("字符串长度:%d", strlen("hello")); // 输出5
return 0;
}
5.3 避坑注意点
- 嵌套调用:禁止嵌套定义,多层嵌套不要过多(否则代码可读性差、调试困难)
- 链式访问:严格保证返回值类型与形参类型一致
- 二者可混用:实际开发中,嵌套调用里可以用链式访问,链式访问的底层也可能是嵌套调用
六、函数的声明与定义
6.1 声明的必要性
C语言编译器从上到下扫描编译源代码,若函数调用在前,定义在后,编译器遇到调用时未找到函数定义,会报"隐式声明"警告或直接报错。解决方法:在函数调用之前,先对函数进行声明。
6.2 函数声明的语法
// 写法1(推荐,清晰):保留形参名和类型
返回值类型 函数名(形参类型1 形参名1, 形参类型2 形参名2, ...);
// 写法2(简洁):仅保留形参类型,省略形参名
返回值类型 函数名(形参类型1, 形参类型2, ...);
// 示例:add函数的两种声明方式
int add(int a, int b); // 写法1
int add(int, int); // 写法2
6.3 声明与定义的位置示例
示例1:单文件场景(调用在前,定义在后)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 函数声明:调用前声明
int add(int a, int b);
int main() {
int res = add(1, 2); // 函数调用(在前)
printf("和为:%d", res);
return 0;
}
// 函数定义:实现功能(在后)
int add(int a, int b) {
return a + b;
}
示例2:多文件场景(企业标准)
add.h(头文件:仅声明)
int add(int a, int b); // 函数声明
add.c(源文件:仅实现)
#include "add.h"
int add(int a, int b) {
return a + b;
}
main.c(主文件:调用函数)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "add.h"
int main() {
printf("3+4=%d", add(3,4)); // 输出7
return 0;
}
七、static与extern(跨文件作用域控制)
7.1 extern:实现跨文件访问
核心作用:声明外部符号(全局变量、函数),告诉编译器该变量/函数在其他.c文件定义。
示例:extern修饰全局变量
add.h
extern int g_num; // 声明:g_num在其他.c文件定义
int add(int a, int b);
add.c
#include "add.h"
int g_num = 100; // 唯一定义:分配内存并初始化
int add(int a, int b) {
return a + b;
}
main.c
#include <stdio.h>
#include "add.h"
int main() {
printf("跨文件访问g_num:%d\n", g_num); // 输出100
g_num = 200;
printf("修改后g_num:%d\n", g_num); // 输出200
return 0;
}
7.2 static:实现多文件封装隔离
核心作用:限制符号的作用域为当前.c文件,成为当前文件的"私有财产",其他文件无法访问。
示例1:static修饰全局变量
#include "add.h"
static int g_num = 100; // 仅add.c可用,其他文件无法访问
int add(int a, int b) {
return a + b;
}
示例2:static修饰函数
#include "add.h"
int g_num = 100;
static int add(int a, int b) { // 仅add.c可用,其他文件无法调用
return a + b;
}
八、数组传参
8.1 一维数组传参
合法写法
// 写法1(推荐):省略数组长度
void printArr(int arr[], int len);
// 写法2:写原数组长度
void printArr(int arr[5], int len);
关键要求
函数内部无法计算出原数组的真实长度,因此传参时必须把数组长度作为第二个参数一起传给函数。
示例1:函数打印一维数组
#include <stdio.h>
void printArr(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[5] = {1,2,3,4,5};
int len = 5;
printArr(arr, len); // 输出1 2 3 4 5
return 0;
}
示例2:函数修改一维数组元素
#include <stdio.h>
void updateArr(int arr[], int len) {
for (int i = 0; i < len; i++) {
arr[i] *= 2;
}
}
int main() {
int arr[3] = {10,20,30};
int len = 3;
updateArr(arr, len);
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]); // 输出20 40 60
}
return 0;
}
8.2 二维数组传参
固定写法
// 写法1(推荐):省略行数,指定列数
void print2DArr(int arr[][3], int row);
// 写法2:指定行数和列数
void print2DArr(int arr[2][3], int row);
核心规则:形参必须指定第二维(列数),列数必须与原数组一致。
示例:函数打印二维数组
#include <stdio.h>
void print2DArr(int arr[][3], int row) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int arr[2][3] = {{1,2,3}, {4,5,6}};
int row = 2;
print2DArr(arr, row);
return 0;
}
九、常见错误汇总
9.1 语法类错误
- 函数体遗漏大括号{},或大括号不匹配
- 形参声明格式错误(如
int add(int x, y)) - 函数名与C语言关键字重名
- 函数嵌套定义
9.2 参数相关错误
- 实参的类型、个数、顺序与形参不一致
- 传参时漏写实参、多写实参
- 试图在函数头中直接初始化形参(C语言不支持)
9.3 返回值相关错误
- void类型函数书写
return 数值; - return语句后的值类型与函数头声明的返回值类型不匹配
- 有返回值的函数未书写return语句
9.4 函数调用相关错误
- 未声明函数就直接调用
- 调用函数时拼写错误函数名
- 数组传参时未传长度/行数,导致数组遍历越界
十、核心总结
10.1 核心要素
- 自定义函数的完整结构:函数头(返回值类型 + 函数名 + 形参列表)+ 函数体{}
- 值传递的参数传递规则:形参是实参的拷贝,修改形参不影响原实参(与数组传参区分)
- return语句与返回值的正确使用:终止函数 + 返回结果,void函数无返回值
- 函数调用的语法规范:普通调用/嵌套调用的规则,有/无返回值的不同调用方式
10.2 核心考点
- 能独立编写自定义函数实现简单功能(求和、求最大值、数组遍历/修改等)
- 能正确区分并使用形参和实参,规避形实参不匹配的错误
- 能灵活调用有返回值/无返回值函数,掌握嵌套调用和链式访问的基础用法
- 熟记stdio.h/string.h常用库函数的使用方法,会引入对应头文件
- 掌握一维/二维数组的传参写法,理解函数内操作原数组的核心特性
- 了解static/extern的基本用法,掌握多文件开发的声明/定义规范
10.3 编程思想
- 代码复用思想:一次定义,多次调用,避免重复编写相同代码,减少冗余
- 模块化编程思想:单个函数实现单一功能,将复杂程序拆分为多个简单的功能模块(函数),让程序结构清晰、便于调试和维护
十一、寄语
今天关于C语言函数的讨论总结就结束了,让我们不忘初心,砥砺前行!
补充内容(拓展考点 + 企业级规范)
一、主文章多余示例迁移
1. 链式访问 - 多层链式示例
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int main() {
// 先add(2,3)=5 → 再mul(5,4)=20 → 直接打印20
printf("结果:%d", mul(add(2, 3), 4)); // 输出20
return 0;
}
2. 一维数组传参 - 求和示例
#include <stdio.h>
// 功能:求数组和,返回int类型结果
int sumArr(int arr[], int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += arr[i];
}
return sum;
}
int main() {
int arr[4] = {1,3,5,7};
int len = 4;
int total = sumArr(arr, len); // 接收返回值
printf("数组和:%d", total); // 输出16
return 0;
}
二、break与return的核心区别
| 对比维度 | break | return |
|---|---|---|
| 作用目标 | 针对当前层循环/switch语句 | 针对整个当前函数 |
| 执行后流程 | 跳出当前循环/switch,函数继续执行后续代码 | 直接终止当前函数,返回到主调函数的调用位置,函数内后续代码全不执行 |
| 返回值 | 无返回值,仅做跳转 | 可带值(非void函数必须带),void函数仅能写return; |
| 嵌套场景 | 仅跳出最近一层循环/switch,不影响外层 | 无视任何嵌套(循环/函数),直接结束当前函数 |
示例对比:
#include <stdio.h>
void test_break() {
for (int i = 0; i < 3; i++) {
if (i == 1) break; // 跳出当前循环
printf("i = %d\n", i);
}
printf("break后,函数继续执行\n"); // 会执行
}
void test_return() {
for (int i = 0; i < 3; i++) {
if (i == 1) return; // 直接终止函数
printf("i = %d\n", i);
}
printf("return后,函数不会执行\n"); // 不会执行
}
int main() {
test_break();
test_return();
return 0;
}
三、static的额外实用特性:修饰局部变量
核心特性:
- 内存变化:局部变量从栈内存转到静态内存,函数调用结束后不会销毁,常驻内存
- 初始化规则:仅初始化一次(第一次调用函数时执行),后续调用直接使用上一次的结果
- 作用域不变:仍为函数局部,只能在函数内访问,外部无法使用
实用示例:统计函数调用次数
#include <stdio.h>
void count() {
static int cnt = 0; // 静态局部变量,只初始化一次,常驻内存
cnt++;
printf("函数被调用%d次\n", cnt);
}
int main() {
count(); // 第1次调用,输出1
count(); // 第2次调用,输出2
count(); // 第3次调用,输出3
return 0;
}
四、企业级多文件开发补充规范
1. 头文件(.h)保护机制
#ifndef __ADD_H__
#define __ADD_H__
// 头文件核心内容:函数声明、extern变量声明、宏定义、类型声明
extern int g_num;
int add(int a, int b);
#endif
2. .h/.c文件严格分工
- 头文件(.h):可放函数声明、extern变量声明、宏定义、类型声明;禁放函数实现、全局变量定义、static变量/函数声明
- 源文件(.c):用#include "xxx.h"引用自定义头文件;不需要跨文件的全局变量/函数,一律加static封装
3. 全局变量使用原则
- 全局变量尽量少用(易导致程序状态混乱)
- 若必须使用,优先用static封装,通过接口函数实现跨文件的读写
- 全局变量只能定义一次,放在唯一的.c文件中
4. static/extern核心避坑点
- 禁止用extern定义变量:
extern int g_num=10;是错误写法 - static不能修饰函数参数
- extern不能修饰局部变量
- 不同.c文件的static全局变量/函数可重名
五、库函数快速参考
stdio.h常用库函数:
printf(const char *format, ...):格式化输出到控制台scanf(const char *format, ...):格式化从控制台输入getchar():从控制台读取单个字符putchar(int c):向控制台输出单个字符puts(const char *s):输出字符串,自动换行fgets(char s, int size, FILE stream):安全的字符串输入
string.h常用库函数:
strlen(const char *s):计算字符串有效长度(不含结束符'\0'),返回unsigned intstrcpy(char dest, const char src):将字符串src拷贝到deststrcmp(const char s1, const char s2):按ASCII码比较两个字符串strcat(char dest, const char src):将字符串src拼接到dest末尾