函数
掌握函数的定义、调用、参数传递(值/引用/指针/const)、返回类型、重载与内联——把代码组织成可复用的模块
学习目标
- 能正确定义和调用一个函数——写出返回类型、形参列表和函数体,并能解释实参到形参的值传递过程
- 能比较并选择值传递、引用传递或 const 引用传递,并通过重载为同一个操作提供不同参数类型的版本
- 能回答:
void reset(int &x) { x = 0; }和void reset(int x) { x = 0; }在调用int a = 5; reset(a);之后,a 的值分别是什么?为什么?
机制总览
函数:机制路径
- 1
直觉:你的代码需要一个“加工车间”
到目前为止你写的程序都可以装在一个 main() 里面——所有代码从上到下一口气跑完。但真实世界的软件不是这样的:一个程序可能有几千行甚至几百万行代码——如果全挤在一个大块里,你找不到哪段代码在干什么、也没法说「同样的逻辑别写两遍,下次直接调用」。
- 2
函数是什么:四个零件拼出一个加工车间
// 调用 —— 把材料送进去、拿成品出来 int main() int sum = add(3, 5); // 3 和 5 是实参——实际的原材料 std::cout << sum; // 输出 8 return 0;
- 3
参数传递:拷贝还是别名?——这是整章最重要的一节
上一节说了函数调用时实参会被「拷贝」给形参。但 C++ 给了你三种传递原材料的方式—— 什么时候用哪一种,是这章最重要的决策 。
章级决策实验
函数:机制与证据
切换《函数》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 直觉:你的代码需要一个“加工车间”
到目前为止你写的程序都可以装在一个 main() 里面——所有代码从上到下一口气跑完。但真实世界的软件不是这样的:一个程序可能有几千行甚至几百万行代码——如果全挤在一个大块里,你找不到哪段代码在干什么、也没法说「同样的逻辑别写两遍,下次直接调用」。
可核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:你的代码需要一个“加工车间”」的契约。
学完《函数》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
函数:失效与核验
直觉:你的代码需要一个“加工车间”
典型失效
若把「直觉:你的代码需要一个“加工车间”」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:你的代码需要一个“加工车间”」的契约。
函数是什么:四个零件拼出一个加工车间
典型失效
若把「函数是什么:四个零件拼出一个加工车间」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「函数是什么:四个零件拼出一个加工车间」的契约。
参数传递:拷贝还是别名?——这是整章最重要的一节
典型失效
若把「参数传递:拷贝还是别名?——这是整章最重要的一节」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「参数传递:拷贝还是别名?——这是整章最重要的一节」的契约。
直觉:你的代码需要一个“加工车间”
到目前为止你写的程序都可以装在一个 main() 里面——所有代码从上到下一口气跑完。但真实世界的软件不是这样的:一个程序可能有几千行甚至几百万行代码——如果全挤在一个大块里,你找不到哪段代码在干什么、也没法说「同样的逻辑别写两遍,下次直接调用」。
想象你要给 100 个不同的零件打孔——你不会每次跑到厂房里手画图纸重新造一台钻床。你会造一台「钻床车间」——画好图纸、标好参数(材料进、钻头型号、打多深),之后每次只消把材料送进去就能拿到打好孔的成品。C++ 里的函数就是你的钻床车间——你把数据送进去,拿到计算结果出来,再也不用重写同样的代码。
如果没有函数会怎样?你会在 main 里反复写一样的逻辑——「计算平方」要写十几遍、「判断素数」每一处都拷贝粘贴——代码又长又难改。本章教你建造自己的加工车间:怎么写、怎么送材料进去、怎么拿成品出来——这是从「写顺序步骤」到「设计可复用的代码模块」的关口。
函数是什么:四个零件拼出一个加工车间
在 C++ 里定义一个函数需要四个零件——缺一个都不行:
↡C++ 中最基本的代码复用单元。由返回类型、函数名、形参列表和函数体四部分组成。函数定义是完整的实现;函数声明(只写前面三部分加分号)告诉编译器「有这个函数、后面会定义」。调用函数时实参的值被拷贝给形参(值传递),函数体执行完毕后把返回值(如果有的话)交还给调用方。——理解成一张车间蓝图:车间名字(函数名)、需要什么原材料(形参列表)、产出什么(返回类型)、以及怎么加工(函数体)。
把车间蓝图翻译成 C++ 代码——下面是一个完整的函数定义和调用:
// 函数定义 —— 车间蓝图
int add(int x, int y) { // int: 返回类型,return 的东西的类型
int result = x + y; // 形参 x, y: 原材料;函数体: 怎么加工
return result; // return: 把成品扔出去
}
// 调用 —— 把材料送进去、拿成品出来
int main() {
int sum = add(3, 5); // 3 和 5 是实参——实际的原材料
std::cout << sum; // 输出 8
return 0;
}整个过程分五步——看上面的 FunctionCallStackDiagram 图,每一步对应图上一个高亮部分:
- 调用前:
main()正常运行,栈上只有 main 的局部变量。 - 传参:实参 3 和 5 被拷贝给形参 x 和 y——这是关键:x 和 y 是 3 和 5 的副本,不在同一个内存位置。
- 执行:CPU 跳到 add 第一行——
int result = x + y,算出 result = 8。此时 main 处于暂停状态。 - 返回:
return result——把 8 放到约定的寄存器(eax/rax)里,add 即将退出。 - 栈帧销毁:add 的整个「车间」——形参 x、y 和局部变量 result——随着栈帧回收全部消失。CPU 回到 main 继续执行,把寄存器的 8 赋给 sum。
参数传递:拷贝还是别名?——这是整章最重要的一节
上一节说了函数调用时实参会被「拷贝」给形参。但 C++ 给了你三种传递原材料的方式——什么时候用哪一种,是这章最重要的决策。
↡把实参的值拷贝一份给形参。函数内修改形参不影响实参——因为改的是副本。适合小对象(int、double 等基本类型)或者函数确实需要独立副本来修改的场景。拷贝大对象(如 vector、string)开销大。——就是复印图纸:车间拿到的是原稿的复印件,在上面写字画图不影响原件。
↡形参用 T& 声明——不拷贝实参,而是让形参直接引用实参的内存。函数内修改形参就是修改实参。适合函数确实需要修改原值的场景(如 swap 交换两个变量)。不能接受字面值(f(5) 编译错误)——因为临时值没有有名字的内存地址可以引用。——就是直接把原件递给你:你往原件上写字,原件就变了。
↡形参用 const T& 声明——不拷贝实参(避免大对象拷贝开销),但也不能修改它。可以接受字面值和临时对象。函数的设计意图清晰——「我只读、不改」。是传递 string、vector、自定义类等大对象的首选方案。——就是给你看原件但不许动笔:你能读上面的全部内容,但不能做任何修改——同时省了复印的成本。
看上面的对比图——左边是值传递:实参 a 存 5 在地址 0x100,形参 x 存 5 的拷贝在地址 0x200——两块独立内存,改 x 不影响 a。右边是引用传递:int &x = a 让 x 和 a 共享同一块内存——修改 x 就是修改 a。
代码对照——三种传递方式的行为差异:
// ① 值传递:改不了原值
void addOne_val(int x) { x = x + 1; } // 改的是副本
// ② 引用传递:能改原值
void addOne_ref(int &x) { x = x + 1; } // 改的是原值本身
// ③ const 引用:只读不拷贝
void print_vec(const std::vector<int> &v) {
// v.push_back(1); ← 编译错误,const 不能改
std::cout << v.size(); // ✓ 只读 OK,且没拷贝整个 vector
}
int main() {
int a = 5;
addOne_val(a); // a 还是 5
addOne_ref(a); // a 变成 6
print_vec({1,2,3}); // const 引用能接受临时对象
return 0;
}把数组当参数传进去——一个你必须知道的大坑
现在你知道值传递会拷贝实参——那把整个数组拷一份传给函数行不行?不行。 C++ 在设计上不允许直接拷贝数组——所以「传数组」这件事有一个特殊的规则。
↡C++ 中数组作为函数形参时会自动转换成指向首元素的指针。void f(int arr[10]) 和 void f(int* arr) 完全等价——不管 [] 里写什么数,编译器都当 T* 处理。函数内部 sizeof(arr) 返回指针大小而非数组大小,必须单独传长度参数。C 和 C++ 的标准委员会几十年来一直在为「怎么安全传数组」争执——目前最安全的办法是用 std::array 或 std::vector。——数组一进函数就脱掉了「数组」的外套,只剩一根指向第一个元素的指针。函数永远不知道你传进来的数组有多长。
看上面的图——左边调用方有一个完整的 int arr[5] = {1,2,3,4,5},sizeof(arr) = 20 能算长度。传进函数后,形参 int arr[] 实际上是 int* arr——一根 8 字节的指针。函数内部的 sizeof(arr) 返回 8,不是 20——长度信息丢了。
代码验证——四种写法的形参完全等价:
// 下面四个函数声明——完全等价!都只收到一个 int* 指针
void print1(int arr[10]); // [10] 被编译器无视
void print2(int arr[5]); // [5] 也被无视
void print3(int arr[]); // 空括号同样退化为指针
void print4(int *arr); // 这才是它真正的样子
// 函数内部 sizeof(arr) = 8 —— 指针大小,不是数组大小
// 必须单独传长度:
void print_correct(int *arr, int n) {
for (int i = 0; i < n; i++) // n 是调用方传入的长度
std::cout << arr[i] << ' ';
}同一车间,多种订单:函数重载
有时候你需要「同一个操作名、但接受不同类型的原材料」。比如 max(3, 5) 比较两个 int、max(3.14, 2.71) 比较两个 double——你不想起两个名字叫 maxInt 和 maxDouble,而是想用一个 max 搞定。
↡C++ 允许同一作用域内有多个同名函数,只要它们的形参列表不同(数量、类型或顺序)。编译器通过比较实参和形参的类型在多个重载版本中选择最匹配的那一个。只改返回类型不算重载——重载只看形参列表。匹配过程分三步:候选函数(同名可见)→ 可行函数(数量对、类型能配)→ 最佳匹配(转换代价最小)。——就是把同一种操作注册为多个工单类型:车间看到来的是 int 就走 int 产线、来的是 double 就走 double 产线。
看上面的流程图——编译器做了三步筛选才能确定调用哪个重载:
- 候选:找所有叫同一个名字且可见的函数——不管形参类型对不对。
- 可行:筛出形参数量对得上、每个实参都能通过隐式转换配上对应形参的。
- 最佳匹配:在可行函数里挑隐式转换最少的那一个。
代码示例——用 max 重载处理 int 和 double:
// 两个重载:同名、形参类型不同
int max(int a, int b) {
return a > b ? a : b;
}
double max(double a, double b) {
return a > b ? a : b;
}
int main() {
auto r1 = max(5, 3); // 调用 max(int, int) → 5
auto r2 = max(3.14, 2.71); // 调用 max(double, double) → 3.14
return 0;
}重载的匹配有优先级——从高到低:① 精确匹配(类型完全一样)② const 转换 ③ 类型提升(int→long)④ 算术/指针转换 ⑤ 类类型转换。如果两个重载版本匹配得「一样好」——就会出现二义性(ambiguous),编译器报错。
特殊车间:内联函数和编译期函数
普通函数调用可能涉及参数传递、跳转与返回,但是否把函数体展开是优化器的决定。C++ 的 inline 首先是跨 translation unit 的 ODR/定义可见性机制,constexpr 则允许在常量表达式上下文中求值;二者都不承诺某种机器指令形态。
↡允许同一函数定义出现在多个 translation units,只要各定义相同;编译器可选择是否在调用点展开,与关键字本身无强制关系。通常定义在头文件中。它消除的是跨 translation units 重复定义的 ODR 障碍,不是强制消除调用开销。
↡用 constexpr 关键字声明的函数——暗示编译器在编译期就能算出结果。要求函数体足够简单(C++11 只允许一条 return 语句,C++14 放宽了),返回值和所有形参都必须是字面值类型。如果所有实参都是常量,编译器在编译期直接算出结果——零运行时开销。如果实参中有变量,退化为普通函数在运行时执行。——就是在工厂开工之前就把结果算好了。适合需要在编译阶段就确定值的场景——比如数组大小、模板参数。
// inline:一行小工具——建议编译器展开
inline int square(int x) {
return x * x; // 体积极小、调用频繁
}
// constexpr:在编译期就能算出来
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
int a = square(5); // 运行时或内联——看编译器心情
constexpr int b = factorial(5); // 编译期计算——b = 120
int arr[factorial(3)]; // 编译期算出来 6——用作数组大小
return 0;
}官方 Chapter 6 的完整函数契约
原书还要求 separate compilation、command-line options、varying parameters、复杂 return type、default arguments、debugging aids 与 pointers to functions。它们共同决定函数能否跨文件、跨调用形态稳定使用。
↡把声明放入头文件、定义放入源文件,并分别编译为目标文件后由链接器组合的组织方式。要求所有调用点看到相同 function declaration,且程序中 non-inline definition 只有一个。修改头文件通常需要重编译包含它的 translation units;只修改 .cpp implementation 可缩小重编译范围。Declaration 中 parameter names 可省略,但 return/parameter types、constexpr 等契约必须一致。
↡让函数接收数量可变的同类型实参;C++11 首选 initializer_list,省略号仅用于兼容特定低级接口。包括 std::initializer_list<T> 和 ellipsis。前者元素类型统一、可安全范围遍历;main(int argc, char* argv[]) 是固定的命令行参数接口,不是普通 variadic function。C 风格 ... 丢失静态类型信息,应限制在兼容接口。
↡返回引用、指针或数组指针等需要额外声明语法和生命周期证明的返回形式。必须先证明对象在函数返回后仍存在。返回 local object 的 reference/pointer 会悬空;按值返回通常由 copy elision 或 move 高效完成。返回数组指针可用 type alias 或 trailing return:
using Row = int[10];
Row* chooseRow(int (&matrix)[4][10], std::size_t index);
auto chooseRow2(int (&matrix)[4][10], std::size_t index) -> int (*)[10];↡在函数声明中为末尾形参提供的调用点默认值;调用省略对应实参时才使用,且同一作用域只指定一次。必须从 parameter list 右侧连续提供。Default expression 在调用时求值,名字按 declaration 所在 scope 绑定;不要在后续 declaration 重复写同一默认值。inline、constexpr、assert 与 NDEBUG 则属于 specialized uses:assert(expr) 在未定义 NDEBUG 时检查条件,不能承载必须发生的 side effect。
↡保存具有特定返回类型和形参列表的函数地址,可作为参数传递、选择并通过指针间接调用。类型由 return 与 parameter types 决定:
bool shorter(const std::string&, const std::string&);
using Compare = bool (*)(const std::string&, const std::string&);
Compare comparator = shorter;
bool result = comparator("alpha", "beta");
void sortWords(std::vector<std::string>& words, Compare compare);函数名在多数表达式中自动转换为 pointer。若名字 overloaded,目标 pointer type 可帮助选择唯一版本;否则要用 static_cast 明确。函数指针参数也可写成 function type,但形参会调整为 pointer type。
动手:一步步看函数调用在栈上发生了什么
每次函数调用 CPU 都在栈上做一套精准的机械动作——压入实参、创建局部变量、执行函数体、回收整个帧。下面把 int add(int x, int y) { int z = x + y; return z; } 的调用拆成五步——每一步对应 FunctionCallStackDiagram 图中高亮的那一帧。
猜一猜:
int add(int x, int y) { x = 100; return x + y; }调用add(3, 5)后,函数返回什么?调用方传进去的 3 在 add 里面被改成 100 之后,调用方手里的 3 受影响了吗?通过下面 Stepper 一步步看。
① 调用前:main 的栈帧
调用 add(3, 5) 之前——栈上只有 main() 的栈帧。main 的局部变量(如果有的话)舒舒服服待在帧里,add 还什么都没分到。
代码逐段拆解:把本章所有工具串起来
这一节覆盖本章函数定义、参数传递、引用与 const、数组、重载、inline/constexpr 的核心用法——每段一个主题,读完就能在自己的程序里设计和使用函数。
用引用参数实现真正的 swap
#include <iostream>
// 引用的经典用例——交换两个变量的值
void swap(int &a, int &b) {
int temp = a; // 把 a 存起来
a = b; // 把 b 写进 a —— 改的就是原值
b = temp; // 把暂存的 a 写进 b
}
int main() {
int x = 10, y = 20;
std::cout << "交换前: " << x << " " << y << '\n';
swap(x, y);
std::cout << "交换后: " << x << " " << y << '\n';
return 0;
}这是引用传递最经典的例子。如果形参没有 &——void swap(int a, int b)——交换的只是副本,x 和 y 纹丝不动。加上 & 之后,a 就是 x 的别名、b 就是 y 的别名——交换 a 和 b 就是交换 x 和 y。
const 引用 vs 值传递的性能对比
#include <iostream>
#include <string>
// 值传递:拷贝一整个 string——str 在这被完整复制了一份
void greet_copy(std::string str) {
std::cout << "你好," << str << "!\n";
}
// const 引用:不拷贝——直接引用调用方手里的 string
void greet_ref(const std::string &str) {
std::cout << "你好," << str << "!\n";
// str = "别的"; ← 编译错误——const 不能改
}
int main() {
std::string name = "张三李四王五赵六钱七孙八周吴郑王";
greet_copy(name); // 把整个 string 拷贝一份进栈
greet_ref(name); // 只传 8 字节引用,零开销
return 0;
}差别在哪?greet_copy 把整个 name 字符串(可能是几十到几千字节)完整拷贝一份到形参 str——内存和时间开销。greet_ref 只传一个 8 字节的引用——零拷贝。结论:凡是比 int/double 大的对象,默认用 const T& 传——除非函数确实需要修改原值或要改独立的副本。
遍历数组并返回统计结果
#include <iostream>
// 传数组 = 传指针 —— 必须同时传长度
void analyze(const int *arr, int n, int &sum, int &max) {
sum = 0;
max = arr[0];
for (int i = 0; i < n; i++) {
sum += arr[i];
if (arr[i] > max) max = arr[i];
}
}
int main() {
int scores[] = {88, 95, 73, 64, 91, 55};
int n = sizeof(scores) / sizeof(scores[0]);
int total, highest;
analyze(scores, n, total, highest);
std::cout << "总和: " << total << '\n';
std::cout << "最高: " << highest << '\n';
std::cout << "平均: " << static_cast<double>(total) / n << '\n';
return 0;
}设计要点:① const int *arr——只读指针,函数不修改数组内容 ② int &sum 和 int &max——引用参数作为「输出」渠道,把计算结果送出去 ③ C++ 风格里一个函数通常通过 return 返回一个值,用引用参数输出附加结果 ④ sizeof(scores)/sizeof(scores[0]) 只在定义数组的作用域里能算出长度——传给 analyze 后 sizeof(arr) 就是 8 了。
函数重载:同一个名字、不同类型
#include <iostream>
#include <string>
// 三个重载——同名、不同形参
void print(int x) {
std::cout << "整数: " << x << '\n';
}
void print(double x) {
std::cout << "浮点数: " << x << '\n';
}
void print(const std::string &s) {
std::cout << "字符串: " << s << '\n';
}
int main() {
print(42); // 匹配 print(int)
print(3.14); // 匹配 print(double)
print(std::string("hello")); // 匹配 print(const string&)
return 0;
}编译器根据实参类型自动选择匹配的版本。print(42) 里 42 是 int 字面值,选 print(int)。print("hello") 可以经 std::string 的非 explicit constructor 做一次 user-defined conversion,匹配 print(const std::string&);若同时存在更好的 print(const char*),则后者会胜出。这提示我们重载时要比较完整 conversion sequence。
用 constexpr 在编译期算阶乘
#include <iostream>
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
constexpr int f5 = factorial(5); // 编译期计算 = 120
int arr[factorial(4)]; // 编译期 24——用作数组大小
int user_input;
std::cin >> user_input;
int f_user = factorial(user_input); // 运行时计算——实参不是常量
std::cout << "5! = " << f5 << '\n';
std::cout << user_input << "! = " << f_user << '\n';
return 0;
}factorial(5) 在编译期展开为 5*4*3*2*1 并算出 120——运行时零开销。同时同一个函数在运行期对用户输入也能正常算——constexpr 函数根据实参是否常量自动切换计算时机。这是现代 C++ 的精妙设计。
静态局部变量:函数调用的记忆
#include <iostream>
// 每次调用都返回当前调用次数——static 变量跨调用保持值
int count_calls() {
static int call_count = 0; // 只在第一次调用时初始化一次
return ++call_count; // 之后每次调用 += 1
}
int main() {
for (int i = 0; i < 5; i++)
std::cout << count_calls() << ' ';
// 输出:1 2 3 4 5
return 0;
}static int call_count 是局部静态对象——只在第一次执行到这行时初始化为 0,之后每次调用 count_calls() 返回自增后的值。它的生命周期是整个程序运行期间——不像普通局部变量函数返回就销毁。适合做「调用计数」「第一次标记」「单例中的持久状态」等场景。
容易踩的坑
小结
- 函数由返回类型、函数名、形参列表和函数体四个部分组成——定义是完整实现,声明是告诉编译器「有这函数」
- 参数传递有三种:值传递(改副本不动原值)、引用传递(改别名动原值)、const 引用(不拷贝只读)——90% 场景选 const T& 或 T&
- 数组作为形参退化为指针——函数内部永远不知道数组长度,必须单独传 n——或用
std::vector/std::array - 函数重载允许同名函数经形参列表区分——编译器通过「候选→可行→最佳」三步匹配,无法唯一选出就是编译错误
- inline 主要解决头文件定义的 ODR 规则,不保证调用点展开;constexpr 在常量上下文中允许编译期求值
练习
问题 1(改代码型) 下面这段代码试图写一个函数 doubleArray 把数组里每个元素翻倍。但它有 bug——找出并写出修正后的代码。
#include <iostream>
void doubleArray(int arr[10]) {
for (int i = 0; i < 10; i++)
arr[i] *= 2;
}
int main() {
int nums[] = {1, 2, 3, 4, 5};
doubleArray(nums);
for (int i = 0; i < 5; i++)
std::cout << nums[i] << ' ';
return 0;
}问题 2(独立实现题) 设计并实现一个函数 read_numbers,从标准输入读取一组整数存进 std::vector<int> 返回。要求:(1) 读到非整数或 EOF 时停止——用 while 循环 (2) 返回 vector<int>(不是引用、不是用引用参数输出)(3) 在 main 中用范围 for 循环打印每个元素及其下标。提示:用 while (std::cin >> val) 判断是否成功读到数。
问题 3(问答型) 本章 §5 Stepper 演示了值传递——形参 x 是实参 3 的副本。如果把 int add(int x, int y) 改成 int add(const int &x, const int &y),函数的抽象语义会发生什么变化?形参 x 和 y 还会被「拷贝」吗?修改 x 在 const 引用下能通过编译吗?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 函数(function)
C++ 中最基本的代码复用单元。由四部分构成:返回类型(产出什么)、函数名(车间名字)、形参列表(需要什么原材料)、函数体(怎么加工)。
int add(int x, int y) { return x + y; }就是一个完整的函数定义。调用时把实参的值拷贝给形参后执行函数体,return 后整个栈帧被销毁。详见本章「函数基础」一节和 FunctionCallStackDiagram 图示。- 值传递(pass by value)
把实参的值拷贝一份给形参——修改形参不影响实参。适合小对象(int、double 等基本类型)或函数需要独立副本来修改的场景。
void f(int x)就是值传递。拷贝大对象(string、vector)开销大——此时用 const 引用更好。详见本章「参数传递」一节和 ParameterPassingDiagram 图示中左侧的内存对比。- 引用传递(pass by reference)
形参用
T&声明——不拷贝实参,而是让形参直接引用实参的内存。void f(int &x) { x = 0; }中修改 x 就是修改调用方传进来的原变量。经典用例是 swap 交换两值。不能接受字面值(f(5)编译错误)——因为临时值没有可以修改的内存地址。详见本章「参数传递」一节和右侧的内存对比图。- const 引用传递(const reference)
形参用
const T&声明——不拷贝实参但也不能修改它。可以接受字面值和临时对象。void f(const std::string &s)是传大对象的首选——清晰表达「我只读」的意图同时还省了拷贝。详见本章「参数传递」一节和 const 引用 vs 值传递的代码示例。- 数组退化(array decay)
C++ 中数组作为函数形参时自动转换成指向首元素的指针。
void f(int arr[10])和void f(int* arr)完全等价——编译器不管[]里写什么数字都当T*处理。函数内部sizeof(arr)返回指针大小而非数组大小,必须单独传长度参数。详见本章「数组形参」一节和 ArrayParameterDiagram 图示。- 函数重载(function overloading)
C++ 允许同一作用域内同名函数但形参列表不同(类型、数量、顺序)。编译器比较实参与形参类型按优先级自动匹配——「候选→可行→最佳」三步。如果找不到唯一最佳就是 ambiguous(二义性)——编译报错。只改返回类型不算重载。详见本章「函数重载」一节和 OverloadResolutionDiagram 图示。
- 内联函数(inline function)
可在多个 translation units 中出现相同定义的函数,通常定义在头文件。关键字不强制机器码在调用点展开;优化器可内联没有该关键字的函数,也可保留 inline 函数调用。
- constexpr 函数
用
constexpr声明的函数——当所有实参都是常量表达式时编译器在编译期就能算出结果。constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }可在编译期计算并用于数组大小等场景。如果实参中有变量则退化为普通运行时函数。constexpr 函数隐式是 inline 的。详见本章「constexpr 函数」一节的编译期与运行时对照示例。- 分离式编译
声明放头文件、定义放源文件,各 translation unit 独立编译为目标文件后再链接的组织方式;函数签名必须在所有使用处一致。
- 可变形参
接收数量可变实参的机制;同类型集合优先使用
initializer_list,C 风格省略号因缺乏静态类型信息只用于必要兼容。- 复杂返回类型
返回引用、指针或数组指针等形式;除声明语法外,还必须证明被引用对象在函数返回后继续存活。
- 默认实参
声明中为末尾形参提供的调用默认值;省略实参时在调用点使用,且同一作用域不能重复指定同一默认值。
- 函数指针
保存特定函数类型地址的指针,可作为值传递并间接调用;overloaded function 需要目标类型帮助选择唯一版本。