1. 什么是默认参数?

核心定义:默认参数允许你在声明函数时,为一个或多个参数指定一个默认值。当调用该函数时,如果没有为这些带有默认值的参数提供实参,那么编译器会自动使用预先设定的默认值。

通俗比喻:想象你在网上订购一件 T 恤。尺码是必填项,但颜色选项默认是“白色”。如果你不特意去选择颜色,那么你收到的就是一件白色的 T 恤。如果你选择了“蓝色”,那么你就会收到一件蓝色的 T 恤。这里的“白色”就是颜色的默认参数。


2. 为什么需要默认参数?(目的与优势)

  1. 简化函数调用:对于一些不常改变的参数,用户不必每次都提供。这使得函数调用更加简洁。
// 没有默认参数
createWindow(800, 600, "My App", true, false);
 
// 有了默认参数
createWindow(800, 600, "My App"); // 其他参数使用默认值
  1. 提高函数的灵活性和可扩展性
  • 当你需要向一个已有的函数添加新功能(通过新参数)时,如果为这个新参数提供一个默认值,那么所有已有的调用该函数的代码都无需修改,它们会继续使用默认值正常工作。这极大地提高了向后兼容性。
  • 一个函数可以像多个重载函数一样工作,但只需要维护一份代码。

3. 如何定义和使用默认参数?

a. 语法规则

  1. 默认值在函数声明(原型)中指定。通常是放在头文件 (.h/.hpp) 中。
  2. 默认参数必须从右向左设置。也就是说,如果一个参数有默认值,那么它右边所有的参数也必须有默认值。
// 正确的语法
void setup(int required_param, int optional_param_1 = 10, bool optional_param_2 = true);
 
// 错误的语法
// void setup(int param_1 = 10, int param_2); // 错误!默认参数右边不能有非默认参数

b. 示例

// --- logger.h ---
#include <string>
 
// 在函数声明中指定默认参数
void log(const std::string& message, int level = 0, const std::string& category = "General");
 
// --- logger.cpp ---
#include "logger.h"
#include <iostream>
 
// 函数定义中不需要(也不应该)重复指定默认值
void log(const std::string& message, int level, const std::string& category) {
    std::cout << "[" << category << "][Level " << level << "]: " << message << std::endl;
}
 
// --- main.cpp ---
#include "logger.h"
 
int main() {
    // 1. 提供所有参数
    log("Detailed debug info", 2, "Debug");
 
    // 2. 省略最右边的参数,使用其默认值 "General"
    log("User logged in", 1);
 
    // 3. 省略右边两个参数,使用它们的默认值
    log("Application started");
 
    // 错误调用:不能跳过中间的参数
    // log("This is wrong", , "Network"); // 编译错误!
    // 想要为 category 指定值,必须也为 level 指定值
    log("This is right", 0, "Network");
 
    return 0;
}

输出:

[Debug][Level 2]: Detailed debug info
[General][Level 1]: User logged in
[General][Level 0]: Application started
[Network][Level 0]: This is right

c. 定义与声明的分离

  • 最佳实践:默认值只在函数声明(通常在头文件中)中指定一次。
  • 函数定义(在 .cpp 文件中)不应再重复默认值。如果重复,某些编译器可能会发出警告或错误。这是因为声明是给调用者看的“接口”,而定义是“实现”。默认值是接口的一部分。
// A.h
void func(int x = 10); // 正确
 
// A.cpp
#include "A.h"
void func(int x /*= 10*/) { // 不要在这里重复 `= 10`
    // ...
}

4. 默认参数与函数重载的关系

默认参数和函数重载可以实现相似的效果,但它们是不同的机制。当它们一起使用时,需要特别小心,因为很容易导致调用不明确 (Ambiguity)

示例:导致调用不明确

#include <iostream>
 
// 重载版本 1
void display(int num) {
    std::cout << "Called display(int): " << num << std::endl;
}
 
// 带有默认参数的函数
void display(int num, double factor = 1.0) {
    std::cout << "Called display(int, double): " << num * factor << std::endl;
}
 
int main() {
    display(10, 2.5); // OK,明确匹配第二个函数
    // display(10);      // !!! 编译错误:调用不明确 !!!
}

当调用 display(10) 时,编译器面临一个两难的选择:

  1. 它可以匹配 display(int)
  2. 它也可以匹配 display(int, double),并使用 factor 的默认值 1.0

因为这两个匹配的“优先级”相同,编译器无法决定调用哪一个,因此会报错。

如何解决?

  • 避免这样的设计。要么使用函数重载,要么使用默认参数,不要让它们产生重叠的调用模式。
  • 在上面的例子中,可以移除第一个重载版本 display(int),只保留带有默认参数的版本,因为它已经能处理 display(10) 的情况。

5. 默认参数的求值时机

默认参数的值可以是一个常量、一个全局变量,甚至是一个函数调用。

求值时机:默认参数表达式是在函数调用点被求值的,而不是在函数声明时。

示例

#include <iostream>
 
int counter = 0;
int get_default_value() {
    return ++counter;
}
 
void print_value(int val = get_default_value()) {
    std::cout << "Value: " << val << std::endl;
}
 
int main() {
    print_value(); // 第一次调用,get_default_value() 被执行,counter 变为 1
    print_value(); // 第二次调用,get_default_value() 再次被执行,counter 变为 2
    print_value(); // 第三次调用,get_default_value() 再次被执行,counter 变为 3
    return 0;
}

输出:

Value: 1
Value: 2
Value: 3

这表明默认值不是一个固定的编译时常量,而是在每次需要它的时候动态计算出来的。


总结与最佳实践

  1. 从右到左规则:默认参数必须位于参数列表的末尾。
  2. 声明而非定义:将默认值放在函数声明(头文件)中,而不是定义(源文件)中。
  3. 简化接口:用它来隐藏那些常用或不常改变的配置参数,让用户的生活更轻松。
  4. 向后兼容:在为现有函数添加新参数时,使用默认参数可以避免破坏旧代码。
  5. 警惕歧义:小心默认参数与函数重载之间的冲突,确保任何函数调用都只有一个唯一的最佳匹配。
  6. 了解求值时机:记住默认参数表达式是在调用时才被求值的。