avatar

目录
296 - 构造函数默认参数

构造函数默认参数

🧑‍💻 User

In this lecture, we’re going to see that we can pass default parameters to event constructors.Here is the class we have been using a while ago, the cylinder class.It had a few member variables, base radius and the height, and it had a few other members.For example, we have two constructors here, one which is the default and the other that is takingtwo parameters and we have two getters, two setters and a volume function which is going to computethe volume for this cylinder.What we want to see is how we can pass default parameters to constructors and it is nothing new.The syntax you use is really simple.You already know this syntax from functions.You just specify your default parameter using an equal sign and assign the value that you want to assignto your member variable.So, for example, when we do this, it will be possible to call this constructor with only one parameterand then the compiler is going to auto fill in the second to be a ten.But if you want, you can even specify the two parameters when you call the constructor and then thecompiler is not going to use the default that we have provided here.So if we have a constructor like this in place, we can call our function like we see here with oneparameter.And if we print the base radius, we’re going to see that it’s going to be a five.Let’s go back and see which default we specified.Yes, it is the second parameter.So the first one is the base radius, which is going to be five here.And for the second one, the compiler is going to use the default and it is going to print ten here.I really want you to understand this.So this is how you can pass default arguments to your constructors.But it is also possible to specify all the two parameters.So for example, the base radius is going to be five by default here and the height is going to be tenby default.So when you have a setup like this, please be careful to see that you don’t have a default constructorin place, because if we have these two constructors in place and they are set up like this and we setup a call which doesn’t pass any parameter, the compiler is going to be confused.It’s going to look at this call and then it’s going to say, I have two constructors that can actuallyserve us this call here.So which one do I choose?It can use the default constructor and not pass any parameters.It can also call our parameter taking constructor and use the default.Let’s go back and really let you see this.So if we don’t specify a parameter, this constructor here, which is the default doesn’t take any parameter,is a candidate.It can service that call.But the constructor here, which has two default parameters, can also be called by the compiler andthen the compiler is going to fill in these default values.So when you have a setup like this and you set up a call like this, your compiler is going to be confusedand you’re going to have a compiler error.And to solve this problem, you have no other way but to choose between the two constructors.You’re going to choose which one you like and which one you don’t like, and you’re going to take outwhich one you don’t want, and the compiler is going to choose the one you are left with.Okay, this is really all there is about the default parameters with constructors.We are going to head over to Visual Studio code and play with this a little more.Okay.Here we are in our working folder of the current project is default parameters for constructors.We are going to grab our template files like we always do and we’re going to put them in place and weare going to open this in Visual Studio code by dragging and dropping here, this is going to open ourfolder.We can bring up the Main.cpp file and we’re going to clean our main CPP file a little bit.And the main function here and we’re going to put in the class we want to use to play with this.Things.Remember, we have designed a cylinder class before, so we’re going to put this in here.We’re going to create a file which is going to contain the header file and we’re going to create anotherone, which is going to be our implementation file.It’s going to be cylinder dot CPP.We’re going to open up our header file and put in our class.You see it is the cylinder class and we are going to open the CPP file and put in our implementation.In this lecture we’re going to go a little simple and put our constant directly in our cylinder doth file because we want to keep things simple.So this is going to be picked up by the functions that include cylinder.And then we’re going to have our constant here, which is what we want to compute the volume.Now that we have this, we can actually go in the main CPP file and use our class.Let’s do that.We’re going to include the cylinder and we’re going to create a cylinder object.Object.We can do something like cylinder C one and pass in two parameters.One is going to be the base radius.Let’s use ten and the other is going to be the height and we can print the volume for this cylinderhere.We’re going to say volume C one and say volume.I think we have this function here and let’s go back and make sure we have that double volume.It’s going to return the volume for our cylinder.And if we go down, we have an implementation for this method.I think this is going to work.So what problem do we have here?Oh, we need a semicolon.Sorry for this.We are going to build now with GCC.We can close this left pane.We don’t need it anymore.Here you see that The world is good.We can bring up a terminal to run this program.We can clear and run Rooster.If we run, we’re going to see our volume here.So the class is working and it is doing whatever it is we want it to do.What we want to see is that we can pass default parameters and the syntax to do that is what we sawin the slides.So for example, we can come here and on the second parameter, specify ten as our default parameter.The moment we do this, we still have the ability to call our constructor like this.In the main function we can specify all the parameters and the compiler is going to use what we specifywhen we set up our objects like this.But because we have a default parameter now we have the option to leave out the second parameter andthe compiler is going to auto fill that end.You see that if we specify one parameter and we don’t have any constructor, that only takes one parameter.We’re not going to get a compiler error.The compiler knows that it can auto fill in the second parameter by using the default parameter thatwe specified in our constructor declaration here.And another thing you should know is that if you are splitting your constructor into declaration anddefinition like we are doing here, the declaration is in the header file and the definition is in theimplementation file.We don’t have to specify the default parameter again in the implementation.If you do that, you’re going to get a compiler error because this is not valid C plus plus syntax.So we don’t need to do that.Okay.So now we have this and we can try and build and see what the compiler is going to do.It’s going to use the default parameter and autofill that N and our height is going to be ten.If we clear and run Rooster, we’re going to see that it has used ten as our height and we have thesame output as we did before.But another thing you can do is also specify that the first parameter is going to have a default value.We can go in and put that in and now we will have a problem because now we have the option to set upa call like this to construct our cylinder object, but the compiler is going to see that it has twooptions to do this.It can either use the default constructor, which is not going to take any parameter, or it can callour cylinder constructor, which is taking two parameters, but use the default parameters in here sothe compiler doesn’t know which one to choose.You’re going to see that if we comment out any of these, let’s comment out the default one becauseit is easier.It doesn’t have an implementation, so we can just comment it out here in the header.We can go back to Main and we’re going to see that the squiggly line is going to go away.And if we build, we’re going to get the output that we want.You see, the build is good.We can clear and run Rooster.We’re going to see our volume here.But I think it is better to really prove that this constructor was used.We can either do that through the debugger.If we set up a breakpoint in the constructor here, we’re going to see that.But we can also put an output statement in here.So let’s say Stdcout two parameter constructor called and we’re going to put Stdendl here and you seethat Stdcout has a squiggly line here.The reason is we don’t have access to Iostream here.It wasn’t included in this file and this is not going to compile.So we need to include that.We can include iostream here and the moment we do that, the squiggly line here should go away and weshould be able to compile and run this application and prove that the two parameter constructor wasused to build this object.Here.Let’s do that.We’re going to run our task to build with GCC.The world is good.We can clear and run Rooster.We’re going to see two parameter constructor called.It is going to construct our object and if we print the volume, we’re going to see that the defaultvalues were used.If, for example, we go in and change the default height to be five and build again, we’re going tosee that it was used.So let’s build.We’re going to use GCC to do that.You can really play with us however you want.You see, the build is good.We can clear and run roster.We’re going to see that our volume is going to change because now the default is five for the height.And if you plug in this radius and this height and compute your volume, you’re going to see that thevolume is exactly what you see here.We can also comment out this constructor.We can do that, but we will have to also go in the implementation file and comment that out and stilluse the default.Let’s go back to our header and we’re going to uncomment the default.Now we’re going to be using the default, which is not passing in any parameter and it’s going to usewhatever we initialized our member variables with.So our volume is going to change.If we use this default constructor here, we can build and really prove this.So we’re going to run the task to build with GCC.You see, the build is good.We can clear and run Rooster.Now you see that our volume is 3.14 because we are basically using a base radius of one and a heightof one.So if we multiply these guys, we’re just going to get Pi, which is 3.14 here and you see that eitherof these constructors is going to work, but we can’t have them both in or the compiler is going tobe confused.Okay.So this is really all we set out to do in this lecture showing you how you can use default parametersfor your constructors.Let’s uncomment all these constructors and only in one with a single default parameter and we’re goingto hop over in the CPP file and uncomment this.I think this is going to be better.And if we set up a call like this, we’re going to use the default constructor.But we can call this with only one parameter and put in one parameter here for the base radius.So the syntax to do default parameters is really simple.You just specify your value with an equal sign here and it’s going to be used by the compiler.When you specify less than the full number of parameters you have in here, you just have to be carefulto order your parameters in a way that makes sense to you.If you specify the default parameter first here, for example, it’s really going to be awkward becauseyou can’t specify a default parameter and have the other one not filled.Let’s show you what I mean here.And I am lucky I remembered this.So let’s put an equal sign here and specify the default parameter first here.If we do things like this, we don’t really have a way we can call this because if we go in here andonly specify one parameter, the compiler won’t have a way to guess the second one and we’re going toget a compiler error.So we really have no choice except specifying all the parameters here.And this way the compiler is going to be happy.So it’s going to ignore the default parameter here and it’s going to use everything you specify whenyou try to construct an object here.Let’s try and build and see that this is actually going to work out and we’re going to see that we havea compiler error.Actually, the compiler is going to stop us from doing something like this because it knows it reallydoesn’t make sense.You want to have a way to call this constructor and use this default parameter if you specify it likethis here.So this is even better.The compiler is stopping us from setting up something that will never make sense.So this is good.We’re going to put our default argument in the second position here and you’re going to see that thecompiler now is going to be happy.If we build, we should build fine.You see, the build is good and we are able to only specify one parameter.When we call our constructor here.We are going to stop here in this lecture and the next one we’re going to learn about initializer lists.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,同学你好!今天我们来学习 C++ 中一个非常实用的特性:构造函数 (constructor) 的默认参数 (default parameters)。这能让我们的类 (class) 设计更加灵活。

1. 标题与概要

标题: C++ 构造函数中的默认参数:让对象创建更灵活

概要:
本节课我们将学习如何为 C++ 类中的构造函数设置默认参数。这和为普通函数设置默认参数的语法是完全一样的。通过使用默认参数,我们可以在创建对象时提供更少的参数,编译器会自动使用预设的默认值。同时,我们也会探讨当默认参数与默认构造函数并存时可能引发的歧义问题以及如何解决。

2. 详细解释

让我们一步一步来理解构造函数默认参数的方方面面。

2.1 什么是构造函数?
在 C++ 中,构造函数是一种特殊的成员函数 (member function),它在创建类的新对象 (object) 时自动被调用。它的主要目的是初始化对象的数据成员 (data members)。构造函数的名称与类名完全相同,并且没有返回类型 (return type)。

例如,我们有一个 Cylinder(圆柱体)类:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
class Cylinder {
private:
double base_radius; // 底面半径
double height; // 高度

public:
// 这是一个构造函数
Cylinder(double r, double h) {
base_radius = r;
height = h;
}
// ... 其他成员 ...
};

2.2 什么是默认参数?
默认参数允许你为一个或多个函数参数指定默认值。如果在调用函数时没有为这些参数提供值,编译器就会自动使用你指定的默认值。这个概念同样适用于构造函数。

2.3 为构造函数设置默认参数的语法
语法非常简单,就是在构造函数的声明中,为你想要设置默认值的参数使用等号 (=) 赋予一个值。

假设我们的 Cylinder 类有一个接收两个参数的构造函数:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Cylinder.h (头文件声明)
class Cylinder {
private:
double base_radius;
double height;
static const double PI; // 声明PI为静态常量

public:
// 构造函数声明,height 参数有默认值 10.0
Cylinder(double r_param, double h_param = 10.0);

// 获取器 (getters)
double get_base_radius() const;
double get_height() const;

// 设置器 (setters)
void set_base_radius(double r_param);
void set_height(double h_param);

double volume() const;
};

在这个例子中,h_param 有一个默认值 10.0

重要规则:

  1. 声明位置: 默认参数值通常在函数的声明中指定(通常是在头文件 .h.hpp 中),而不是在函数的定义中(通常是在源文件 .cpp 中)。如果在定义中也指定,或者只在定义中指定,可能会导致编译错误或不一致的行为。

    cpp
    1
    2
    3
    4
    5
    6
    7
    // Cylinder.cpp (源文件定义)
    // 注意:这里不需要再写 = 10.0
    Cylinder::Cylinder(double r_param, double h_param) {
    base_radius = r_param;
    height = h_param;
    std::cout << "带参数的构造函数被调用。半径: " << base_radius << ", 高度: " << height << std::endl;
    }

    如果你在定义中也写了 Cylinder::Cylinder(double r_param, double h_param = 10.0),编译器通常会报错,提示“默认参数重定义 (redefinition of default argument)”。

  2. 尾部参数优先: 如果一个参数有默认值,那么它右边的所有参数都必须有默认值。

    • 正确:MyClass(int a, int b = 10, int c = 20);
    • 错误:MyClass(int a = 5, int b, int c = 20); // b 没有默认值,但它在 a (有默认值) 之后
      这是因为编译器是从左到右匹配你传递的参数。如果中间某个参数你省略了,编译器就无法知道你后续传递的参数是对应哪一个了。

2.4 如何使用带默认参数的构造函数
有了上面定义的 Cylinder(double r_param, double h_param = 10.0),我们可以这样创建对象:

  • 提供所有参数:

    cpp
    1
    Cylinder c1(5.0, 12.0); // base_radius = 5.0, height = 12.0 (提供的12.0覆盖了默认的10.0)

    这种情况下,编译器会忽略为 h_param 设置的默认值 10.0,而使用我们显式传递的 12.0

  • 只提供没有默认值的参数(或部分有默认值的参数):

    cpp
    1
    Cylinder c2(7.0); // base_radius = 7.0, height = 10.0 (编译器自动使用h_param的默认值)

    由于我们只提供了一个参数,编译器会将其匹配给第一个参数 r_param。对于第二个参数 h_param,由于我们没有提供值,编译器就会使用其默认值 10.0

2.5 所有参数都有默认值的情况
我们甚至可以为所有参数都提供默认值:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Cylinder.h (头文件声明)
class Cylinder {
// ...
public:
// 所有参数都有默认值
Cylinder(double r_param = 1.0, double h_param = 1.0);
// ...
};

// Cylinder.cpp (源文件定义)
Cylinder::Cylinder(double r_param, double h_param) { // 定义中不重复默认值
base_radius = r_param;
height = h_param;
std::cout << "带参数的构造函数被调用。半径: " << base_radius << ", 高度: " << height << std::endl;
}

现在,我们可以这样创建对象:
cpp
1
2
3
Cylinder c3(2.0, 3.0); // base_radius = 2.0, height = 3.0
Cylinder c4(5.0); // base_radius = 5.0, height = 1.0 (默认)
Cylinder c5; // base_radius = 1.0 (默认), height = 1.0 (默认)

注意最后一种情况 Cylinder c5;,它看起来就像是在调用一个默认构造函数 (default constructor)(即不带任何参数的构造函数)。事实上,如果一个构造函数的所有参数都有默认值,那么它也可以充当默认构造函数的角色。

2.6 默认参数引发的歧义 (ambiguity)
这是一个非常重要的点!如果你的类中同时存在:

  1. 一个所有参数都有默认值的构造函数,例如 Cylinder(double r = 1.0, double h = 1.0);
  2. 一个显式定义的无参默认构造函数,例如 Cylinder();

那么,当你尝试这样创建对象时:

cpp
1
Cylinder myCylinder;

编译器会感到困惑!它不知道应该调用哪个构造函数:是调用 Cylinder(double r = 1.0, double h = 1.0) 并使用所有默认值呢,还是调用显式的 Cylinder()?这种情况会导致编译错误 (compiler error),提示调用具有歧义。

如何解决歧义?
你必须做出选择,只保留一个能够处理无参调用的构造函数。

  • 方法一: 移除显式的无参默认构造函数 Cylinder();,让带有全部默认参数的构造函数 Cylinder(double r = 1.0, double h = 1.0); 来处理无参调用。
  • 方法二: 移除或修改带有全部默认参数的构造函数,使其至少有一个参数没有默认值(或者干脆移除所有默认值),然后保留显式的无参默认构造函数 Cylinder();

选择哪种方法取决于你的设计意图。通常,如果一个构造函数通过默认参数能自然地覆盖无参情况,那么保留它并移除单独的无参默认构造函数是更简洁的做法。

2.7 示例回顾与补充
在提供的原始文本中,作者演示了 Cylinder 类的例子:

  • 最初有两个构造函数:一个默认的 Cylinder(); 和一个带两个参数的 Cylinder(double param_radius, double param_height);
  • 然后,他修改了带参数的构造函数,为 param_height 添加了默认值:Cylinder(double param_radius, double param_height = 10.0);。此时,调用 Cylinder my_cyl(5.0); 成为可能,height 会是 10.0
  • 接着,他也为 param_radius 添加了默认值:Cylinder(double param_radius = 1.0, double param_height = 10.0);。这时,如果原来的 Cylinder(); 默认构造函数仍然存在,那么当写下 Cylinder my_cyl; 时,就会产生歧义。作者通过注释掉 Cylinder(); 来解决这个问题。
  • 作者还强调了默认参数必须是尾随参数。如果尝试 Cylinder(double param_radius = 1.0, double param_height);,编译器会报错,因为它不知道如果你只传一个参数,这个参数是给 param_height 还是说要使用 param_radius 的默认值(后者是不可能的,因为 param_height 没有默认值且在它之后)。

2.8 关于 PI 的处理
在示例代码中,PI 被定义为一个常量。在类中,如果需要在多个成员函数中使用一个共享的常量,通常将其定义为 static const 成员,或者在 C++17 及以后版本使用 inline static conststatic constexpr
.h 文件中:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
// Cylinder.h
class Cylinder {
// ...
public:
// C++17 之前,通常在 .h 声明,在 .cpp 定义并初始化
// static const double PI;
// C++17 及之后,可以在类内直接初始化 inline static const/constexpr
inline static const double PI = 3.1415926535; // 或者 static constexpr double PI = ...;
// ...
};

// Cylinder.cpp (如果 PI 不是 inline static const/constexpr)
// const double Cylinder::PI = 3.1415926535;

或者,像原始文本那样,直接在需要它的 .cpp 文件中定义一个全局常量,或者包含一个定义了此常量的头文件。为了简单起见,作者将其直接放在了 cylinder.h 中(作为全局常量或命名空间内的常量),这在小型项目中也可以接受,但对于大型项目,放在类作用域内或者一个专门的数学常量命名空间会更好。

3. 代码示例

我们将创建一个 Cylinder 类,并演示构造函数默认参数的使用。

cylinder.h (头文件)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef CYLINDER_H
#define CYLINDER_H

#include <iostream> // 为了在构造函数中输出调试信息

// 将 PI 定义为一个可以在类内外使用的常量
// 更现代的做法是使用 C++17 的 inline static const double PI = ...; 在类中
// 或者 #include <numbers> 然后使用 std::numbers::pi
const double PI_CONST = 3.1415926535;

class Cylinder {
private:
double base_radius; // 底面半径 (base radius)
double height; // 高度 (height)

public:
// 构造函数:
// base_radius 默认值为 1.0
// height 默认值为 1.0
// 注意:默认值只在声明中给出
Cylinder(double r_param = 1.0, double h_param = 1.0);

// 为了演示歧义,我们先注释掉一个显式的默认构造函数
// Cylinder(); // 如果这个也存在,并且上面的构造函数所有参数都有默认值,就会有歧义

// 获取器 (Getters)
double get_base_radius() const;
double get_height() const;

// 设置器 (Setters)
void set_base_radius(double r_param);
void set_height(double h_param);

// 计算体积 (volume)
double volume() const;
};

#endif // CYLINDER_H

cylinder.cpp (实现文件)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "cylinder.h" // 包含类声明
#include <iostream> // 为了 std::cout 和 std::endl

// 构造函数定义
// 注意:这里不再重复默认参数值
Cylinder::Cylinder(double r_param, double h_param) : base_radius(r_param), height(h_param) {
// 使用成员初始化列表 (member initializer list) 是推荐的做法
// base_radius = r_param; // 也可以在函数体内赋值
// height = h_param;
std::cout << "Cylinder 带参数构造函数被调用: 半径=" << base_radius
<< ", 高度=" << height << std::endl;
}

/*
// 如果需要一个显式的默认构造函数,且与带默认参数的构造函数不冲突
Cylinder::Cylinder() : base_radius(0.5), height(0.5) { // 提供不同的默认值
std::cout << "Cylinder 显式默认构造函数被调用: 半径=" << base_radius
<< ", 高度=" << height << std::endl;
}
*/


// Getter 实现
double Cylinder::get_base_radius() const {
return base_radius;
}

double Cylinder::get_height() const {
return height;
}

// Setter 实现
void Cylinder::set_base_radius(double r_param) {
base_radius = r_param;
}

void Cylinder::set_height(double h_param) {
height = h_param;
}

// Volume 计算实现
double Cylinder::volume() const {
return PI_CONST * base_radius * base_radius * height;
}

main.cpp (主程序,使用 Cylinder 类)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include "cylinder.h" // 引入 Cylinder 类

int main() {
std::cout << "--- 创建 c1 (提供所有参数) ---" << std::endl;
Cylinder c1(2.0, 3.0); // 使用 r=2.0, h=3.0
std::cout << "c1 体积: " << c1.volume() << std::endl;
std::cout << "c1 半径: " << c1.get_base_radius() << ", 高度: " << c1.get_height() << std::endl;
std::cout << std::endl;

std::cout << "--- 创建 c2 (只提供一个参数,第二个用默认值) ---" << std::endl;
Cylinder c2(5.0); // 使用 r=5.0, h=1.0 (默认)
std::cout << "c2 体积: " << c2.volume() << std::endl;
std::cout << "c2 半径: " << c2.get_base_radius() << ", 高度: " << c2.get_height() << std::endl;
std::cout << std::endl;

std::cout << "--- 创建 c3 (不提供参数,全部用默认值) ---" << std::endl;
Cylinder c3; // 使用 r=1.0 (默认), h=1.0 (默认)
std::cout << "c3 体积: " << c3.volume() << std::endl;
std::cout << "c3 半径: " << c3.get_base_radius() << ", 高度: " << c3.get_height() << std::endl;
std::cout << std::endl;

// 编译指令示例 (假设你在Linux/macOS终端或MinGW/Cygwin环境):
// g++ main.cpp cylinder.cpp -o cylinder_test
// ./cylinder_test

// 如果取消 cylinder.h 和 cylinder.cpp 中对显式默认构造函数的注释,
// 并且 Cylinder(double r_param = 1.0, double h_param = 1.0) 仍然存在,
// 那么 Cylinder c3; 这一行会导致编译错误 (调用歧义)。

return 0;
}

预期输出:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
--- 创建 c1 (提供所有参数) ---
Cylinder 带参数构造函数被调用: 半径=2, 高度=3
c1 体积: 37.6991
c1 半径: 2, 高度: 3

--- 创建 c2 (只提供一个参数,第二个用默认值) ---
Cylinder 带参数构造函数被调用: 半径=5, 高度=1
c2 体积: 78.5398
c2 半径: 5, 高度: 1

--- 创建 c3 (不提供参数,全部用默认值) ---
Cylinder 带参数构造函数被调用: 半径=1, 高度=1
c3 体积: 3.14159
c3 半径: 1, 高度: 1

4. QA 闪卡 (QA Flash Cards)

  1. 问: 如何为C++构造函数参数指定默认值?
    答: 在构造函数的声明中(通常是头文件中),使用等号 = 为参数赋一个默认值。例如:MyClass(int count = 0, double value = 10.5);

  2. 问: 如果一个构造函数的所有参数都有默认值,它有什么特殊作用?
    答: 它可以像默认构造函数(无参构造函数)一样被调用。例如,如果 MyClass(int val = 0);,那么 MyClass obj; 是合法的,val 会被设为 0

  3. 问: 默认参数值应该在函数声明中还是定义中指定?
    答: 应该在函数/构造函数的声明中(通常是 .h 文件)。定义(.cpp 文件)中不应重复指定。

  4. 问: 关于默认参数的顺序有什么规则?
    答: 具有默认值的参数必须位于参数列表的末尾。一旦某个参数有了默认值,其后的所有参数都必须有默认值。例如 void func(int a, int b = 10, int c = 20); 是正确的,但 void func(int a = 5, int b, int c = 20); 是错误的。

  5. 问: 什么情况下,使用默认参数的构造函数会与默认构造函数产生歧义?
    答: 当类中同时存在一个显式的无参默认构造函数(如 MyClass();)和一个所有参数都有默认值的构造函数(如 MyClass(int val = 0);)时,若尝试无参数创建对象(如 MyClass obj;),编译器会报错,因为它不知道该调用哪一个。

5. 常见误解或易犯错误

  1. 在定义中重复默认值:

    • 误:在 .hCylinder(double r = 1.0);,在 .cppCylinder::Cylinder(double r = 1.0) { ... }
    • 正:默认值只在声明(.h 文件)中。.cpp 中应该是 Cylinder::Cylinder(double r) { ... }
  2. 非尾随默认参数 (non-trailing default parameters):

    • 误:Cylinder(double r = 1.0, double h);
    • 正:如果 r 有默认值,则 h 也必须有,或者 r 不能有默认值如果 h 没有。正确的是 Cylinder(double r, double h = 1.0);Cylinder(double r = 1.0, double h = 2.0);
  3. 忽视歧义问题:

    • 误:同时定义 Cylinder();Cylinder(double r = 1.0, double h = 1.0);,然后尝试 Cylinder c;
    • 正:确保只有一种方式可以无参数地构造对象,要么移除其中一个,要么修改带默认参数的构造函数使其至少有一个参数没有默认值。
  4. 认为默认参数会改变函数签名 (function signature) 从而允许重载 (overloading):

    • 误解:认为 void foo(int x);void foo(int x = 0); 是两个可以共存的重载函数。
    • 实际:它们是同一个函数的声明。默认参数只是提供了一种调用该函数时的便利,并没有创建新的可重载函数。如果同时声明 void foo(int x);void foo(int x = 0);,编译器通常会认为是重定义或冲突。如果先声明 void foo(int x = 0); 然后再声明 void foo(int x);(没有默认值),可能会被视为对默认参数的移除,这在不同作用域可能是合法的,但通常应避免这种混淆。关键是,对编译器而言,foo(int) 是这个函数的基本签名。
  5. 忘记包含必要的头文件:

    • 例如,在构造函数的实现(.cpp 文件)中使用了 std::cout 但忘记了 #include <iostream>。虽然这不直接与默认参数相关,但在编写示例和测试时很容易犯。

6. 编码练习

现在,轮到你来实践一下了!

我们有一个 Box(盒子)类,它有 length(长度)、width(宽度)和 height(高度)三个属性。
请你完成以下任务:

  1. 修改 Box.h 中的构造函数声明,使其:
    • length 参数没有默认值。
    • width 参数的默认值为 1.0
    • height 参数的默认值为 1.0
  2. main.cpp 中,使用这个修改后的构造函数创建三个 Box 对象:
    • box1: 只提供 length5.0
    • box2: 提供 length4.0width3.0
    • box3: 提供 length2.0width2.0height2.0
  3. 打印出这三个盒子的体积。

Box.h

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef BOX_H
#define BOX_H

#include <iostream>

class Box {
private:
double length;
double width;
double height;

public:
// TODO: 修改此构造函数声明以满足练习要求
Box(double l, double w, double h);

double volume() const;
void print_dimensions() const;
};

#endif //BOX_H

Box.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Box.h"

// 构造函数定义 (你不需要修改这个文件,默认值只在 .h 文件中)
Box::Box(double l, double w, double h) : length(l), width(w), height(h) {
std::cout << "Box created with L=" << length << ", W=" << width << ", H=" << height << std::endl;
}

double Box::volume() const {
return length * width * height;
}

void Box::print_dimensions() const {
std::cout << "Box dimensions: L=" << length << ", W=" << width << ", H=" << height << std::endl;
}

main.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include "Box.h"

int main() {
std::cout << "--- 练习开始 ---" << std::endl;

// TODO 1: 创建 box1,只提供 length = 5.0
// Box box1(...);
// std::cout << "Box1: ";
// box1.print_dimensions();
// std::cout << "Box1 volume: " << box1.volume() << std::endl << std::endl;

// TODO 2: 创建 box2,提供 length = 4.0, width = 3.0
// Box box2(...);
// std::cout << "Box2: ";
// box2.print_dimensions();
// std::cout << "Box2 volume: " << box2.volume() << std::endl << std::endl;

// TODO 3: 创建 box3,提供 length = 2.0, width = 2.0, height = 2.0
// Box box3(...);
// std::cout << "Box3: ";
// box3.print_dimensions();
// std::cout << "Box3 volume: " << box3.volume() << std::endl << std::endl;

std::cout << "--- 练习结束 ---" << std::endl;
return 0;
}

请你将 Box.hmain.cpp// TODO 部分的代码补充完整。完成后,将你修改的 Box.hmain.cpp 内容发给我,我会帮你检查并给出反馈!加油!


评论