好的,我们来深入解析 explicit 这个虽小但非常重要的关键字。
你的理解完全正确!explicit 关键字就像一个“门卫”,它守在你的构造函数门口,防止一些“不请自来”的隐式类型转换,从而让代码的行为更加清晰、可控。
一、问题根源:什么是构造函数的“隐式转换”?
在 C++ 中,编译器有时候会“自作主张”地帮你进行类型转换,让代码能够通过编译。如果一个类的构造函数可以只用一个参数来调用,那么这个构造函数就可能被编译器用来进行隐式转换 (Implicit Conversion)。
让我们来看一个没有 explicit 的“事故现场”:
场景设定:我们有一个字符串类 MyString,它有一个构造函数,可以接受一个整数 n 来创建一个由 n 个 'A' 组成的字符串。
| |
现在,在 main 函数中,我们可能会写出这样的代码:
| |
程序输出:
--- 显式调用 ---
MyString(10) 被调用了!
打印: AAAAAAAAAA
--- 隐式转换发生在这里 ---
MyString(10) 被调用了!
打印: AAAAAAAAAA
--- 赋值时的隐式转换 ---
MyString(20) 被调用了!分析:
- 第 1 种情况
MyString s1(10);是我们期望的,清晰明了。 - 第 2 种情况
printString(10);,我们明明传递的是int,但代码居然通过了!因为编译器看到printString需要一个MyString对象,而你给了一个int。它很“聪明”地发现MyString有一个MyString(int)构造函数,于是它就偷偷地用这个构造函数创建了一个临时的MyString对象,然后把这个临时对象传递给了函数。 - 第 3 种情况
MyString s2 = 20;看起来像赋值,但实际上也发生了一次隐式转换,等价于MyString s2(20);。
为什么这是个问题?
这种“自作主张”的转换有时候会让代码变得难以理解,甚至引发难以察觉的 bug。一个读代码的人看到 printString(10);,可能完全意识不到这里发生了一次成本不低的、临时的对象构造和析构。代码的意图变得模糊了。我们希望对象的创建是有意的、明确的。
二、解决方案:使用 explicit 关键字
explicit 的含义是“明确的”、“显式的”。把它放在构造函数前面,就等于在告诉编译器:
“嘿,这个构造函数只能用于显式的对象创建,不许用它来进行任何形式的隐式转换!”
让我们给刚才的构造函数加上 explicit:
| |
现在,我们再次尝试编译之前的 main 函数:
| |
结果:
加上 explicit 后,所有可能产生歧义的、隐式的转换都被编译器禁止了,它会直接报错。这迫使程序员必须明确地写出 MyString(10),让代码的意图变得一清二楚。
总结与黄金法则
explicit的作用: 修饰单参数构造函数(或所有参数都有默认值的多参数构造函数),禁止其被用于隐式类型转换。为什么要用它?
- 防止意外:避免因编译器“自作聪明”而导致的非预期对象构造。
- 提高代码清晰度:让每一次对象创建都成为一次明确、有意的行为。
- 减少潜在 Bug:避免了因隐式转换带来的性能损耗或逻辑错误。
黄金法则: 在 C++ 中,一个好的编程习惯是:给所有可以被单个参数调用的构造函数都加上
explicit关键字,除非你真的有明确的、充分的理由希望它能进行隐式转换(比如,std::string可以从const char*隐式构造,这是为了方便)。(自 C++11 起,
explicit也可以用于类型转换运算符,以阻止对象被隐式转换为其他类型,原理类似。)