一、回顾与本篇目标
上一篇你学会了 C 语言中的字符串——本质上是以 \0 结尾的字符数组。你知道了怎么声明字符串、怎么用 strlen 和 strcmp 等标准库函数处理字符串。
其实从第一篇开始,你就在用函数了——main 函数、printf 函数、scanf 函数。但这些都是别人写好的函数。这一篇我们要学的是你自己怎么写函数——把一段逻辑封装起来,起个名字,需要的时候直接调用。
如果你已经跟过《Python 零基础入门》的函数篇,或者写过 JavaScript 的函数,这一篇的核心概念你会觉得很熟悉。C 语言的函数在思路上和它们一致,但在传参机制上有根本的不同——C 语言只有一种参数传递方式:传值。理解这一点,是写好 C 函数的关键。
本篇的目标:
- 学会 C 语言函数的定义和调用语法
- 理解函数原型的作用
- 彻底搞懂传值调用的本质
- 掌握数组作为函数参数的用法
- 学会递归函数的基本思想
二、函数的定义和调用
C 语言中定义一个函数的基本语法和 JavaScript 非常相似:
返回类型 函数名(参数列表) {
// 函数体
return 返回值; // 如果返回类型是 void,可以省略 return
}
最简单的函数:没有参数,没有返回值
#include <stdio.h>
// 定义一个函数:打印一条分割线
void print_line() {
printf("============================\n");
}
int main() {
printf("这是第一段内容\n");
print_line(); // 调用函数
printf("这是第二段内容\n");
print_line(); // 再次调用
return 0;
}
逐行解释:
void print_line():void表示“没有返回值”。print_line是函数名。空括号表示没有参数。print_line():函数名加括号就是调用这个函数。程序执行到这里会跳进函数体,执行完再跳回来。
带参数和返回值的函数
#include <stdio.h>
// 定义一个函数:计算两个整数的和
int add(int a, int b) {
int result = a + b;
return result; // 把结果返回给调用者
}
int main() {
int sum = add(10, 20); // 调用 add,把返回值赋给 sum
printf("10 + 20 = %d\n", sum); // 30
printf("5 + 8 = %d\n", add(5, 8)); // 直接使用返回值
return 0;
}
C 函数和 JavaScript/Python 函数的对比:
| 要素 | JavaScript | Python | C |
|---|---|---|---|
| 定义关键字 | function 或箭头函数 |
def |
无关键字,直接写返回类型 |
| 参数类型 | 不需要声明 | 不需要声明 | 必须声明类型 |
| 返回类型 | 不需要声明 | 不需要声明 | 必须声明 |
| 代码块 | 花括号 {} |
缩进 + 冒号 | 花括号 {} |
最大的区别:C 语言的函数必须声明参数类型和返回类型。 编译器需要这些类型信息来检查类型错误和生成正确的机器指令。
三、函数原型:先声明,后使用
C 语言有一个重要的规则:函数在被调用之前,必须已经被编译器“认识”——要么已经定义过,要么已经声明过。这和 Python 不一样(Python 中函数定义可以放在调用之后)。
假设你把自定义的函数写在 main 函数之后:
#include <stdio.h>
int main() {
int sum = add(10, 20); // 编译错误!编译器不认识 add
printf("%d\n", sum);
return 0;
}
int add(int a, int b) {
return a + b;
}
编译器从上往下读代码,读到 add(10, 20) 时还没见过 add,就会报错。解决方案是在调用之前加上函数原型:
#include <stdio.h>
// 函数原型:告诉编译器有这个函数,但具体实现在后面
int add(int a, int b);
int main() {
int sum = add(10, 20); // 编译通过!
printf("%d\n", sum);
return 0;
}
// 函数的具体实现
int add(int a, int b) {
return a + b;
}
函数原型就是函数的第一行加上分号:返回类型 函数名(参数类型列表);。它只告诉编译器“有这个函数”,不关心函数体是什么。函数体可以写在后面的任何地方。
这就像你在文章的目录里先列出所有章节的标题,读者可以先知道全文结构,具体内容在后面展开。
四、传值调用:C 语言唯一的参数传递方式
这是 C 语言函数中最重要、也最容易被误解的概念。C 语言中,所有的函数参数都是“传值”的。
“传值”是什么意思?当你调用 add(x, y) 时,不是把 x 和 y 这两个变量本身传给函数,而是把 x 和 y 的值复制一份,传给函数的参数 a 和 b。 函数内部操作的是这份副本,原始变量完全不受影响。
#include <stdio.h>
// 这个函数试图修改变量的值
void try_to_change(int a) {
a = 100; // 修改的是副本
printf("函数内部:a = %d\n", a); // 100
}
int main() {
int x = 10;
try_to_change(x); // 把 x 的值(10)复制给参数 a
printf("函数外部:x = %d\n", x); // 10 —— 没有变!
return 0;
}
输出:
函数内部:a = 100
函数外部:x = 10
这就像你把一份文件复印了一份,把复印件给了同事。同事在复印件上写满了笔记,你的原文件还是一张白纸。
那怎么让函数修改外部变量?传指针!
如果函数需要修改外部变量的值,就传这个变量的地址(指针)。函数拿到地址后,通过解引用直接操作原始变量:
#include <stdio.h>
// 参数是指针:接收一个地址
void try_to_change(int *a) {
*a = 100; // 通过地址修改原始变量
printf("函数内部:*a = %d\n", *a); // 100
}
int main() {
int x = 10;
try_to_change(&x); // 传 x 的地址
printf("函数外部:x = %d\n", x); // 100 —— 真的变了!
return 0;
}
“传指针”本质上也是传值——把地址值(一个数字)复制一份传给参数。只不过这个地址值让你能访问到原始变量。所以严格来说,C 语言只有传值,没有传引用(C++ 才有引用类型)。
什么时候需要传指针?
- 函数需要修改外部变量的值(如
swap)。 - 需要传递大块数据(如数组、结构体),避免复制整个数据块。
- 需要通过“输出参数”返回多个值。
五、数组作为函数参数
把数组传给函数时,实际上传的是指向数组首元素的指针。在函数参数中,int arr[] 和 int *arr 是完全等价的。
#include <stdio.h>
// 以下三种写法完全等价
void print_array(int arr[], int length) { /* ... */ }
void print_array(int *arr, int length) { /* ... */ }
void print_array(int arr[10], int length) { /* ... */ } // 10 会被忽略
// 实际使用
void print_array(int arr[], int length) {
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int scores[] = {85, 92, 78, 95};
int len = sizeof(scores) / sizeof(scores[0]);
print_array(scores, len); // 传数组名即可
return 0;
}
为什么函数内部需要额外传长度?
因为数组传进函数后,sizeof(arr) 不再是整个数组的大小,而是指针本身的大小(8 或 4 字节)。所以在 C 语言中,数组作为函数参数时必须同时传递长度。
void test(int arr[]) {
printf("sizeof(arr) = %d\n", (int)sizeof(arr)); // 输出 8(指针大小),不是数组大小!
}
六、递归函数
递归就是函数自己调用自己。它适合解决那些可以分解为“更小规模的同类问题”的任务。
经典示例:计算阶乘
#include <stdio.h>
int factorial(int n) {
if (n <= 1) {
return 1; // 递归终止条件
}
return n * factorial(n - 1); // n! = n × (n-1)!
}
int main() {
printf("5! = %d\n", factorial(5)); // 120
return 0;
}
递归的执行过程(以 factorial(5) 为例):
factorial(5) → 5 * factorial(4)
factorial(4) → 4 * factorial(3)
factorial(3) → 3 * factorial(2)
factorial(2) → 2 * factorial(1)
factorial(1) → 1(终止条件,不再递归)
然后逐层返回:
factorial(2) → 2 * 1 = 2
factorial(3) → 3 * 2 = 6
factorial(4) → 4 * 6 = 24
factorial(5) → 5 * 24 = 120
递归的两个必备要素:
- 终止条件:告诉函数什么时候停止递归。没有终止条件会导致无限递归,最终栈溢出(程序崩溃)。
- 递归步骤:把问题分解为更小规模的同类问题。
另一个例子:斐波那契数列
int fibonacci(int n) {
if (n <= 0) return 0;
if (n == 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
注意:这个递归实现虽然简洁,但效率很低——同一个值会被重复计算很多次。实际项目中,斐波那契数列通常用循环实现,或者用“记忆化”优化递归。
七、局部变量和全局变量
局部变量
在函数内部声明的变量是局部变量,只能在那个函数内部访问。不同函数可以有同名的局部变量,它们互不影响:
void func_a() {
int x = 10;
printf("func_a: x = %d\n", x);
}
void func_b() {
int x = 20; // 和 func_a 的 x 是完全独立的
printf("func_b: x = %d\n", x);
}
全局变量
在所有函数外部声明的变量是全局变量,整个程序中的所有函数都可以访问它:
#include <stdio.h>
int counter = 0; // 全局变量
void increment() {
counter++;
}
int main() {
printf("初始:%d\n", counter); // 0
increment();
increment();
printf("之后:%d\n", counter); // 2
return 0;
}
全局变量的使用原则:尽量少用。全局变量会让函数之间产生隐式的耦合——任何函数都能修改它,出 bug 时很难定位是谁改的。能用参数传递解决的问题,就不要用全局变量。
八、综合演示:用函数重构学生成绩管理
下面这段代码把之前学生成绩管理的功能拆分成多个函数,每个函数只做一件事:
#include <stdio.h>
#define MAX_STUDENTS 100
// 函数原型
void input_scores(int scores[], int count);
int calculate_total(const int scores[], int count);
float calculate_average(int total, int count);
int find_max(const int scores[], int count);
int find_min(const int scores[], int count);
void print_report(const int scores[], int count, int total, float avg, int max, int min);
int main() {
int scores[MAX_STUDENTS];
int count;
printf("请输入学生人数(最多 %d):", MAX_STUDENTS);
scanf("%d", &count);
if (count <= 0 || count > MAX_STUDENTS) {
printf("无效的学生人数\n");
return 1;
}
input_scores(scores, count);
int total = calculate_total(scores, count);
float avg = calculate_average(total, count);
int max = find_max(scores, count);
int min = find_min(scores, count);
print_report(scores, count, total, avg, max, min);
return 0;
}
// 输入学生成绩
void input_scores(int scores[], int count) {
printf("请逐个输入 %d 个学生的成绩:\n", count);
for (int i = 0; i < count; i++) {
printf("学生 %d:", i + 1);
scanf("%d", &scores[i]);
}
}
// 计算总分
int calculate_total(const int scores[], int count) {
int total = 0;
for (int i = 0; i < count; i++) {
total += scores[i];
}
return total;
}
// 计算平均分
float calculate_average(int total, int count) {
return (float)total / count;
}
// 查找最高分
int find_max(const int scores[], int count) {
int max = scores[0];
for (int i = 1; i < count; i++) {
if (scores[i] > max) {
max = scores[i];
}
}
return max;
}
// 查找最低分
int find_min(const int scores[], int count) {
int min = scores[0];
for (int i = 1; i < count; i++) {
if (scores[i] < min) {
min = scores[i];
}
}
return min;
}
// 输出成绩报告
void print_report(const int scores[], int count, int total, float avg, int max, int min) {
printf("\n===== 成绩报告 =====\n");
printf("学生成绩:");
for (int i = 0; i < count; i++) {
printf("%d ", scores[i]);
}
printf("\n总分:%d\n", total);
printf("平均分:%.1f\n", avg);
printf("最高分:%d\n", max);
printf("最低分:%d\n", min);
}
代码设计要点:
- 每个函数只做一件事,函数名准确描述了它的功能。
const int scores[]:const关键字表示“这个数组在函数内部不会被修改”。这是良好编程习惯——读代码的人一看就知道这个函数只读数据,不会改数据。main函数变成了“总指挥”——调用各个子函数完成任务,自己不处理具体逻辑。#define MAX_STUDENTS 100:宏定义,相当于给 100 这个数字起了个名字。后面要用到数组大小时就用这个名字,修改时只需改一处。
九、本篇动手练习
练习 1:写一个计算器函数
新建 practice7-1.c,写一个函数 calculate(int a, int b, char operator),根据 operator 的值('+'、'-'、'*'、'/')返回对应的计算结果。在 main 中测试所有四种运算。
练习 2:数组处理函数
新建 practice7-2.c,写三个函数:reverse_array(反转数组)、sort_array(冒泡排序)、is_sorted(判断数组是否已排序)。在 main 中测试。
练习 3:递归求最大公约数
新建 practice7-3.c,用递归实现欧几里得算法求两个正整数的最大公约数。算法规则:gcd(a, b) = gcd(b, a % b),当 b == 0 时返回 a。
练习 4:字符串处理函数
新建 practice7-4.c,写三个函数:my_strlen(计算字符串长度)、my_strcpy(复制字符串)、my_strcat(拼接字符串)。用指针实现,不调用标准库的 string.h。
十、本篇小结
这一篇你学会了 C 语言函数的完整知识:
- 函数的定义:
返回类型 函数名(参数列表) { 函数体 }。每个参数必须声明类型,返回类型也必须声明。没有返回值用void。 - 函数原型:在调用函数之前先声明函数的签名,让编译器知道函数的存在。函数体可以在后面。
- 传值调用:C 语言只有传值。函数参数是实参的副本,修改参数不影响原始变量。要修改外部变量,必须传指针。
- 数组作为参数:数组传进函数后变成指针,
sizeof失效。必须额外传递数组长度。 - 递归函数:函数调用自己。必须有终止条件,否则会栈溢出。递归代码简洁,但要注意效率。
- 变量作用域:局部变量只能在函数内访问,全局变量全程序可访问(应尽量少用)。
函数是编程中最核心的抽象工具。把一个大问题拆成多个小函数,每个函数只做一件事,代码就会变得清晰、可测试、可复用。下一篇,我们学习 C 语言中另一个重要的概念——结构体,它让你能把不同类型的数据打包成一个整体,是 C 语言面向对象编程的基础。
下一篇预告
下一篇——《结构体——把不同类型的数据打包在一起》:结构体的定义和初始化、访问结构体成员、结构体指针、结构体作为函数参数、typedef 简化类型名。结构体是 C 语言面向对象编程的基础,也是理解 C++ 类的前置知识。
C/C++ 零基础入门,每周更新。












暂无评论内容