C++ Primer Plus 重读精讲 _ 函数进阶:重载、默认参数、函数指针与

全文连载前置回顾(前15篇完整知识链路)

  1. 1-6篇:开发环境、基础数据类型、运算符全集
  2. 7-10篇:分支结构、三大循环、数组、字符数组与字符串
  3. 11-12篇:指针基础与进阶、三类const指针、指针数组与内存地址
  4. 13-14篇:基础输入输出、结构体与自定义数据类型
  5. 15篇:函数基础、参数传递机制、模块化编程

上一篇我们掌握了函数的基础语法、声明与定义、参数传递的三种方式、返回值形式。但C++函数的能力远不止于此。真实工业开发中还需要:同名函数处理不同类型、省略常用参数、动态选择处理逻辑、把函数作为参数传递给另一个函数等高级能力。


前言

函数进阶的四大核心能力是函数重载、默认参数、函数指针、递归。它们让函数的使用更加灵活、更加智能。

函数重载(Function Overloading):同一个函数名,根据参数类型/数量自动匹配合适的实现。比如一个print函数,可以传int、double、字符串、结构体,编译器自动选择对应版本。

默认参数(Default Arguments):给某些参数指定默认值,调用时可以省略------最常用的参数不需要每次都写。

函数指针(Function Pointer):把函数的地址存储在指针变量中,可以作为参数传递、动态选择调用。这是实现回调函数、策略模式的基础。

递归(Recursion):函数调用自己来解决可以分解为更小同类问题的场景。常用于树结构遍历、分层数据处理、分治算法。

本篇逐一讲解这四项能力,每一项都配合工业场景说明实际应用价值。

一、函数重载:同名函数处理不同类型

函数重载允许在同一个作用域内定义多个同名函数,但它们的参数列表必须不同(参数的类型、数量或顺序不同)。编译器根据调用时的实参自动匹配。

1. 重载的基本用法
cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

// 重载版本1:打印整数
void print(int value)
{
    cout << "[整数] " << value << endl;
}

// 重载版本2:打印浮点数
void print(double value)
{
    cout << "[浮点数] " << value << endl;
}

// 重载版本3:打印字符串
void print(const char* str)
{
    cout << "[字符串] " << str << endl;
}

struct Device
{
    int id;
    char name[30];
    double temp;
};

// 重载版本4:打印设备结构体
void print(const Device& dev)  // &是引用,后续详解,这里可先当作安全指针
{
    cout << "[设备] ID=" << dev.id << " 名称=" << dev.name
         << " 温度=" << dev.temp << "度" << endl;
}

int main()
{
    print(1001);           // 自动匹配版本1
    print(82.5);           // 自动匹配版本2
    print("加热炉A");      // 自动匹配版本3

    Device d = {1001, "加热炉A", 82.5};
    print(d);              // 自动匹配版本4
    return 0;
}

重载规则 :重载必须通过参数区分,不能通过返回值区分。下面这种写法是错误的:

cpp 复制代码
int getValue() { return 10; }
double getValue() { return 10.5; }  // 错误!仅返回值不同不能重载
2. 重载在工业代码中的应用

设备数据处理是典型场景------同一个处理函数名,根据数据类型自动选择不同实现:

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

struct SensorData
{
    int sensorId;
    double value;
    char unit[10];
};

struct DeviceAlert
{
    int deviceId;
    char message[100];
    int level;  // 1=警告 2=严重 3=紧急
};

void processData(int rawValue)  // 处理原始整数采集
{
    cout << "[整型采集] 值=" << rawValue << " (存入寄存器)" << endl;
}

void processData(double calibratedValue)  // 处理校准后的浮点数
{
    cout << "[浮点数据] 值=" << calibratedValue << " (写入数据库)" << endl;
}

void processData(const SensorData& data)  // 处理完整传感器数据包
{
    cout << "[传感器 " << data.sensorId << "] "
         << data.value << data.unit << endl;
}

void processData(const DeviceAlert& alert)  // 处理报警信息
{
    cout << "[报警] 设备" << alert.deviceId
         << " 级别=" << alert.level
         << " 消息=" << alert.message << endl;
}

int main()
{
    int raw = 4095;
    double cal = 82.5;
    SensorData s = {101, 82.5, "C"};
    DeviceAlert a = {1001, "温度超阈值", 2};

    processData(raw);
    processData(cal);
    processData(s);
    processData(a);
    return 0;
}

设计价值 :对外提供统一的processData接口,调用者不需要记住多个函数名。内部实现可以自由优化而不影响调用方。

二、默认参数:简化常用调用

1. 基本用法

在函数声明中给某些参数指定默认值。调用时如果省略这些参数,就使用默认值。

cpp 复制代码
#include <iostream>
using namespace std;

// 声明中指定默认参数:默认温度阈值80度,默认压力上限1.2MPa
void checkDevice(int deviceId, double temperature,
                 double tempLimit = 80.0, double pressureLimit = 1.2)
{
    cout << "设备" << deviceId << ": ";
    if (temperature > tempLimit)
        cout << "温度" << temperature << "度 超过阈值" << tempLimit << "度!";
    else
        cout << "温度" << temperature << "度 正常";
    cout << endl;
}

int main()
{
    checkDevice(1001, 75.0);          // 用默认的温度阈值
    checkDevice(1002, 85.5);          // 用默认阈值
    checkDevice(1003, 125.0, 120.0);  // 反应釜:自定义高温阈值
    checkDevice(1004, 28.5, 45.0);    // 冷却设备:低温阈值
    return 0;
}

关键规则:默认参数必须从右向左连续设置。

cpp 复制代码
// 正确:从右开始连续
void func(int a, int b = 10, int c = 20);

// 错误:跳过了b却给c设默认值
void func(int a, int b = 10, int c);  // 编译错误!

工业最佳实践 :默认参数写在声明中(头文件),不要在定义中重复写(部分编译器允许,但不统一)。

三、函数指针:把函数当作数据

函数本质上是一段可执行代码,它在内存中也有地址。函数指针就是存储这个地址的变量。

1. 函数指针的声明语法
cpp 复制代码
// 普通函数:接收两个int,返回int
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

// 函数指针类型:指向"接收两个int返回int"的函数
typedef int (*MathFunc)(int, int);

语法记忆:返回值类型 (*指针变量名)(参数类型列表)。括号不能省略,否则就变成返回指针的函数了。

2. 通过函数指针调用
cpp 复制代码
#include <iostream>
using namespace std;

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int subtract(int a, int b) { return a - b; }

typedef int (*MathFunc)(int, int);

int main()
{
    MathFunc op;  // op是一个函数指针变量

    // 让op指向add函数
    op = add;
    cout << "10 + 5 = " << op(10, 5) << endl;  // 通过指针调用

    // 让op指向multiply函数
    op = multiply;
    cout << "10 * 5 = " << op(10, 5) << endl;

    // 让op指向subtract
    op = subtract;
    cout << "10 - 5 = " << op(10, 5) << endl;

    return 0;
}
3. 函数指针作为参数(策略模式)

这是工业代码中最重要的应用:把处理逻辑作为参数传入,实现策略选择。

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

struct Device { int id; char name[30]; double temp; double press; };

// 不同的处理策略
void strategyNormal(Device* d)
{
    cout << d->name << ": 正常运行,继续监控" << endl;
}

void strategyWarn(Device* d)
{
    cout << d->name << ": 注意,参数接近阈值,加强监控" << endl;
}

void strategyShutdown(Device* d)
{
    cout << d->name << ": 紧急!超过安全阈值,立即停机" << endl;
}

typedef void (*Strategy)(Device*);

// 根据温度选择策略并执行
void processDevice(Device* d, double tempLimit, double pressLimit)
{
    Strategy strat;
    if (d->temp > tempLimit || d->press > pressLimit)
        strat = strategyShutdown;
    else if (d->temp > tempLimit * 0.9 || d->press > pressLimit * 0.9)
        strat = strategyWarn;
    else
        strat = strategyNormal;

    strat(d);  // 调用选中的策略
}

int main()
{
    Device devices[] = {
        {1001, "加热炉A", 78.5, 0.75},
        {1002, "加热炉B", 88.5, 0.85},
        {1003, "反应釜C", 128.5, 1.35},
        {1004, "冷却塔D", 28.5, 0.42}
    };
    int total = sizeof(devices)/sizeof(Device);

    cout << "=== 设备巡检(温度阈值80度,压力1.2MPa)===" << endl;
    for (int i = 0; i < total; i++)
    {
        cout << "设备" << devices[i].id << "(" << devices[i].name
             << ") T=" << devices[i].temp << "度 P=" << devices[i].pressure << "MPa: ";
        processDevice(&devices[i], 80.0, 1.2);
    }
    return 0;
}

四、递归:函数调用自己

递归的核心思想:把一个大问题分解成结构相似的小问题,直到遇到基础情形(Base Case)为止。

1. 递归的基本结构

每个递归函数必须包含:

  • 基础情形(Base Case):递归终止条件,直接返回结果
  • 递归调用(Recursive Call):把问题缩小后调用自身
  • 结果合并:把递归得到的子问题结果组合成当前问题的答案
cpp 复制代码
// 经典例子:计算阶乘
// n! = n * (n-1)!,基础情形:0! = 1, 1! = 1
int factorial(int n)
{
    if (n <= 1) return 1;              // 基础情形
    return n * factorial(n - 1);       // 递归调用 + 结果合并
}
2. 工业应用:分层设备组递归巡检

真实工厂中设备按组管理:工厂包含多个车间,车间包含多条生产线,生产线包含多台设备。这种嵌套结构天然适合递归处理。

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

// 简化的设备组结构:每个组可以包含若干子组和若干设备
struct Group
{
    char name[30];        // 组名称:如"一号车间"、"生产线A"
    int level;            // 层级:1=工厂 2=车间 3=生产线 4=设备
    int childCount;       // 子组数量
    int children[10];     // 子组索引(简化用数组)
    double temperature;   // 仅叶子节点(设备)有温度
    int deviceId;         // 仅叶子节点(设备)有ID
};

// 递归巡检:从指定组开始,遍历其下所有子节点
void inspectGroup(Group groups[], int groupIndex, int depth)
{
    // 打印缩进(表示层级)
    for (int i = 0; i < depth; i++) cout << "  ";

    Group& g = groups[groupIndex];

    // 基础情形:是设备(叶子节点)
    if (g.level == 4)
    {
        cout << "[设备" << g.deviceId << "] " << g.name
             << " 温度=" << g.temperature << "度";
        if (g.temperature > 80.0) cout << " [超温!]";
        cout << endl;
        return;
    }

    // 递归情形:是组节点,先打印组名,再递归处理所有子组
    cout << "[组" << g.level << "] " << g.name << endl;
    for (int i = 0; i < g.childCount; i++)
    {
        inspectGroup(groups, g.children[i], depth + 1);  // 递归调用
    }
}

int main()
{
    // 构建简化设备树:
    // 工厂(0) -> 车间A(1) -> 生产线1(3) -> 设备101(5)、设备102(6)
    //                   -> 生产线2(4) -> 设备103(7)、设备104(8)
    //          车间B(2) -> 生产线3(9) -> 设备201(10)、设备202(11)
    Group g[] = {
        {"总工厂", 1, 2, {1, 2}, 0, 0},
        {"车间A", 2, 2, {3, 4}, 0, 0},
        {"车间B", 2, 1, {9}, 0, 0},
        {"生产线1", 3, 2, {5, 6}, 0, 0},
        {"生产线2", 3, 2, {7, 8}, 0, 0},
        {"加热炉101", 4, 0, {}, 78.5, 101},
        {"冷却塔102", 4, 0, {}, 28.5, 102},
        {"反应釜103", 4, 0, {}, 125.5, 103},
        {"阀门104", 4, 0, {}, 45.5, 104},
        {"生产线3", 3, 2, {10, 11}, 0, 0},
        {"加热炉201", 4, 0, {}, 85.5, 201},
        {"冷却塔202", 4, 0, {}, 30.2, 202}
    };

    cout << "=== 全厂设备巡检报告 ===" << endl;
    inspectGroup(g, 0, 0);  // 从根节点(索引0)开始遍历
    return 0;
}

核心要点

  • 每个节点的处理方式相同:打印信息,然后对每个子节点递归处理
  • 基础情形保证递归一定能终止(叶子设备不再继续递归)
  • 递归深度受限于调用栈大小(通常几千层内安全)

五、独家C#语法对照

对比维度 C++ C# 工业开发差异
函数重载 同名函数不同参数 同名方法不同参数 概念一致,C#同样支持重载
默认参数 void f(int x = 10),从右向左设置 void F(int x = 10),同样规则 两者几乎相同
函数指针 typedef int (*Func)(int, int) 委托Delegate / Func<,> / Action C#委托更安全、更强大,内置事件机制
匿名函数 后续学习lambda表达式 (x, y) => x + y lambda C# lambda语法更简洁
递归 函数直接调用自己 方法直接调用自己 两者一致,都受栈深度限制
重载决策 编译器根据实参类型匹配 编译器根据实参类型匹配 两者重载规则几乎相同

六、重读专属:进阶函数六大易错点

  • 坑1:重载歧义 --- 两个重载版本都能匹配,编译器不知道选哪个

    cpp 复制代码
    void f(int x) {}
    void f(double x) {}
    f(10L);  // long可以转int也可以转double,歧义!
  • 坑2:默认参数与重载冲突 --- 默认参数造成与重载版本重叠

  • 坑3:递归没有基础情形 --- 缺少终止条件导致栈溢出崩溃

  • 坑4:递归深度过大 --- 嵌套几千层也会栈溢出(可改用迭代或动态规划)

  • 坑5:函数指针类型不匹配 --- 函数签名不同不能互相赋值

  • 坑6:返回值被忽略 --- 函数返回重要状态但调用方不检查(应显式处理或用void)

七、原书课后习题解析

习题:用递归计算斐波那契数列的第N项
cpp 复制代码
#include <iostream>
using namespace std;

// 递归版本:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)
int fibonacci(int n)
{
    if (n <= 0) return 0;
    if (n == 1) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// 非递归版本(效率更高,工业代码推荐)
int fibonacciIter(int n)
{
    if (n <= 0) return 0;
    if (n == 1) return 1;
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; i++)
    {
        c = a + b;
        a = b;
        b = c;
    }
    return b;
}

int main()
{
    cout << "F(0)~F(10): ";
    for (int i = 0; i <= 10; i++)
        cout << fibonacci(i) << " ";
    cout << endl;

    cout << "F(20) = " << fibonacciIter(20) << endl;
    return 0;
}

核心考点:递归基础结构(基础情形 + 递归调用 + 合并)。注意纯递归斐波那契效率极低(指数级),工业中应用迭代或动态规划。这说明了一个重要原则:递归表达思路简洁,但实际工程中要评估性能,必要时转为循环实现

本篇总结

  • 函数重载允许同名函数处理不同类型参数,编译器根据实参自动匹配
  • 默认参数从右向左连续设置,简化常用场景的调用
  • 函数指针把函数地址存起来,可以动态选择处理逻辑,实现策略模式/回调
  • 递归把大问题分解为同类小问题,必须有基础情形保证终止
  • 工业中递归常用于树结构、分层数据、分治场景,但要注意栈深度和性能
  • 重载 + 默认参数 + 函数指针 + 递归,四项组合使用能写出非常灵活的代码

下篇预告

下一篇第十七篇:名称空间与代码组织。学习namespace关键字解决多人协作开发中的命名冲突、using声明与编译指令、名称空间嵌套与匿名空间、配合模块化项目结构,掌握大型项目代码组织的核心手段。