在C++中,如何设置默认参数值?

342

默认参数值应该放在哪个位置?只放在函数定义中,还是声明中,或者两个地方都可以?

10个回答

351

默认参数值必须出现在函数声明中,因为这是调用者唯一看到的内容。

编辑:正如其他人指出的那样,你可以将默认参数值放在函数定义中,但我建议编写所有代码时都假设这不成立。


51
即使从技术上讲你可以进行选择,但在声明中进行选择是唯一自我说明的方式。 - Matthieu M.
我想知道是否有任何情况下默认值是特定于实现的。这让我想到另一件事:是否可能混合使用?比如一个参数在实现中设置默认值,而另一个参数在声明中设置默认值?不确定为什么要这样做。但还是很好奇。 - Lysol
1
@AidanMueller 这是不可能的。默认值是由调用方基于它看到的声明传递的,而非在定义中指定的值。 - Marcelo Cantos

104

你可以二选一,但不能同时存在。通常情况下,你可以在函数声明时设置默认值,然后所有调用者都可以使用该默认值。然而,你也可以在函数定义时设置默认值,那么只有看到定义的人才能使用默认值。


28
从技术上讲或许是正确的,但我认为这不是一个好建议。 - Marcelo Cantos
35
如果你想真的很可怕,你实际上可以两个参数分别这么做。 :-) - Bo Persson
10
@Bo Persson:好主意,不过我们已经有了可以让人们变得像恶魔一样的模板。 - sharptooth
8
@sharptooth: and macros :)@sharptooth: 和宏 :) - Janik Zikovsky
为我执行以下操作会产生以下g++错误:error: default argument given for parameter <x> of <fun(args)> [-fpermissive] ..... after previous specification in <fun(args)> [-fpermissive] - Patrizio Bertoni
显示剩余2条评论

97

C++默认参数的逻辑放在调用方,这意味着如果默认值表达式无法从调用位置计算出来,则不能使用默认值。

其他编译单元通常只包含声明,因此定义中放置的默认值表达式仅可在定义的编译单元本身中使用(且在定义后,即编译器看到默认值表达式之后)。

最有效的位置是在声明(.h)中,以便所有用户都能看到它。

有些人也喜欢在实现中添加默认值表达式(作为注释):

void foo(int x = 42,
         int y = 21);

void foo(int x /* = 42 */,
         int y /* = 21 */)
{
   ...
}

然而,这意味着出现了重复,并且会增加注释与代码不同步的可能性(有什么比没有注释的代码更糟糕的呢?有误导性的注释!)。


我对这些注释没有问题,因为99.99%的时间参数只是值初始化。 - Mooing Duck

44

虽然这是一个“老”线程,但我仍然想向其添加以下内容:

我经历了下一个情况:

  • 在类的头文件中,我有
[需要翻译的内容包括在引用标签中,不做修改]
int SetI2cSlaveAddress( UCHAR addr, bool force );
在该类的源文件中,我有
int CI2cHal::SetI2cSlaveAddress( UCHAR addr, bool force = false )
{
   ...
}
如您所见,我已将参数“force”的默认值放在类源文件中,而不是类头文件中。 然后我在一个派生类中如下使用该函数(该派生类以公共方式继承了基类):
``` SetI2cSlaveAddress(addr); ```
假设它会将"force"参数默认设置为 "false"。 然而,编译器(处于c++11模式)报错并给出了以下错误提示:
/home/.../mystuff/domoproject/lib/i2cdevs/max6956io.cpp: In member function 'void CMax6956Io::Init(unsigned char, unsigned char, unsigned int)':
/home/.../mystuff/domoproject/lib/i2cdevs/max6956io.cpp:26:30: error: no matching function for call to 'CMax6956Io::SetI2cSlaveAddress(unsigned char&)'
/home/.../mystuff/domoproject/lib/i2cdevs/max6956io.cpp:26:30: note: candidate is:
In file included from /home/geertvc/mystuff/domoproject/lib/i2cdevs/../../include/i2cdevs/max6956io.h:35:0,
                 from /home/geertvc/mystuff/domoproject/lib/i2cdevs/max6956io.cpp:1:
/home/.../mystuff/domoproject/lib/i2cdevs/../../include/i2chal/i2chal.h:65:9: note: int CI2cHal::SetI2cSlaveAddress(unsigned char, bool)
/home/.../mystuff/domoproject/lib/i2cdevs/../../include/i2chal/i2chal.h:65:9: note:   candidate expects 2 arguments, 1 provided
make[2]: *** [lib/i2cdevs/CMakeFiles/i2cdevs.dir/max6956io.cpp.o] Error 1
make[1]: *** [lib/i2cdevs/CMakeFiles/i2cdevs.dir/all] Error 2
make: *** [all] Error 2

但是,当我在基类的头文件中添加了默认参数:

int SetI2cSlaveAddress( UCHAR addr, bool force = false );

并将其从基类的源文件中删除:

int CI2cHal::SetI2cSlaveAddress( UCHAR addr, bool force )

那么编译器就会很高兴,并且所有的代码都可以如预期般工作(我可以向函数SetI2cSlaveAddress()传递一个或两个参数)!

所以,不仅对于类的用户来说,在头文件中放置参数的默认值很重要,而且从编译和功能上看似乎是必须的!


1
这是因为调用者需要知道函数需要哪些参数,所以调用者看到的声明(通常在头文件中)需要包含这些信息(包括默认值等)。 - CoffeeTableEspresso
6502的回答(https://dev59.com/pG445IYBdhLWcg3wM3bx#4989591)解释了为什么会发生这种情况。 - Hari

15

我还没有看到任何人提到的一个要点是:

如果您有虚方法,每个声明都可以有自己的默认值!

使用哪个值取决于您调用的接口。

ideone上的示例

struct iface
{
    virtual void test(int a = 0) { std::cout << a; }
};

struct impl : public iface
{
    virtual void test(int a = 5) override { std::cout << a; }
};

int main()
{
    impl d;
    d.test();
    iface* a = &d;
    a->test();
}

它打印出50

我强烈不建议您像这样使用它。

这是因为在d.test()a->test()中,尽管默认参数值分别来自于impliface,但都会执行impltest函数。请参阅下面Gufino2的评论,他提到了Effective C++的第37条款。


1
我们不能默认将参数分配给对象吗?例如 void test(obj, a, b = a); - Beyondo
1
这是因为即使在虚函数中,参数的默认值也是静态绑定的:这意味着参数的默认值是通过评估变量的静态类型来选择的。在这种情况下,即使a指向impl对象,a仍然是iface指针,当您调用a->test()时,编译器使用iface的默认参数。这就是为什么在重写中更改默认参数值是一个可怕的想法。Scott Meyers在他的一本书中谈到了这个问题。 - Gufino2

14
如果函数是公开的 -- 非成员、公有或保护 -- 那么调用方应该知道它们,并且默认值必须在头文件中。
如果函数是私有的并且在外部定义,那么将默认值放在实现文件中确实有意义,因为这允许进行更改而不触发客户端重新编译(在企业规模开发中有时是一个严重的问题)。尽管如此,这肯定会产生潜在的混淆,并且在头文件中以更直观的方式呈现API具有文档价值,所以要在一致性和没有强制推荐任何一种方式的情况下做出妥协。

11

通常情况下,声明语句是最“有用”的,但这取决于您如何使用类。

两种都无效。


10

不错的问题... 我发现编程人员通常使用声明来声明默认值。根据编译器的不同,我曾被要求采用某种方式(或警告)。

void testFunct(int nVal1, int nVal2=500);
void testFunct(int nVal1, int nVal2)
{
    using namespace std;
    cout << nVal1 << << nVal2 << endl;
}

3
添加一个要点,带有默认参数的函数声明应该从右到左且从上到下排序。
例如,在下面的函数声明中,如果更改声明顺序,则编译器会给出缺少默认参数错误。原因是编译器允许您在同一作用域内分隔具有默认参数的函数声明,但它应该按照从右到左(默认参数)和从上到下(函数声明默认参数的顺序)的顺序排列。
//declaration
void function(char const *msg, bool three, bool two, bool one = false);
void function(char const *msg, bool three = true, bool two, bool one); // Error 
void function(char const *msg, bool three, bool two = true, bool one); // OK
//void function(char const *msg, bool three = true, bool two, bool one); // OK

int main() {
    function("Using only one Default Argument", false, true);
    function("Using Two Default Arguments", false);
    function("Using Three Default Arguments");
    return 0;
}

//definition
void function(char const *msg, bool three, bool two, bool one ) {
    std::cout<<msg<<" "<<three<<" "<<two<<" "<<one<<std::endl;
}

2
在给定的函数声明中,每个默认参数之后的参数都必须在此前或之前的声明中提供默认参数。 - Lightness Races in Orbit
这里有更多的示例:https://www.ibm.com/docs/en/zos/2.1.0?topic=only-restrictions-default-arguments-c - Hari

3

根据标准,你可以选择其中一种方式,但请记住,如果你的代码在看到包含默认参数的定义之前看到了没有默认参数的声明,则可能会出现编译错误。

例如,如果你包含了不带默认参数列表的函数声明的头文件,则编译器将查找该原型,因为它不知道你的默认参数值,因此原型不匹配。

如果你在定义中放置带有默认参数的函数,请包含该文件,但我不建议这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接