← 目录 / 第八章 · 函数 / 8.2 函数参数详解

8.2 函数参数详解

参数是函数与外界沟通的桥梁。本节讲清楚值传递、引用传递、指针传递这三种"数据怎么送进函数"的方式,以及能让调用更省事的默认参数。

三种传递方式总览

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}
TryDouble(a) 调用瞬间 —— a 和 x 是两块完全独立的内存
a(main 里)
5
0x1000
x(函数里的副本)
5 → 10
0x2000(另一块地址)
a 的地址是 0x1000,从头到尾没变过
x 是另一块地址 0x2000,被改成 10 也不会影响 a
📌
内存模型:值传递会在栈上额外分配一块空间存放副本。对于 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}
RealDouble(a) 调用瞬间 —— a 和 x 其实是同一块内存的两个名字
a(main 里)/ x(引用形参)
5 → 10
同一个地址 0x1000
只有一块内存,只是同时挂了 a、x 两个名字牌
不管通过哪个名字改,改的都是同一个值
💡
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}
PtrDouble(&a) 调用瞬间 —— x 里存的不是数值,而是 a 的地址
x(指针形参)
0x1000
0x2000
a(main 里)
5 → 10
0x1000
x 自己也占一块内存(0x2000),但存的内容是 a 的地址
*x 就是"顺着箭头走过去",操作箭头指向的 a
⚠️
指针 vs 引用的区别:引用一旦绑定就不能改变指向,而指针可以指向不同变量,也可以为 nullptr(空指针)。因此指针比引用更灵活,但也更危险——解引用空指针会导致程序崩溃。
📖
本节只介绍参数传递中指针的基本用法。指针的完整机制(指针运算、动态内存、多级指针等)将在 第九章「指针与引用」 中系统讲解。

三种方式横向对比

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

默认参数

有些参数在大多数情况下都传同一个值,只有极少数情况才需要改。C++ 允许在定义函数时,给形参预设一个默认值——调用时如果省略这个实参,就自动使用默认值;如果传了,就用传入的值覆盖默认值。

C++ · 默认参数基本用法
1void PrintLine(string msg, char sep = '-') // sep 有默认值 '-'
2{
3 for (int i = 0; i < 20; i++) cout << sep;
4 cout << endl << msg << endl;
5}
6
7int main()
8{
9 PrintLine("Hello"); // 省略 sep,自动用默认值 '-'
10 PrintLine("World", '*'); // 传入 '*',覆盖默认值
11 return 0;
12}
📌
可以理解成"预填好的表格":默认参数就像一张已经帮你填好大部分内容的表格——大多数情况直接交上去就行(省略该实参),只有个别情况需要改一下再交(显式传入新值)。

规则一:默认参数必须从右往左连续设置

一旦某个参数有了默认值,它右边的所有参数也必须有默认值,不能中间空出一个没有默认值的参数——因为调用时省略的实参只能从右边开始省略,编译器没法猜中间空缺的是哪个。

C++ · 默认参数的顺序规则
1// ✅ 正确:默认参数从右往左连续
2void F1(int a, int b = 1, int c = 2);
3
4// ❌ 错误:a 有默认值,但右边的 b 没有——编译不通过
5void F2(int a = 1, int b);

规则二:默认值只能出现一次

如果函数既有声明又有定义(见 8.1.3 节),默认值只能写在其中一处,通常写在声明里,定义处不再重复写默认值——写两遍反而会导致编译错误。

1
调用时不能跳过中间参数:只能从右往左省略实参,不能"只省略中间那个"。比如 F1(int a, int b=1, int c=2),调用 F1(5, , 10) 是非法的,必须写全 F1(5, 1, 10)
2
默认值别写重了:声明处写了 int b = 1,定义处又写一遍 int b = 1,编译器会报"重复默认参数"错误——定义处直接写 int b 即可。
3
默认参数 + 函数重载可能产生二义性:如果同时存在 void F(int a)void F(int a, int b = 5),调用 F(10) 时两个函数都能满足调用,编译器不知道选哪个,会报错。下一节「函数重载」会详细展开这个陷阱。