11-5 默认实参

默认实参default argument是为函数形参提供的默认值。例如:

void print(int x, int y=10) // 10 is the default argument
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

调用函数时,调用方可为任何具有默认实参的函数形参可选地提供实参值。若调用方提供了实参值,则使用函数调用中的该实参值;若未提供实参值,则使用默认实参值。

考虑以下程序:

#include <iostream>

void print(int x, int y=4) // 4 is the default argument
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

int main()
{
    print(1, 2); // y will use user-supplied argument 2
    print(3); // y will use default argument 4, as if we had called print(3, 4)

    return 0;
}

该程序输出如下结果:

image

首次函数调用中,调用方为两个形参都提供了显式实参值,因此使用了这些实参值。第二次函数调用中,调用方省略了第二个实参,因此使用了默认值 4。

请注意,必须使用等号来指定默认实参。使用括号或大括号初始化不会生效:

void foo(int x = 5);   // ok
void goo(int x ( 5 )); // compile error
void boo(int x { 5 }); // compile error

值得注意的是,默认实参由编译器在调用处处理。在上例中,当编译器遇到print(3)时,会将其重写为print(3, 4),使形参个数与调用实参匹配。重写后的函数调用将按常规方式执行。

核心要点
默认实参由编译器在函数调用处插入。

默认实参在C++中应用广泛,你将在日常代码(及后续课程)中频繁见到它们。


何时使用默认实参

当函数需要具有合理默认值的实参,同时允许调用方根据需要覆盖该值时,默认实参是绝佳选择。

例如以下函数原型就常使用默认实参:

int rollDie(int sides=6);
void openLogFile(std::string filename="default.log");

作者注
由于用户可选择提供具体实参值或使用默认值,带默认值的实参有时被称为可选形参optional parameter。但“可选形参”一词还指代其他多种形参类型(包括按地址传递的形参和使用std::optional的形参),因此建议避免使用该术语。

当需要为现有函数添加新形参时,默认实参同样具有价值。若添加新形参时未设置默认实参值,将导致所有现有函数调用失效(因未为该形参提供实参值)。这可能引发大量现有调用的修改工作(若调用方代码非您所有,甚至可能无法修改)。但若添加带默认实参值的新形参,所有现有调用仍可正常工作(因其将采用该形参的默认实参值),同时允许新调用在需要时显式指定实参值。


多个默认实参

函数可包含多个带默认实参值的形参:

#include <iostream>

void print(int x=10, int y=20, int z=30)
{
    std::cout << "Values: " << x << " " << y << " " << z << '\n';
}

int main()
{
    print(1, 2, 3); // all explicit arguments
    print(1, 2); // rightmost argument defaulted
    print(1); // two rightmost arguments defaulted
    print(); // all arguments defaulted

    return 0;
}

输出结果如下:

image

C++(截至C++23标准)不支持类似print(,,3)的函数调用语法(即在使用x和y的默认实参时为z显式提供实参值)。这导致三项主要后果:

  1. 在函数调用中,任何显式提供的实参必须位于最左侧(具有默认的实参不可跳过)。

例如(#include <string_view> 原文没有):

#include <string_view>

void print(std::string_view sv="Hello", double d=10.0);

int main()
{
    print();           // okay: both arguments defaulted
    print("Macaroni"); // okay: d defaults to 10.0
    print(20.0);       // error: does not match above function (cannot skip argument for sv)

    return 0;
}

image

  1. 如果某个形参被赋予默认实参值,则所有后续形参(位于右侧)也必须被赋予默认实参值。

以下内容不允许:

void print(int x=10, int y); // not allowed

规则
若某个形参被赋予默认实参值,则所有后续形参(右侧)也必须赋予默认实参值。

  1. 当多个形参具有默认实参值时,最左侧的形参应是最可能被用户显式设置的形参。

默认实参不可重新声明,且必须在使用前声明。

一旦声明,默认实参不能在同一翻译单元中重新声明。这意味着对于具有前向声明和函数定义的函数,默认实参可以在前向声明或函数定义中声明,但不能同时出现在两者中。

#include <iostream>

void print(int x, int y=4); // forward declaration

void print(int x, int y=4) // compile error: redefinition of default argument
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

image

默认实参也必须在翻译单元中声明后才能使用:

#include <iostream>

void print(int x, int y); // forward declaration, no default argument

int main()
{
    print(3); // compile error: default argument for y hasn't been defined yet

    return 0;
}

void print(int x, int y=4)
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

image

最佳实践是在前向声明中声明默认实参,而非在函数定义中声明。这是因为前向声明更可能被其他文件看到,并在使用前被包含(尤其当它位于头文件中时)。

在 foo.h 中:

#ifndef FOO_H
#define FOO_H
void print(int x, int y=4);
#endif

在 main.cpp 中:

#include "foo.h"
#include <iostream>

void print(int x, int y)
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

int main()
{
    print(5);

    return 0;
}

请注意,在上例中,我们能够使用函数 print() 的默认实参,是因为 main.cpp 通过 #include foo.h 包含了该文件,而 foo.h 中包含了定义该默认实参的前向声明。

最佳实践
若函数存在前向声明(尤其在头文件中),则将默认实参置于该处;否则应置于函数定义中。


默认实参与函数重载

带默认实参的函数可被重载。例如以下写法是允许的:

#include <iostream>
#include <string_view>

void print(std::string_view s)
{
    std::cout << s << '\n';
}

void print(char c = ' ')
{
    std::cout << c << '\n';
}

int main()
{
    print("Hello, world"); // resolves to print(std::string_view)
    print('a');            // resolves to print(char)
    print();               // resolves to print(char)

    return 0;
}

image

对print()的调用实际调用的是print(char),效果等同于显式调用print(‘ ’)。

现在考虑此情况:

void print(int x);                  // signature print(int)
void print(int x, int y = 10);      // signature print(int, int)
void print(int x, double y = 20.5); // signature print(int, double)

默认值不属于函数签名的一部分,因此这些函数声明属于可区分的重载。

相关内容
函数重载区分将在第11.2节——函数重载区分中讨论


默认实参可能导致模糊匹配

默认实参极易引发模糊函数调用:

void foo(int x = 0)
{
}

void foo(double d = 0.0)
{
}

int main()
{
    foo(); // ambiguous function call

    return 0;
}

image

在此示例中,编译器无法判断foo()应解析为foo(0)还是foo(0.0)。

以下是更复杂的示例:

void print(int x);                  // signature print(int)
void print(int x, int y = 10);      // signature print(int, int)
void print(int x, double y = 20.5); // signature print(int, double)

int main()
{
    print(1, 2);   // will resolve to print(int, int)
    print(1, 2.5); // will resolve to print(int, double)
    print(1);      // ambiguous function call

    return 0;
}

image

对于调用 print(1),编译器无法判断其应解析为 print(int)、print(int, int) 还是 print(int, double)。

当我们需要调用 print(int, int) 或 print(int, double) 时,总能显式指定第二个形参。但若想调用 print(int) 呢?如何实现并不明显。


通过函数指针调用时默认实参无效(进阶)

我们在第20.1节——函数指针中探讨了这个主题。由于该方法不会考虑默认实参,这也为调用因默认实参而可能产生歧义的函数提供了一种变通方案。

posted @ 2026-03-07 10:18  游翔  阅读(1)  评论(0)    收藏  举报