在 C++ 中,explicit 是一个非常常见的关键字。很多初学者知道它的作用是:
explicit可以阻止隐式类型转换。
但如果进一步追问:
- 什么叫隐式类型转换?
explicit到底阻止了什么?- 为什么构造函数可以使用
explicit? - 类型转换函数为什么也可以使用
explicit? explicit拷贝构造函数又是怎么回事?
这些问题就很容易混在一起。
本文从最基础的概念开始,系统梳理 explicit 的常见使用场景。
一、explicit 构造函数
这是 explicit 最常见的使用场景。
先看一个没有 explicit 的例子:
cpp
class A {
public:
A(int x) {}
};
A a = 10;
这里看起来是在用 10 初始化 a,但实际上发生了一个隐式类型转换。
10 的类型是 int,而 a 的类型是 A,他们类类型不同,本不应该可以转换,但由于类 A 存在:
cpp
A(int x)
编译器发现可以利用这个构造函数把 int 转换成 A。
可以把它理解成:
cpp
A a = A(10);
也就是说:
text
int
↓
A(int)
↓
A
这个过程是由编译器自动完成的,因此叫做隐式类型转换。当然,编译器做了优化之后,就只接变成调用一次构造函数了,而不是上面说的先构造再复制
使用 explicit
如果我们不希望编译器自动进行这种转换,可以在构造函数前加上 explicit:
cpp
class A {
public:
explicit A(int x) {}
};
此时:
cpp
A a = 10; // ❌ 编译错误
因为 explicit 告诉编译器:
不要偷偷使用这个构造函数进行隐式类型转换。
但是,如果我们明确写出构造过程:
cpp
A a(10); // ✅
就是允许的。
所以:
cpp
class A {
public:
explicit A(int x) {}
};
可以理解成:
text
A a(10); // ✅ 明确告诉编译器:我要构造 A
A a = 10; // ❌ 不允许编译器偷偷把 int 转成 A
因此:
explicit构造函数并不是禁止类型转换,而是禁止编译器隐式地进行类型转换。
二、explicit 构造函数还可以阻止函数参数的隐式转换
explicit 的作用不仅体现在:
cpp
A a = 10;
还体现在函数参数中。
例如:
cpp
class A {
public:
A(int x) {}
};
void func(A a) {}
此时:
cpp
func(10);
也是合法的。
为什么?
因为函数需要一个 A 类型的参数,而我们传入的是 int 类型的 10。
编译器可以自动调用:
cpp
A(int)
于是整个过程相当于:
cpp
func(A(10));
也就是说:
text
10
↓
A(10)
↓
func()
这里同样发生了隐式类型转换。
加上 explicit
如果修改成:
cpp
class A {
public:
explicit A(int x) {}
};
void func(A a) {}
那么:
cpp
func(10); // ❌
因为 A(int) 是 explicit 构造函数,编译器不能偷偷把:
cpp
10
转换成:
cpp
A(10)
但是:
cpp
func(A(10)); // ✅
是可以的。
因为这里是我们主动构造了 A。
因此:
text
func(10)
↓
需要 A
↓
是否允许 int → A 的隐式转换?
↓
explicit
↓
不允许
而:
cpp
func(A(10));
则是:
text
明确构造 A
↓
允许
三、explicit 构造函数到底禁止什么?
假设:
cpp
class A {
public:
explicit A(int);
};
那么最重要的就是区分下面几种情况。
情况 1:直接初始化
cpp
A a(10);
允许。
因为这是我们明确告诉编译器:
请使用
10构造一个A。
情况 2:拷贝初始化
cpp
A a = 10;
不允许。
因为这种写法需要把 10 隐式转换成 A。
而 explicit 禁止了这种隐式转换。
情况 3:函数参数
cpp
void func(A a);
func(10);
不允许。
因为这里同样需要:
text
int → A
的隐式转换。
情况 4:明确构造
cpp
func(A(10));
允许。
因为我们明确告诉编译器:
text
我要构造一个 A
所以第一大类可以总结成:
explicit构造函数的主要作用,就是防止编译器偷偷使用该构造函数完成隐式类型转换。
四、explicit 类型转换函数
除了构造函数之外,C++ 中还有一种函数也可以进行类型转换:
类型转换函数(conversion function)
例如:
cpp
class A {
public:
operator int() {
return 10;
}
};
这个函数的作用是:
text
A → int
例如:
cpp
A a;
int x = a;
编译器会自动调用:
cpp
a.operator int();
因此:
text
A
↓
operator int()
↓
int
这里同样发生了隐式类型转换。
使用 explicit
如果不希望这种转换自动发生,可以写:
cpp
class A {
public:
explicit operator int() {
return 10;
}
};
此时:
cpp
A a;
int x = a; // ❌
编译器不能偷偷把 A 转换成 int。
但是:
cpp
int x = static_cast<int>(a); // ✅
可以。
因为这里我们明确告诉编译器:
我要把
a转换成int。
所以:
text
int x = a;
属于隐式转换,被 explicit 阻止。
而:
cpp
int x = static_cast<int>(a);
属于显式转换,因此可以正常进行。
五、explicit operator bool() 的特殊应用
在实际 C++ 编程中,我们经常会看到:
cpp
explicit operator bool() const;
这是 explicit 类型转换函数中非常经典的一个应用。
例如:
cpp
class A {
public:
explicit operator bool() const {
return true;
}
};
我们可以这样使用:
cpp
A a;
if (a) {
// ...
}
这是允许的。
但是:
cpp
bool x = a;
通常是不允许的。
为什么?
因为:
cpp
if (a)
这种场景本身就是在进行一个明确的条件判断。
而我们通常希望一个对象:
可以被用于
if判断,但不要让它随随便便转换成bool后参与其他隐式转换。
例如一个表示资源是否有效的类:
cpp
class MyPtr {
public:
explicit operator bool() const {
return ptr != nullptr;
}
private:
int* ptr = nullptr;
};
于是:
cpp
MyPtr p;
if (p) {
// p 有效
}
非常自然。
但是我们不希望:
cpp
int x = p;
这种奇怪的隐式转换发生。
所以:
cpp
explicit operator bool()
在很多资源管理类、容器类、智能指针类中都非常有用。
你以后看到:
cpp
explicit operator bool() const
可以直接理解成:
这个对象可以用于条件判断,但不希望它随意隐式转换成
bool。
六、拷贝构造函数可以使用 explicit 吗?
答案是:
可以。
例如:
cpp
class A {
public:
explicit A(const A& other) = default;
};
这里:
cpp
A(const A&)
仍然是一个拷贝构造函数。
explicit 并不会让它变成其他类型的函数。
那么它有什么作用?
关键在于区分:
直接初始化 和拷贝初始化。
例如:
cpp
A a;
A b(a);
这里是直接初始化。
因此可以使用 explicit 拷贝构造函数。
但是:
cpp
A c = a;
这是拷贝初始化。
此时 explicit 拷贝构造函数不能用于这种初始化。
所以:
cpp
class A {
public:
explicit A(const A&) = default;
};
情况下:
cpp
A b(a); // ✅
A c = a; // ❌
注意:explicit 并不是禁止复制
这是这里最容易产生误解的地方。
例如:
cpp
explicit A(const A&);
并不意味着:
"A 不能复制。"
它真正表达的是:
这个拷贝构造函数不能用于隐式的拷贝初始化。
所以:
cpp
A b(a);
仍然可以复制。
只是:
cpp
A c = a;
这种初始化方式不再允许使用这个 explicit 拷贝构造函数。
七、总结:explicit 到底在干什么?
到这里可以把 explicit 的作用统一起来。
它并不是:
"禁止类型转换。"
而是:
"禁止编译器自动、隐式地使用这个函数进行类型转换或初始化。"
主要有两种常见使用场景。
1. explicit 构造函数
例如:
cpp
class A {
public:
explicit A(int);
};
阻止:
cpp
A a = 10; // ❌
func(10); // ❌,如果 func 参数是 A
允许:
cpp
A a(10); // ✅
func(A(10)); // ✅
核心:
text
int → A
不允许隐式转换,允许显式构造。
2. explicit 类型转换函数
例如:
cpp
class A {
public:
explicit operator int() const;
};
阻止:
cpp
int x = a; // ❌
允许:
cpp
int x = static_cast<int>(a); // ✅
核心:
text
A → int
不允许隐式转换,允许显式转换。
3. explicit 拷贝构造函数
例如:
cpp
class A {
public:
explicit A(const A&) = default;
};
允许:
cpp
A b(a); // ✅
不允许:
cpp
A c = a; // ❌
它控制的是:
拷贝构造函数能不能参与拷贝初始化。
不过实际开发中,给普通拷贝构造函数加 explicit 并不常见。
最后记住一句话
如果以后面试或者学习 C++ 时被问:
explicit的作用是什么?
最准确的回答是:
explicit用于防止构造函数或类型转换函数被用于隐式类型转换,要求程序员明确写出构造或转换操作。
最典型的代码就是:
cpp
class A {
public:
explicit A(int x); // int → A:禁止隐式转换
explicit operator int(); // A → int:禁止隐式转换
};
所以看到 explicit,第一反应就是:
"这里作者不希望编译器偷偷帮我做类型转换。"