avatar

目录
299 - 显式构造函数

显式构造函数

🧑‍💻 User

In this lecture, we’re going to learn about explicit constructors.And to really drive the point home, we’re going to look at a simple example.Here is a class which is called Square.It is going to be modeling a square.So we’re going to use a member variable, which is going to represent the size of our square, and wecan use this.Then we’re going to have a function which is going to compute the surface of this square here, andit is going to be a cast member function.This function is really simple and there is really nothing more one can say about this.Let’s look at how we can implement the functions we have and we could implement our constructor usinginitializer list like this.So we’re going to take the side member variable and we’re going to initialize this with our side param.Then we’re going to compute the surface.The surface is going to basically be side multiplied by side and then we’re going to return this.And whoever called this surface function on our class object is going to be able to use the return valuehere.The destructor is going to be empty.It’s basically going to do nothing.It’s just going to be in there.So this is how we could set up our square class.Okay, so suppose we have our square class like we have seen, and we set up a function which is goingto compare two squares.So we have the compare function here and it’s going to take two square parameters by const referencebecause we don’t want to be copying squares here and we just want to use the original values and readfrom them and do some computation in the body.Here in the body you see that we are comparing the surfaces for the squares.So we are basically saying if the surface of square one is greater, then we’re going to consider thatas the greater square.And then if the surface of square two is greater, we’re going to consider that as the greater square.So what’s the function here will be doing is checking if square one is greater than square two.So what we have in the first position here is going to be compared to what we have in position two hereand in the body.What we’re going to say is ask if the surface of square one is greater than the surface of square twohere.If that’s true, we’re going to return true.If it’s not true, we’re going to return false.This is what the function here is doing.So once we have the function here, we can call it in the main function.We’re going to have two squares, square one and square two, and we can compare them like we do here.But the problem is going to come if we use arguments that may involve implicit conversions.And look at the example we have here.We are basically comparing square one with 45.5, which is a double literal here.When we issue a call like this, the compiler is going to say that the second parameter to this functionis really a square and it’s going to ask itself, Can I do an implicit conversion from this argumentI have here to a square?And the fact is it can because our constructor is only taking one parameter.So what the compiler is going to do is convert this guy into a square object and then it is going tocompare these two square objects.And if these implicit conversions are what you want, this is going to be fine.But sometimes the color of this function will really want to compare a square to a number.So they want to take the square one and compare that to 44.5, if this makes sense for whatever youare doing in your application.And in that case, implicit conversions are not going to be what you want and you’re going to get wrongresults.If you don’t want these implicit conversions, it is possible to mark your constructor as explicit,and what this tells the compiler is to never use this constructor in implicit conversions.So if we have this constructor here and this explicit and we try to issue a call like this, we’re goingto get a compiler error because the constructor for our square object can never be used in implicitconversions.And what we are doing here is really going to set up implicit conversions so you can use explicit constructorsif you don’t want your constructor to be involved in implicit conversions that are done by the compiler.Okay.I really hope you understand this.So we may also fall in the same case if we have a constructor which doesn’t take only one parameters,but we have a single parameter which is not using a default parameter.Let’s show you what I mean here.Here we have our side parameter, but we have a second parameter which is specified by default.So if we have this set up and we have the same call we have in the main function with the second parameterset up as 44.5 the.Compiler is going to see that it can still use this constructor to do implicit conversions from a numberto this square object here.And it’s going to also use this constructor.So if we don’t want implicit conversions, we can also mark this constructor as an explicit constructorand it is going to be prevented from being used in implicit conversions.This can be confusing, and if you still don’t really understand this, please bear with me.We’re going to head over to Visual Studio code and play with this a little more.Okay.Here we are in our working folder.The current project is explicit constructors.We’re going to use our template files here, so let’s grab them and we’re going to paste them in placeand we’re going to open this little guy in Visual Studio code by dragging and dropping here.This is going to open our folder.We’re going to have our main function and we need to create our square class because we want to playwith it in here.So what we’re going to do is create the header file and the CPP file for that.Let’s do square dot h for the header and we’re going to add a new file for square dot CPP.This is done and we are going to head in our header file and put in our class.So we don’t want to type this.I am just going to put it in here.Then we’re going to explain it a little bit.We have our include guards and then we have our class which is declared and defined Here.We have two sections in the class.We have a public section and we have a private section.And our private section we have our member variable which is declared to be inside.It’s type is double.And in the public section, we’re going to have a constructor which is going to take one parameter.We have a destructor which is going to destroy our object and we have a function which is going to computeour surface here.Nothing special here so far.We are going to head in square dot CPP and put in the implementation for our class so we have the implementationfor the constructor.It is going to be using initializer list.You see the syntax here we have our colon, we have our member variable and we are using curly bracesto do that.So you can use curly braces with your initializer list.This is going to work down here.You see we have the surface function.It is a cast member function.We need to specify that here and it is basically going to return M side multiplied by M side becausethat’s going to be the surface and the destructor is really not doing anything.So it is going to be empty here, but we’re going to leave this in.Okay.So now that we have this little guy here, let’s head over to the main function and play with it.So we’re going to close the left sidebar here because we don’t need it anymore.We’re going to take out whatever we don’t need in the main CPP file.We’re going to include our square class.We can do that and then we’re going to put in the function, which is going to compare two squares.We have the function called compare.It’s going to compare square one and square two.The parameters are passed by const reference because we don’t want to copy our square objects.We just want to read the data from them, compare them and return the result.So we’re going to ask, is the surface of square one greater than the surface of square two?That’s what this function here is doing.If square one is greater than two in terms of surface, we’re going to return.True.If square one is not greater, we’re going to return false.That’s what we are doing here.So we can set up two square objects and try this function here.We’re going to say square and we’re going to call this s one.We’re going to put in a 10.0 and we’re going to do a second one.This one is going to be S two and we’re going to put in a 20.0.You already know which one is greater.So we’re going to try and use our function here and see that it is actually working.We’re going to say S one greater than S two and we’re going to do that using the call here.Compare s one to S two.We’re going to be passing this by reference, by the way, and we want the output to be in the formof true and false.So we’re going to put our bool alpha manipulator here.So we’re going to say Stdcout, STD Bull Alpha, and we’re going to build and run this program.We expect to see a false because the surface of square one is really not going to be greater than thesurface of square two here.So we’re going to see a false printed out here.Let’s world we’re going to bring up a terminal and then we’re going to use the task to build with GCC.This is going to build define we can clear and run rooster and we’re going to see that square one isnot greater than square two.This is what we expect.We can change the side of square one to be 100 and build again.And we see that our function is doing what we want.We’re going to clear the build is good, by the way.We can clear and run Rooster.Now we’re.To see that square one is greater than square two because its surface is definitely going to be greaterthan that of square two.Here, this is working, but we may have problems if we set up a call that involves implicit conversions.So let’s do a call in which we want to compare square one to a double literal.We can do that.So we’re going to put our stdcout statement here and we want to see if square one is greater than 45.9.For example, we can do this.So we’re going to put 45.9 here and notice that we don’t have a squiggly line or any problem.And if we go back to our compare function, you see that we don’t have any overload.That takes the second parameter as a double.So the compiler is going to look at the call here and it’s going to say, Let me see if I can use asecond parameter as a double here.And it’s going to say, Oh, I cannot do that because the second parameter is a square object.The compiler isn’t going to give up easily.So it’s going to say, I can’t use a double argument for the second parameter here directly, but it’sgoing to ask is it possible that I can convert from this double to a square object?And it’s going to look at the constructors we have in here?It’s going to say, Huh, I have one constructor which is going to take a double parameter.I can use this and convert from this double argument I have here to a square object and the compileris going to do that and create a temporary somewhere, which is going to be a square object and it isgoing to pass that square object in this function here and do the comparison and then it’s going tothrow the temporary away and it’s going to give you the result.So what we will be doing here is really comparing two squares, one square, whose side is 100 and anotherwhose side is 45.9.And this can really be hard to see because what you see here is a number.And you might think that the compiler is really comparing square one to a number, but it is going tobe comparing two squares because it inserted an implicit conversion from this double here to a square.If this is what you want, we’re going to see that this is going to compare two squares and we’re goingto see our output in a minute.So let’s do that.We’re going to build this with GCC so that you see that this is going to build without a problem.The world is good and we can see our output here.We’re going to run Rooster and we’re going to see that square one is actually greater than 45.9 becausesquare one has a side which is 100, and the square two is going to be having a side of 45.9 here.But we have an implicit conversion and sometimes this is not what we want.So if we don’t want these implicit conversions, we can mark our constructor as explicit.And what this tells the compiler is don’t use my constructor here in implicit conversions.If somebody wants to use my constructor here, they have to call it explicitly like they have createdan object here and then use that object to call the compare function.So we can do that using the explicit keyword like we see here.So if we mark this constructor as explicit, we’re going to see that at this line.Now we’re going to have a squiggly line because the compiler is not allowed to insert an implicit conversionfrom double to our square object using our constructor here.So if we use this now, we’re going to get a compiler error.You see, Visual Studio code already has a problem.If we look at the problems tab here, we’re going to see no suitable constructor exists to convert fromdouble to square.And this is a hint that the compiler is actually trying to convert from double to square and we havedisallowed that or prevented that through the explicit keyword here.And this is really cool.So we can build with GCC and see the compiler error.We’re going to see that the compiler error is going to say invalid initialization of reference constsquare.The second parameter couldn’t be initialized properly.So the second thing here is not going to work and it’s going to basically say, I can’t really figureout how to use a double argument and compare that as we are doing in the compare function here.So you’re going to get a compiler error and this is good if you don’t want your constructor involvedin implicit conversions, We may also face the same problem if we are using default arguments.And I am going to show you that here we’re going to include the string object, the string class, Ishould say, and we’re going to set up a second member variable in our class, which is going to beof string type, and we’re going to say color.This is going to be the color of our square.We might model our square like this and give it a color, and we’re going to specify a second parameterto the constructor, which is going to initialize our member variable with the default parameter.So if we do something like this, we might say const STD string reference, and then we’re going tosay color param, we can save that and we’re going to set this to black.For example, let’s say our square is going to be black by default and we need to specify the secondparameter in our implementation in the CPP file.So let’s copy that and we’re going to put that in place here.And this is right.And we need to put our initializer list to the second line and then we’re going to put in another line,which is going to initialize our color.So we’re going to take the color and initialize this with color param.Okay, We can do this and you’re going to see that the compiler is going to take this because we arejust initializing our member variables using this initializer lists.But what we want to see is that even if you have a lot of parameters, but the first one isn’t usinga default parameter and the other ones are going to be using the default parameter.This can also be used in implicit conversions and the compiler might pick this up to convert from adouble to a square object because it’s just going to use that double for the first parameter.And for the others it’s going to use the defaults.So we can also protect this and make this explicit and the compiler is not going to use it in implicitconversions.And now if we build, we’re going to get the same error we had.We’re going to build with GCC and we are basically going to be told the compiler can’t use this constructorin implicit conversions because it is explicit and this is really all explicit.Constructors are all about you.Mark your constructor explicit if you don’t want it involved in implicit conversions that are done bythe compiler.So this is all for this lecture.I hope you found it interesting.We are going to stop here in this one.In the next one we’re going to learn about copy constructors.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,没问题!作为你的C++向导,我会用最清晰、最友好的方式,带你一步步探索 explicit 构造函数的奥秘。让我们开始吧!


1. 标题与简介

标题: C++ 探秘:显式构造函数 (explicit constructor) —— 防止意外的类型转换

简介:
本节课我们将学习C++中的一个重要关键字 explicit,它用于修饰类的构造函数 (constructor)。我们将通过一个简单的 Square (正方形) 类的例子,深入理解为什么以及如何使用 explicit 构造函数来避免编译器 (compiler) 进行一些我们可能不期望的隐式类型转换 (implicit conversion),从而写出更安全、更可控的代码。


2. 详细解释

想象一下,我们正在设计一个表示正方形的程序。

步骤 1: 创建 Square 类基础结构

首先,我们需要一个 Square 类 (class)。一个正方形最重要的属性是什么?是它的边长。所以,我们会有一个私有的成员变量 (member variable) m_side 来存储边长。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Square.h (头文件)
#ifndef SQUARE_H
#define SQUARE_H

class Square {
private:
double m_side; // 边长

public:
// 构造函数:用于创建 Square 对象并初始化边长
Square(double side);

// 析构函数:用于对象销毁前进行清理 (这里为空)
~Square();

// 成员函数:计算并返回正方形的面积
double get_surface() const; // const 表示此函数不会修改对象的状态
};

#endif //SQUARE_H
  • #ifndef SQUARE_H ... #define SQUARE_H ... #endif:这些是头文件保护宏 (header guards),防止同一个头文件被多次包含,导致编译错误。
  • private::这部分定义的成员变量和成员函数只能在类的内部访问。m_side 是私有的,意味着外部代码不能直接修改它,只能通过类提供的公共接口。
  • public::这部分定义的成员变量和成员函数可以从类的外部访问。
  • Square(double side);:这是构造函数。它的名字和类名相同,没有返回类型。它接收一个 double 类型的参数 (parameter) side,用于初始化正方形的边长。
  • ~Square();:这是析构函数。它的名字是类名前面加一个波浪号 ~。当一个 Square 对象 (object) 的生命周期结束时,析构函数会被自动调用。在这个简单的例子里,它什么也不做。
  • double get_surface() const;:这是一个公共成员函数,用于计算正方形的面积。const 关键字表示这个函数不会修改调用它的 Square 对象的任何成员变量。

步骤 2: 实现 Square 类的成员函数

现在我们来实现这些函数,通常在一个对应的 .cpp 文件中。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Square.cpp (源文件)
#include "Square.h" // 包含头文件

// 构造函数的实现
Square::Square(double side)
: m_side{side} { // 使用成员初始化列表初始化 m_side
// 构造函数体,这里为空,因为初始化已在初始化列表中完成
}

// 析构函数的实现
Square::~Square() {
// 清理代码,这里不需要
}

// get_surface 函数的实现
double Square::get_surface() const {
return m_side * m_side; // 面积 = 边长 * 边长
}
  • Square::Square(double side) : m_side{side}:这是构造函数的实现。冒号 : 后面的是成员初始化列表 (initializer list)。这是一种推荐的初始化成员变量的方式,它在构造函数体执行之前就完成了成员变量的初始化。这里,m_side{side} 表示用传入的 side 参数的值来初始化 m_side 成员变量。花括号 {} 是C++11引入的统一初始化语法。
  • Square::~Square() {}:析构函数体为空,因为它不需要做任何特殊的清理工作。
  • double Square::get_surface() const { return m_side * m_side; }:计算面积很简单,就是边长的平方。

步骤 3: 引入比较函数和隐式转换的问题

假设我们有一个函数,用来比较两个正方形的面积大小:

cpp
1
2
3
4
5
6
7
8
// main.cpp 或其他地方
#include <iostream>
#include "Square.h"

// 比较函数:如果 s1 的面积大于 s2,返回 true,否则返回 false
bool compare_squares(const Square& s1, const Square& s2) {
return s1.get_surface() > s2.get_surface();
}
  • const Square& s1: 参数 s1 是一个对 Square 对象的常量引用 (const reference)。使用引用可以避免复制整个对象,提高效率。const 确保函数不会修改传入的对象。

现在,在 main 函数中使用它:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.cpp
int main() {
Square sq1(10.0); // 创建一个边长为10的正方形
Square sq2(5.0); // 创建一个边长为5的正方形

if (compare_squares(sq1, sq2)) {
std::cout << "sq1 is larger than sq2" << std::endl;
} else {
std::cout << "sq1 is NOT larger than sq2" << std::endl;
}

// 问题来了!
double side_value = 7.0;
if (compare_squares(sq1, side_value)) { // 尝试将 sq1 与一个 double 值比较
std::cout << "sq1 is larger than a square of side " << side_value << std::endl;
} else {
std::cout << "sq1 is NOT larger than a square of side " << side_value << std::endl;
}

return 0;
}

看到 compare_squares(sq1, side_value) 这一行了吗?compare_squares 函数期望第二个参数是 Square 类型的,但我们传递了一个 double 类型的 side_value

奇怪的是,这段代码通常能够编译通过并且运行!为什么呢?

这就是隐式转换 (implicit conversion) 在作祟。
编译器看到 compare_squares 函数需要一个 Square 对象,而我们提供了一个 double。它会检查 Square 类是否有办法从一个 double “构造” 出一个 Square 对象。它找到了我们的构造函数 Square(double side),这个构造函数正好接受一个 double 参数。
于是,编译器会偷偷地自动地使用这个 double7.0 调用 Square(7.0) 来创建一个临时的 Square 对象,然后将这个临时对象传递给 compare_squares 函数。

这种行为有时候是我们想要的,但很多时候,它可能隐藏了逻辑错误。程序员的意图可能是比较 sq1 的面积和一个数字 7.0 (也许这个7.0代表的是另一个面积值),但实际上代码却创建了一个边长为 7.0 的新正方形并用它进行比较。这可能导致难以察觉的bug。

步骤 4: 使用 explicit 关键字阻止隐式转换

为了防止这种不期望的隐式转换,我们可以在构造函数声明前加上 explicit 关键字。

修改 Square.h

cpp
1
2
3
4
5
// Square.h
// ... 其他代码 ...
public:
explicit Square(double side); // 在这里添加了 explicit
// ... 其他代码 ...

explicit 告诉编译器:“嘿,这个构造函数不能用于隐式转换。如果想用它,必须明确地、显式地调用它。”

加上 explicit 后,之前的 compare_squares(sq1, side_value) 这行代码就会导致编译错误!编译器会抱怨说,它无法将 double 转换为 Square

例如,错误信息可能类似于:
error: could not convert 'side_value' from 'double' to 'Square'
或者
error: no matching function for call to 'compare_squares(Square&, double&)'
note: candidate: 'bool compare_squares(const Square&, const Square&)'
note: no known conversion for argument 2 from 'double' to 'const Square&'

这正是我们想要的!编译器现在帮助我们捕获了一个潜在的逻辑错误。

如果我们确实想用 side_value 创建一个 Square 对象并进行比较,我们必须显式地这样做:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
// main.cpp (修改后)
// ...
double side_value = 7.0;
// 错误:compare_squares(sq1, side_value); // 现在这行会编译失败

// 正确的方式:显式创建 Square 对象
if (compare_squares(sq1, Square(side_value))) {
std::cout << "sq1 is larger than a square of side " << side_value << std::endl;
} else {
std::cout << "sq1 is NOT larger than a square of side " << side_value << std::endl;
}
// ...

或者使用 static_cast (虽然对于构造函数调用,直接写 Square(value) 更常见和清晰):

cpp
1
// if (compare_squares(sq1, static_cast<Square>(side_value))) { ... }

步骤 5: 涉及默认参数的构造函数

explicit 关键字对于那些“看起来不像”单参数构造函数,但实际上可以被单参数调用的构造函数也同样重要。

假设我们的 Square 类构造函数接受第二个参数,比如颜色,并且这个颜色参数有默认值:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Square.h (修改)
#ifndef SQUARE_H
#define SQUARE_H

#include <string> // 需要包含 string 头文件

class Square {
private:
double m_side;
std::string m_color; // 新增颜色成员变量

public:
// 修改构造函数,增加一个带默认值的颜色参数
explicit Square(double side, const std::string& color = "black"); // 注意 explicit 还在

~Square();
double get_surface() const;
std::string get_color() const; // 获取颜色的函数
};

#endif //SQUARE_H

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Square.cpp (修改)
#include "Square.h"

// 修改构造函数实现
Square::Square(double side, const std::string& color)
: m_side{side}, m_color{color} { // 初始化两个成员
}

Square::~Square() {}

double Square::get_surface() const {
return m_side * m_side;
}

std::string Square::get_color() const {
return m_color;
}

即使构造函数现在是 Square(double side, const std::string& color = "black"),如果我们只提供一个 double 值来调用它,例如 Square my_sq(10.0);,编译器仍然会使用默认的颜色值 "black"
因此,如果这个构造函数没有 explicit 关键字,它仍然可以被用于从 doubleSquare 的隐式转换!编译器会用提供的 double 作为第一个参数,用默认值 "black" 作为第二个参数。

cpp
1
2
3
// 如果 Square 构造函数没有 explicit:
// Square s = 10.0; // 隐式转换!等同于 Square s = Square(10.0, "black");
// compare_squares(sq1, 25.0); // 隐式转换!用 25.0 和默认颜色创建临时 Square

所以,只要一个构造函数可以通过提供单个参数(其他参数都有默认值)来调用,explicit 关键字对于防止不期望的隐式转换就是有意义的。

总结一下 explicit 的核心作用:

  1. 用于构造函数前。
  2. 阻止编译器使用该构造函数进行隐式类型转换。
  3. 鼓励更清晰的代码,因为类型转换必须是显式的。
  4. 有助于防止因意外的类型转换而导致的细微错误。

通常,一个好的经验法则是:除非你有充分的理由允许构造函数进行隐式转换,否则都应该将其声明为 explicit 特别是对于接受单个参数的构造函数,或者其他参数都有默认值的构造函数。


3. 代码示例

我们将整合上面讨论的所有内容,并提供一个完整的可运行示例。

Square.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
#ifndef SQUARE_H
#define SQUARE_H

#include <string> // For std::string
#include <iostream> // For std::cout in example

class Square {
private:
double m_side;
std::string m_color;

public:
// 显式构造函数
explicit Square(double side, const std::string& color = "black");

// 析构函数 (通常默认的就够用,这里为了完整性写出来)
~Square();

// 获取面积
double get_surface() const;

// 获取颜色
std::string get_color() const;

// 打印信息,方便演示
void print_info() const;
};

// 一个独立的比较函数
bool compare_squares_by_surface(const Square& s1, const Square& s2);

#endif //SQUARE_H

Square.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
#include "Square.h" // Ensure to include the header

Square::Square(double side, const std::string& color)
: m_side{side}, m_color{color} {
std::cout << "Square(" << m_side << ", \"" << m_color << "\") constructed." << std::endl;
}

Square::~Square() {
std::cout << "Square(" << m_side << ", \"" << m_color << "\") destructed." << std::endl;
}

double Square::get_surface() const {
return m_side * m_side;
}

std::string Square::get_color() const {
return m_color;
}

void Square::print_info() const {
std::cout << "Square Info: Side = " << m_side
<< ", Color = " << m_color
<< ", Surface = " << get_surface() << std::endl;
}

// 比较函数的实现
bool compare_squares_by_surface(const Square& s1, const Square& s2) {
std::cout << "Comparing s1 (surface " << s1.get_surface()
<< ") and s2 (surface " << s2.get_surface() << ")" << std::endl;
return s1.get_surface() > s2.get_surface();
}

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include "Square.h" // 包含我们定义的 Square 类

// 为了让输出 true/false 更易读
#include <iomanip> // For std::boolalpha

int main() {
std::cout << std::boolalpha; // 设置输出 bool 值为 "true" 或 "false"

Square sq1(10.0, "red"); // 边长10,红色
Square sq2(20.0); // 边长20,颜色使用默认值 "black"

sq1.print_info();
sq2.print_info();

std::cout << "Is sq1 larger than sq2? "
<< compare_squares_by_surface(sq1, sq2) << std::endl;
std::cout << std::endl;

// 尝试隐式转换 (如果 Square 构造函数没有 explicit,这会通过)
// 由于构造函数是 explicit 的,下面这行代码会导致编译错误:
// std::cout << "Is sq1 larger than 15.0 (implicitly converted)? "
// << compare_squares_by_surface(sq1, 15.0) << std::endl;
// 编译器会报错,例如:"no matching function for call to 'compare_squares_by_surface(Square&, double)'"
// 或者 "cannot convert 'double' to 'const Square&'"
// 这很好!它阻止了潜在的错误。

// 如果我们确实想比较 sq1 和一个边长为 15.0 的临时正方形,必须显式构造:
std::cout << "--- Explicitly creating a temporary Square for comparison ---" << std::endl;
std::cout << "Is sq1 larger than a Square(15.0)? "
<< compare_squares_by_surface(sq1, Square(15.0)) << std::endl;
// 注意上面一行中 Square(15.0) 的构造和析构输出
std::cout << "--- End of explicit temporary Square comparison ---" << std::endl;
std::cout << std::endl;


// 另一种显式创建的方式
Square sq_temp(15.0, "blue");
std::cout << "Is sq1 larger than sq_temp (15.0, blue)? "
<< compare_squares_by_surface(sq1, sq_temp) << std::endl;
std::cout << std::endl;

std::cout << "Main function ending. Objects will be destructed in reverse order of creation." << std::endl;
return 0;
}

编译和运行指南 (假设使用 g++):

  1. 保存文件:Square.h, Square.cpp, main.cpp 到同一个目录下。
  2. 打开终端或命令行。
  3. 编译:g++ main.cpp Square.cpp -o explicit_demo -std=c++11 (或更高版本如 -std=c++17)
  4. 运行:./explicit_demo

预期输出 (大致如此,构造/析构顺序可能因临时对象稍有不同):

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Square(10, "red") constructed.
Square(20, "black") constructed.
Square Info: Side = 10, Color = red, Surface = 100
Square Info: Side = 20, Color = black, Surface = 400
Comparing s1 (surface 100) and s2 (surface 400)
Is sq1 larger than sq2? false

--- Explicitly creating a temporary Square for comparison ---
Square(15, "black") constructed.
Comparing s1 (surface 100) and s2 (surface 225)
Is sq1 larger than a Square(15.0)? false
Square(15, "black") destructed.
--- End of explicit temporary Square comparison ---

Square(15, "blue") constructed.
Comparing s1 (surface 100) and s2 (surface 225)
Is sq1 larger than sq_temp (15.0, blue)? false

Main function ending. Objects will be destructed in reverse order of creation.
Square(15, "blue") destructed.
Square(20, "black") destructed.
Square(10, "red") destructed.

如果你把 Square.h 中的 explicit Square(double side, const std::string& color = "black"); 改为 Square(double side, const std::string& color = "black"); (去掉 explicit),然后取消 main.cpp 中那段被注释掉的隐式转换代码的注释,你会发现程序可以编译通过,并且会有一个边长为 15.0、颜色为 "black" 的临时 Square 对象被创建用于比较。


4. QA 闪卡 (QA Flash Cards)

Q1: 什么是构造函数中的隐式转换 (implicit conversion)?
A1: 当一个构造函数可以用单个参数调用时(或者其他参数都有默认值),编译器可能会自动使用这个构造函数将该参数类型的值转换为类类型的对象,而无需程序员显式请求。

Q2: explicit 关键字用在C++中的什么地方?有什么作用?
A2: explicit 关键字用在类的构造函数声明之前。它的作用是告诉编译器,这个构造函数不能被用于隐式类型转换。

Q3: 为什么我们要阻止隐式转换?
A3: 为了避免因编译器自动执行的、程序员可能未预料到的类型转换而导致的潜在逻辑错误和行为混淆,从而提高代码的清晰度、可读性和安全性。

Q4: 如果一个构造函数有多个参数,explicit 还有用吗?
A4: 有用,前提是该构造函数的第二个及以后的所有参数都有默认值。这样,它仍然可以用单个参数调用,从而可能参与隐式转换。explicit 可以阻止这种情况。例如 explicit MyClass(int a, bool b = true, std::string s = "");

Q5: 使用 explicit 后,我还能创建对象吗?
A5: 当然!你仍然可以通过显式调用构造函数来创建对象,例如 Square s(10.0); 或者 Square s = Square(10.0); (注意,对于 explicit 构造函数,Square s = 10.0; 是不允许的,但 Square s = Square(10.0); 是显式调用构造函数然后进行拷贝/移动初始化,这是允许的)。


5. 常见误解或易犯错误

  1. 误解:explicit 会禁止所有类型的转换。

    • 纠正: explicit 只禁止隐式转换。你仍然可以进行显式转换,例如 Square(10.0)static_cast<Square>(10.0) (尽管后者对于构造函数不常用)。
  2. 忘记对“实际上”的单参数构造函数使用 explicit

    • 易犯错误: 构造函数如 MyClass(int mandatory_param, std::string optional_param = "default") 如果没有 explicit,依然可以由一个 int 值隐式转换而来,因为 optional_param 有默认值。很多初学者可能只关注严格只有一个参数的构造函数。
  3. 不理解隐式转换的潜在危害。

    • 易犯错误: 认为隐式转换总是方便的,而忽略了它可能引入的细微的、难以调试的bug,或者使代码意图变得模糊。例如,一个期望 Angle 对象的函数,如果 Angle 有一个 explicit Angle(double degrees);,那么传递一个 double 就会报错。如果没有 explicit,它可能会默默地把 double 当作角度值创建一个 Angle,这可能不是调用者的本意。
  4. 过度使用或在不必要的地方使用 explicit

    • 说明: 虽然通常建议使用 explicit,但如果一个类的设计确实就是为了能和某种基础类型无缝、安全地进行隐式转换(例如一个简单的字符串包装类可能希望从 const char* 隐式构造),那么可以不使用 explicit。但这需要仔细考虑。对于大多数情况,特别是当转换可能不直观或有歧义时,explicit 是更安全的选择。
  5. 混淆 explicit 构造函数和拷贝构造函数/赋值运算符。

    • 说明: explicit 主要用于控制从其他类型到类类型的转换型构造函数。它通常不用于(也不应该用于)拷贝构造函数或移动构造函数,尽管在C++11之后理论上可以,但这样做会阻止很多自然的拷贝和移动场景,通常不是期望的行为。
  6. 认为 Square s = 10.0;Square s(10.0); 在有 explicit 构造函数时行为一致。

    • 纠正:
      • Square s(10.0); 是直接初始化 (direct initialization),总是调用构造函数,即使是 explicit 的也可以。
      • Square s = 10.0; 是拷贝初始化 (copy initialization)。它首先尝试将 10.0 隐式转换为 Square 类型(创建一个临时对象),然后用这个临时对象拷贝(或移动)初始化 s。如果构造函数是 explicit,第一步的隐式转换就会失败。
      • 但是,Square s = Square(10.0); 也是拷贝初始化,但因为 Square(10.0) 是显式构造,所以这是允许的(尽管现代编译器通常会优化掉这个拷贝,直接在 s 的位置构造)。

6. 编码练习

下面是一个简单的 Distance 类,它有一个接受 double 类型(表示米)的构造函数。当前,它可以从 double 隐式转换。

你的任务是:修改这个类,使得它不能从 double 值隐式转换,但仍然可以通过显式调用构造函数来创建 Distance 对象。

Distance.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
#ifndef DISTANCE_H
#define DISTANCE_H

#include <iostream>

class Distance {
private:
double m_meters;

public:
// TODO: 在下一行合适的位置添加一个关键字,以防止隐式转换
Distance(double meters) : m_meters(meters) {
std::cout << "Distance of " << m_meters << "m created." << std::endl;
}

void print_distance() const {
std::cout << "Distance is: " << m_meters << " meters." << std::endl;
}

// 仅为演示隐式转换的函数
static void display_length_implicitly(Distance d) {
std::cout << "Implicitly displayed: ";
d.print_distance();
}

static void display_length_explicitly(const Distance& d) {
std::cout << "Explicitly displayed: ";
d.print_distance();
}
};

#endif // DISTANCE_H

main.cpp (在你修改 Distance.h 后,这个 main 函数中的某一行应该会导致编译错误)

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
#include "Distance.h"

int main() {
Distance d1(100.5); // 显式构造,总是可以的
d1.print_distance();

Distance d2 = 200.75; // 拷贝初始化,目前会发生隐式转换
d2.print_distance();

std::cout << "--- Trying implicit conversion in function call ---" << std::endl;
// 下面这行调用依赖于从 double到 Distance 的隐式转换
// 在你修改 Distance.h 之后,这行应该会产生编译错误
Distance::display_length_implicitly(300.0);
std::cout << "--- End of implicit conversion test ---" << std::endl;


std::cout << "--- Trying explicit conversion in function call ---" << std::endl;
// 这种方式总是可以的,因为我们显式创建了 Distance 对象
Distance::display_length_explicitly(Distance(400.25));
std::cout << "--- End of explicit conversion test ---" << std::endl;

// 如果你想让 display_length_implicitly 在修改后也能工作,你需要这样做:
// Distance::display_length_implicitly(Distance(300.0));

return 0;
}

请你思考一下,应该在 Distance.h// TODO: 下一行添加哪个关键字?然后尝试编译并观察结果。


等你准备好后,请告诉我你的答案(你添加的关键字),或者如果你有任何疑问,随时提出!我会在这里指导你。


评论