11.5 — 默认参数

默认参数是为函数参数提供的一个默认值。例如

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;
}

此程序生成以下输出:

x: 1
y: 2
x: 3
y: 4

在第一次函数调用中,调用者为两个参数都提供了显式实参,因此使用了这些实参值。在第二次函数调用中,调用者省略了第二个实参,因此使用了默认值 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");

作者注

由于用户可以选择提供特定的实参值或使用默认值,因此提供默认值的参数有时被称为可选参数。然而,可选参数这个术语也用于指代其他几种类型的参数(包括通过地址传递的参数和使用 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;
}

产生以下输出

Values: 1 2 3
Values: 1 2 30
Values: 1 20 30
Values: 10 20 30

C++(截至 C++23)不支持 print(,,3) 这样的函数调用语法(作为提供 z 的显式值而使用 xy 的默认参数的方式)。这有三个主要后果

  1. 在函数调用中,任何显式提供的实参都必须是最左边的实参(带有默认值的实参不能跳过)。

例如

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;
}
  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';
}

默认参数也必须在使用前在翻译单元中声明

#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';
}

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

在 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 #includes 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;
}

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;
}

在此示例中,编译器无法判断 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;
}

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

如果我们打算调用 print(int, int)print(int, double),我们总是可以显式指定第二个参数。但是如果我们想调用 print(int) 呢?如何做到这一点并不明显。

通过函数指针调用的函数不支持默认参数 高级

我们在课程 20.1 -- 函数指针 中讨论此主题。由于使用此方法不考虑默认参数,这也提供了一种解决方法,可以调用原本会因默认参数而模糊的函数。

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
130 评论
最新
最早 最多投票
内联反馈
查看所有评论