← 目录 / C++ 编程语法

八、函数

函数是将特定功能的代码打包封装、起名复用的机制。好的函数让程序结构清晰,有效避免重复代码。本章还将介绍递归这一强大技术。

函数的定义

函数是对实现某一功能的代码进行模块化封装。定义格式如下:

int   Add ( int a, int b { return a + b; }
返回值类型 — 函数返回什么类型的数据
函数名 — 调用时使用的名字
参数列表 — 传入的数据(类型 + 名称)
函数体 — 具体执行的代码
组成部分说明是否必须
返回值类型函数执行完毕后返回的数据类型,如 intdoublestring✅ 必需(无返回值写 void
函数名调用函数时使用的名字,命名规则同变量名✅ 必需
参数列表调用时传入的数据,需指定类型和名称,多个参数用逗号分隔可选(无参数时留空)
函数体花括号 {} 内要执行的具体代码✅ 必需
return 语句返回计算结果并结束函数;返回值类型为 void 时可省略视返回类型而定
C++ · 函数定义示例
1// 示例1:计算两数之和(有返回值)
2int Add(int a, int b) // 返回类型 int,两个 int 参数
3{
4 int result = a + b;
5 return result; // 返回计算结果
6}
7
8// 示例2:打印问候语(无返回值)
9void Greet(string name) // 返回类型 void,表示不返回值
10{
11 cout << "Hello, " << name << "!" << endl;
12 // 无需 return
13}
💡
命名建议:函数名应见名知义,用动词或动词短语描述其功能(如 AddGetMaxPrintResult)。多个参数即使类型相同也不能省略类型声明,必须写 int a, int b,不能写 int a, b

函数的调用

调用函数就是使用函数的功能来执行代码或获取返回值。在函数名后跟圆括号,括号内传入实参,多个参数用逗号分隔。

main() → 调用 → Add(3, 5) → 返回 → 8 // 用变量接收:int sum = Add(3, 5);
main() → 调用 → Add(3, 5) * 2 → 直接用于表达式 → 16
main() → 调用 → Greet("小明") // void 函数:作为独立语句调用

形参与实参

形参形式参数
函数定义时括号内的参数,规定调用者需要传入什么类型的数据。只是一个"占位符",没有实际的值。
实参实际参数
函数调用时实际传入的具体值或变量。实参的数量、顺序、类型必须与形参一一对应。
C++ · 形参 vs 实参
1int Add(int a, int b) // a、b 是形参(定义处)
2{ return a + b; }
3
4int main()
5{
6 int x = 3, y = 5;
7 int sum = Add(x, y); // x、y 是实参(变量)
8 Add(10, 20); // 10、20 也是实参(字面量)
9 return 0;
10}

函数的声明

C++ 要求函数必须先定义(或声明)后调用。如果函数定义在 main() 之后,需要在前面写函数声明(也叫函数原型),提前告诉编译器这个函数的存在。

概念格式作用
函数声明(原型)返回类型 函数名(参数列表);告诉编译器函数"长什么样",没有具体实现代码
函数定义(实现)返回类型 函数名(参数列表) { 函数体 }给出函数的完整实现代码
C++ · 函数声明示例
1#include <iostream>
2using namespace std;
3
4int Add(int a, int b); // ← 函数声明(只写函数头,不写函数体)
5 // 参数名可省略:int Add(int, int);
6
7int main()
8{
9 cout << Add(3, 5) << endl; // 正常调用
10 return 0;
11}
12
13int Add(int a, int b) // ← 函数定义(在 main 之后也没问题)
14{
15 return a + b;
16}

函数参数详解

C++ 中函数参数共有三种传递方式:值传递引用传递指针传递。三者的核心区别在于:函数内对参数的操作,是否能影响到调用方的原始变量。

📋① 值传递
传入原变量的拷贝副本,函数内的修改与原变量完全隔离。

写法:int x
调用:f(a)
❌ 不影响原变量
🔗② 引用传递
传入原变量的别名,函数内操作参数就是操作原变量本身。

写法:int &x
调用:f(a)(同值传递,自动绑定)
✅ 直接修改原变量
📍③ 指针传递
传入原变量的内存地址,函数内通过解引用 * 修改原变量。

写法:int *x
调用:f(&a)(需取地址)
✅ 可修改原变量

① 值传递(Pass by Value)

最常用的默认方式。调用时将实参的值复制一份传给形参,两者互不干扰。适合传递小型数据(int、double、char 等)且不需要修改原值的场景。

C++ · 值传递
1void TryDouble(int x) // x 是副本
2{
3 x = x * 2; // 只改了副本
4 cout << "函数内 x = " << x; // 10
5}
6
7int main()
8{
9 int a = 5;
10 TryDouble(a); // 传入 a 的副本
11 cout << "函数外 a = " << a; // 仍是 5,原变量未变
12 return 0;
13}
📌
内存模型:值传递会在栈上额外分配一块空间存放副本。对于 int 这样的小型类型开销极小;但若传递大型结构体或 string,拷贝成本较高,此时应改用引用传递。

② 引用传递(Pass by Reference)

在参数类型后加 &,函数接收的是原变量的别名——形参和实参指向同一块内存,对形参的任何操作都直接作用于原变量。调用时写法和值传递完全相同,& 只写在函数定义处。

C++ · 引用传递
1void RealDouble(int &x) // & 表示引用,x 就是原变量的别名
2{
3 x = x * 2; // 直接修改原变量
4 cout << "函数内 x = " << x; // 10
5}
6
7// 经典用途:交换两个变量
8void Swap(int &a, int &b)
9{
10 int tmp = a;
11 a = b; b = tmp;
12}
13
14int main()
15{
16 int a = 5;
17 RealDouble(a); // 调用写法与值传递相同
18 cout << "函数外 a = " << a; // 10,原变量已修改
19
20 int x = 3, y = 7;
21 Swap(x, y);
22 cout << x << " " << y; // 7 3
23 return 0;
24}
💡
const 引用:只读不改
如果只想避免大对象拷贝、但不需要修改原值,可以用 const int &x。编译器会阻止你在函数内修改 x,既安全又高效。

③ 指针传递(Pass by Pointer)

将原变量的内存地址传入函数,形参是一个指针变量(int *x)。函数内需要用解引用运算符 *x 才能访问或修改原变量的值。调用时须用取地址运算符 &a 传入地址。

指针传递与引用传递效果相同(都能修改原变量),但写法更繁琐。在 C++ 中优先使用引用传递;指针传递主要出现在与 C 语言兼容、或需要传递数组的场景。

C++ · 指针传递
1void PtrDouble(int *x) // x 是指向 int 的指针
2{
3 *x = *x * 2; // 解引用:通过地址修改原变量
4 cout << "函数内 *x = " << *x; // 10
5}
6
7// 经典用途:指针版 Swap
8void PtrSwap(int *a, int *b)
9{
10 int tmp = *a;
11 *a = *b; *b = tmp;
12}
13
14int main()
15{
16 int a = 5;
17 PtrDouble(&a); // 传地址:&a
18 cout << "函数外 a = " << a; // 10,原变量已修改
19
20 int x = 3, y = 7;
21 PtrSwap(&x, &y); // 注意:传 &x 不是 x
22 cout << x << " " << y; // 7 3
23 return 0;
24}
⚠️
指针 vs 引用的区别:引用一旦绑定就不能改变指向,而指针可以指向不同变量,也可以为 nullptr(空指针)。因此指针比引用更灵活,但也更危险——解引用空指针会导致程序崩溃。

三种方式横向对比

对比项值传递引用传递指针传递
形参写法 int x int &x int *x
调用写法 f(a) f(a) f(&a)
函数内访问 x(操作副本) x(操作原变量) *x(解引用操作原变量)
能否修改原变量 ❌ 不能 ✅ 能 ✅ 能
是否产生拷贝 ✅ 产生(开销大) ❌ 不产生 ❌ 不产生(仅传地址)
可以为空 ❌ 引用必须绑定有效变量 ✅ 可以是 nullptr
推荐使用场景 读取小型数据、不需修改原值 需修改原值,或传递大对象(加 const) 与 C 语言兼容、传递数组、动态内存
💡
参数选择建议:
· 只读小数据 → 值传递
· 需要修改原值 / 传大对象 → 引用传递(最常用)
· 传大对象但不修改 → const 引用(最优雅)
· 传数组 / 与 C 风格库交互 → 指针传递
📖
本章只介绍参数传递中指针的基本用法。指针的完整机制(指针运算、动态内存、多级指针等)将在 第九章「指针与引用」 中系统讲解。

递归函数

我们知道函数可以调用其他函数——那函数能不能调用自己?答案是可以,这就是递归

以计算 4!(4的阶乘)为例:4! = 4 × 3!,而 3! = 3 × 2!2! = 2 × 1!1! = 1。问题的规模在每一步都缩小了一级,直到答案已知为止。这种"把大问题分解成同结构的小问题"的思路,正是递归的本质。

一个正确的递归函数必须具备两个要素,缺一不可:

① 递归边界(终止条件)
让递归能够停下来,防止无限递归。没有终止条件 = 程序崩溃。
② 递归推进
每次调用时让问题规模向边界靠近(如 n 减小),最终达到终止条件。
C++ · 递归示例(阶乘 & 求和)
1// 计算 n!(n 的阶乘)
2int Factorial(int n)
3{
4 if (n == 1) return 1; // ① 边界:1! = 1
5 return n * Factorial(n - 1); // ② 推进:n! = n × (n-1)!
6}
7
8// 计算 1+2+...+n
9int Sum(int n)
10{
11 if (n == 1) return 1; // ① 边界
12 return n + Sum(n - 1); // ② 推进:Sum(n) = n + Sum(n-1)
13}
14
15cout << Factorial(5); // 120
16cout << Sum(10); // 55

Factorial(4) 的调用栈展开

⬇ 递推阶段(逐层调用)
Factorial(4) = 4 × Factorial(3)
Factorial(3) = 3 × Factorial(2)
Factorial(2) = 2 × Factorial(1)
Factorial(1) = 1 ← 触底!边界条件
触底
回溯
⬆ 回归阶段(逐层返回)
返回 1 1! = 1
返回 2×1 = 2 2! = 2
返回 3×2 = 6 3! = 6
返回 4×6 = 24 4! = 24 ✓
⚠️
三条递归注意事项:
① 必须有明确的终止条件,否则无限递归会导致栈溢出(程序崩溃)。
② 递归层数不能太深。竞赛中若递归深度超过约 10⁵~10⁶ 层,需改用循环或手动模拟栈。
③ 递归代码简洁,但每次函数调用都有额外开销。大规模问题优先考虑循环迭代替代递归。