函数是一组一起执行一个任务的语句。每个 C++ 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。

函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。

默认的函数声明和定义总是extern的。

一.函数参数

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数

1.传值调用

传值调用 是函数参数传递的一种基本方式,其核心特点是:将实参的 “副本” 传递给函数形参,函数内部对形参的修改不会影响外部实参

传值调用的执行过程

  1. 复制实参:当函数被调用时,编译器会为函数的形参创建一个新的变量,并将实参的值复制到这个新变量中(形参与实参是两个独立的变量,占用不同的内存空间)。
  2. 函数内操作形参:函数内部对形参的所有修改(如赋值、运算等),都仅作用于这个副本(形参),与外部的实参无关。
  3. 函数返回后:形参的生命周期结束(被销毁),实参的值保持调用前的状态不变。
#include <iostream>

// 函数:传值调用,形参是n(实参的副本)
void increment(int n) {
n++; // 仅修改形参n(副本)
std::cout << "函数内n的值:" << n << std::endl; // 输出 6
}

int main() {
int num = 5;
increment(num); // 传递num的副本给形参n
std::cout << "函数外num的值:" << num << std::endl; // 输出 5(num未被修改)
return 0;
}

2.指针调用

传指针调用 是函数参数传递的一种方式,核心是将实参的地址(指针值)传递给函数的形参(指针变量),函数内部通过指针间接访问并修改指针所指向的实参。这种方式允许函数间接修改外部实参的值,同时避免了传值调用中复制大型对象的开销。

传指针调用的执行过程

  1. 传递地址:函数调用时,将实参的内存地址(指针值)复制给函数的形参(指针变量)。此时,形参指针和实参指针指向指针指向的是同一块内存空间(即指向同一个变量)。
  2. 通过指针操作实参:函数内部可以通过 “解引用指针”(*指针名)访问或修改指针所指向的实参的值。
  3. 函数返回后:形参指针(副本)被销毁,但通过它对实参的修改会保留下来(因为修改的是指针指向的内存内容)。
#include <iostream>

// 函数:传指针调用,形参是指针n(接收实参的地址)
void increment(int* n) { // n是指针,存储实参的地址
(*n)++; // 解引用指针,修改指针所指向的变量(实参)
std::cout << "函数内*n的值:" << *n << std::endl; // 输出 6
}

int main() {
int num = 5;
increment(&num); // 传递num的地址(&num是指向num的指针)
std::cout << "函数外num的值:" << num << std::endl; // 输出 6(num被修改)
return 0;
}

3.引用调用

引用调用 是函数参数传递的一种高效方式,核心是将实参的 “别名”(引用)传递给函数形参,函数内部对形参的操作会直接作用于外部实参,从而实现对实参的修改。

引用调用的执行过程

  1. 建立别名关系:函数声明时,形参被声明为引用类型(格式:类型& 形参名)。调用函数时,编译器不复制实参,而是让形参成为实参的 “别名”—— 形参和实参指向同一块内存空间(本质是同一个变量的两个名字)。
  2. 直接操作实参:函数内部对形参的所有修改(如赋值、运算等),都会直接反映到外部实参上(因为两者是同一个变量)。
  3. 函数返回后:形参的生命周期结束,但对实参的修改已被保留(因为修改的是共享的内存内容)。
#include <iostream>

// 函数:引用调用,形参ref是实参的别名
void increment(int& ref) { // ref是int类型的引用(实参的别名)
ref++; // 直接修改ref(即修改实参本身)
std::cout << "函数内ref的值:" << ref << std::endl; // 输出 6
}

int main() {
int num = 5;
increment(num); // 传递num本身,ref成为num的别名
std::cout << "函数外num的值:" << num << std::endl; // 输出 6(num被修改)
return 0;
}

4.参数的默认值

当您定义一个函数,您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。

默认参数在函数声明中提供,当又有声明又有定义时,定义中不允许默认参数。如果函数只有定义,则默认参数才可以出现在函数定义中。

如果一个函数中有多个默认参数,则形参分布中,默认参数应从左至右逐渐定义。

这是通过在函数定义中使用赋值运算符来为参数赋值的。调用函数时,如果未传递参数的值,则会使用默认值,如果指定了值,则会忽略默认值,使用传递的值。请看下面的实例:

#include <iostream>
using namespace std;

int sum(int a, int b=20)
{
int result;

result = a + b;

return (result);
}

int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
int result;

// 调用函数来添加值
result = sum(a, b);
cout << "Total value is :" << result << endl;

// 再次调用函数
result = sum(a);
cout << "Total value is :" << result << endl;

return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:
Total value is :300
Total value is :120

三.内联函数

内联函数并不是一种新的函数类型,而是一种对编译器建议

当你用 inline 关键字修饰一个函数时,你是在建议编译器:“请尝试在程序调用这个函数的地方,直接把函数体的代码插进去,而不是执行传统的函数调用流程。”

1. 传统函数调用 vs. 内联函数

传统函数调用

每次调用函数时,程序都需要执行一系列步骤,这些步骤会带来额外的开销:

  1. 保存当前执行状态(如寄存器值)。
  2. 将参数压入栈。
  3. 跳转到函数代码的内存地址。
  4. 执行函数体。
  5. 将返回值压入栈或寄存器。
  6. 恢复保存的状态。
  7. 跳转回调用点。

内联函数展开

当编译器接受 inline 建议并进行内联时,它会在编译阶段将函数调用点替换为函数的实际代码,就像使用一样,但它保留了函数的类型检查和作用域规则,因此比宏更安全。

// 假设有内联函数
inline int add(int a, int b) { return a + b; }

// 原始代码
int result = add(3, 5);

// 编译后实际执行的代码(概念上)
int result = 3 + 5;

二.使用 inline 关键字

1. 如何定义内联函数

在函数定义前加上 inline 关键字即可。

#include <iostream>

// 建议将短小、频繁调用的函数定义为内联函数
inline int max(int a, int b) {
return (a > b) ? a : b;
}

int main() {
int x = max(10, 20); // 编译器可能会直接替换成 int x = (10 > 20) ? 10 : 20;
std::cout << x << std::endl;
return 0;
}

2. 内联函数与头文件

  • 如果函数只在一个 .cpp 文件中使用,那么定义在 .cpp 文件中即可。
  • 如果函数需要在多个 .cpp 文件中使用,为了让所有调用点都能看到定义,定义必须放在头文件中。

四.重载函数

核心概念:函数签名

要理解函数重载,首先要理解函数签。
在 C++ 中,函数的身份由其函数签名唯一确定,函数签名包括:

  1. 函数名称
  2. 参数列表(参数的数量和类型,以及顺序)

函数重载的规则:

只要两个或多个函数的函数签名不同,它们就可以同名。

注意:函数的返回类型不属于函数签名的一部分,因此不能仅凭返回类型不同来重载函数。

1)函数重载的机制和示例

1. 基于参数类型不同进行重载

这是最常见的情况,允许函数对不同数据类型执行相同的操作(如计算最大值、打印、相加)。

#include <iostream>

// 重载 1: 适用于整数相加
int add(int a, int b) {
std::cout << "调用 int 版本的 add()" << std::endl;
return a + b;
}

// 重载 2: 适用于浮点数相加
double add(double a, double b) {
std::cout << "调用 double 版本的 add()" << std::endl;
return a + b;
}

int main() {
// 编译器根据传入参数的类型选择正确的重载版本
std::cout << add(5, 10) << std::endl; // 匹配 int 版本
std::cout << add(5.5, 10.1) << std::endl; // 匹配 double 版本

return 0;
}

2. 基于参数数量不同进行重载

你可以使用相同的函数名来处理不同数量的输入。

#include <string>
#include <iostream>

// 重载 1: 两个字符串合并
std::string combine(const std::string& s1, const std::string& s2) {
return s1 + " " + s2;
}

// 重载 2: 三个字符串合并
std::string combine(const std::string& s1, const std::string& s2, const std::string& s3) {
return s1 + " " + s2 + " " + s3;
}

3. 基于参数顺序不同进行重载

如果参数的类型不同,即使数量相同,但顺序不同,也可以构成重载。

// 重载 1: 先 int,后 double
void print_data(int i, double d) {
std::cout << "Int: " << i << ", Double: " << d << std::endl;
}

// 重载 2: 先 double,后 int
void print_data(double d, int i) {
std::cout << "Double: " << d << ", Int: " << i << std::endl;
}

2)不能构成重载的情况

1. 仅返回类型不同

如前所述,返回类型不是函数签名的一部分。编译器在编译时只检查函数的名称和参数类型,它无法在调用点根据返回类型来确定调用哪个函数。

// 错误!不能重载
int calculate(int a);
double calculate(int a); // 编译错误!与上一个函数签名相同

2. 仅默认参数值不同

默认参数并不会改变函数的签名,它只是在调用时不提供参数时使用。

// 错误!不能重载
void func(int a);
void func(int a = 0); // 编译错误!签名仍然是 func(int)

3. 仅 constvolatile 修饰符修饰返回类型

类似于返回类型,constvolatile 修饰返回值也不能构成重载。

3)编译器如何选择重载函数?

当编译器遇到一个函数调用时,它会执行一个复杂的匹配过程:

  1. 精确匹配 (Exact Match): 寻找与传入参数类型和数量完全匹配的函数。
  2. 通过提升匹配 (Promotion): 如果没有精确匹配,编译器会尝试使用安全的提升(如 char 提升到 intfloat 提升到 double)。
  3. 通过标准转换匹配 (Standard Conversion): 如果仍不匹配,编译器会尝试标准类型转换(如 int 转换为 double,或 double 转换为 int)。
  4. 通过用户定义转换匹配 (User-Defined Conversion): 最后尝试用户自定义的类型转换(通常在类中)。

如果编译器找到两个或多个函数都可以通过相同级别的转换进行匹配,它就会报告 “二义性调用(Ambiguous Call)” 错误。