构造函数默认参数
🧑💻 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
(圆柱体)类:
1 | class Cylinder { |
2.2 什么是默认参数?
默认参数允许你为一个或多个函数参数指定默认值。如果在调用函数时没有为这些参数提供值,编译器就会自动使用你指定的默认值。这个概念同样适用于构造函数。
2.3 为构造函数设置默认参数的语法
语法非常简单,就是在构造函数的声明中,为你想要设置默认值的参数使用等号 (=
) 赋予一个值。
假设我们的 Cylinder
类有一个接收两个参数的构造函数:
1 | // Cylinder.h (头文件声明) |
在这个例子中,
h_param
有一个默认值 10.0
。
重要规则:
声明位置: 默认参数值通常在函数的声明中指定(通常是在头文件
.h
或.hpp
中),而不是在函数的定义中(通常是在源文件.cpp
中)。如果在定义中也指定,或者只在定义中指定,可能会导致编译错误或不一致的行为。cpp1
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)”。尾部参数优先: 如果一个参数有默认值,那么它右边的所有参数都必须有默认值。
- 正确:
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)
,我们可以这样创建对象:
提供所有参数:
cpp1
Cylinder c1(5.0, 12.0); // base_radius = 5.0, height = 12.0 (提供的12.0覆盖了默认的10.0)
这种情况下,编译器会忽略为
h_param
设置的默认值10.0
,而使用我们显式传递的12.0
。只提供没有默认值的参数(或部分有默认值的参数):
cpp1
Cylinder c2(7.0); // base_radius = 7.0, height = 10.0 (编译器自动使用h_param的默认值)
由于我们只提供了一个参数,编译器会将其匹配给第一个参数
r_param
。对于第二个参数h_param
,由于我们没有提供值,编译器就会使用其默认值10.0
。
2.5 所有参数都有默认值的情况
我们甚至可以为所有参数都提供默认值:
1 | // Cylinder.h (头文件声明) |
现在,我们可以这样创建对象:
1 | Cylinder c3(2.0, 3.0); // base_radius = 2.0, height = 3.0 |
注意最后一种情况
Cylinder c5;
,它看起来就像是在调用一个默认构造函数 (default constructor)(即不带任何参数的构造函数)。事实上,如果一个构造函数的所有参数都有默认值,那么它也可以充当默认构造函数的角色。
2.6 默认参数引发的歧义 (ambiguity)
这是一个非常重要的点!如果你的类中同时存在:
- 一个所有参数都有默认值的构造函数,例如
Cylinder(double r = 1.0, double h = 1.0);
- 一个显式定义的无参默认构造函数,例如
Cylinder();
那么,当你尝试这样创建对象时:
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 const
或 static constexpr
。
在 .h
文件中:
1 | // Cylinder.h |
或者,像原始文本那样,直接在需要它的
.cpp
文件中定义一个全局常量,或者包含一个定义了此常量的头文件。为了简单起见,作者将其直接放在了 cylinder.h
中(作为全局常量或命名空间内的常量),这在小型项目中也可以接受,但对于大型项目,放在类作用域内或者一个专门的数学常量命名空间会更好。
3. 代码示例
我们将创建一个 Cylinder
类,并演示构造函数默认参数的使用。
cylinder.h
(头文件)
1 |
|
cylinder.cpp
(实现文件)
1 |
|
main.cpp
(主程序,使用 Cylinder 类)
1 |
|
预期输出:
1 | --- 创建 c1 (提供所有参数) --- |
4. QA 闪卡 (QA Flash Cards)
问: 如何为C++构造函数参数指定默认值?
答: 在构造函数的声明中(通常是头文件中),使用等号=
为参数赋一个默认值。例如:MyClass(int count = 0, double value = 10.5);
问: 如果一个构造函数的所有参数都有默认值,它有什么特殊作用?
答: 它可以像默认构造函数(无参构造函数)一样被调用。例如,如果MyClass(int val = 0);
,那么MyClass obj;
是合法的,val
会被设为0
。问: 默认参数值应该在函数声明中还是定义中指定?
答: 应该在函数/构造函数的声明中(通常是.h
文件)。定义(.cpp
文件)中不应重复指定。问: 关于默认参数的顺序有什么规则?
答: 具有默认值的参数必须位于参数列表的末尾。一旦某个参数有了默认值,其后的所有参数都必须有默认值。例如void func(int a, int b = 10, int c = 20);
是正确的,但void func(int a = 5, int b, int c = 20);
是错误的。问: 什么情况下,使用默认参数的构造函数会与默认构造函数产生歧义?
答: 当类中同时存在一个显式的无参默认构造函数(如MyClass();
)和一个所有参数都有默认值的构造函数(如MyClass(int val = 0);
)时,若尝试无参数创建对象(如MyClass obj;
),编译器会报错,因为它不知道该调用哪一个。
5. 常见误解或易犯错误
在定义中重复默认值:
- 误:在
.h
中Cylinder(double r = 1.0);
,在.cpp
中Cylinder::Cylinder(double r = 1.0) { ... }
。 - 正:默认值只在声明(
.h
文件)中。.cpp
中应该是Cylinder::Cylinder(double r) { ... }
。
- 误:在
非尾随默认参数 (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);
。
- 误:
忽视歧义问题:
- 误:同时定义
Cylinder();
和Cylinder(double r = 1.0, double h = 1.0);
,然后尝试Cylinder c;
。 - 正:确保只有一种方式可以无参数地构造对象,要么移除其中一个,要么修改带默认参数的构造函数使其至少有一个参数没有默认值。
- 误:同时定义
认为默认参数会改变函数签名 (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)
是这个函数的基本签名。
- 误解:认为
忘记包含必要的头文件:
- 例如,在构造函数的实现(
.cpp
文件)中使用了std::cout
但忘记了#include <iostream>
。虽然这不直接与默认参数相关,但在编写示例和测试时很容易犯。
- 例如,在构造函数的实现(
6. 编码练习
现在,轮到你来实践一下了!
我们有一个 Box
(盒子)类,它有 length
(长度)、width
(宽度)和 height
(高度)三个属性。
请你完成以下任务:
- 修改
Box.h
中的构造函数声明,使其:length
参数没有默认值。width
参数的默认值为1.0
。height
参数的默认值为1.0
。
- 在
main.cpp
中,使用这个修改后的构造函数创建三个Box
对象:box1
: 只提供length
为5.0
。box2
: 提供length
为4.0
和width
为3.0
。box3
: 提供length
为2.0
、width
为2.0
和height
为2.0
。
- 打印出这三个盒子的体积。
Box.h
1 |
|
Box.cpp
1 |
|
main.cpp
1 |
|
请你将
Box.h
和 main.cpp
中 // TODO
部分的代码补充完整。完成后,将你修改的 Box.h
和 main.cpp
内容发给我,我会帮你检查并给出反馈!加油!