前言
在 C++ 中,对于一个类来说,根据接收的参数数量和类型不同,可以定义多个构造函数。如果在创建一个类的对象时,不需要输入任何实参作为初始值,此时调用的是默认构造函数。如果将同一个类的另外一个对象作为实参初始值,则调用的称为拷贝构造函数。有一种特殊的构造函数,该类构造函数只接收一个实参,他实际上不仅仅实现了一种初始化对象的方式,而且实现了一种转换为此类类型的转换机制,这种构造函数就被称为转换构造函数。在使用 C++提供的内置类型时,会接触到很多类型转换机制,其实用户自定义的类型也存在转换机制,而此篇文章就主要讨论接收一个参数的转换构造函数,以及其带来的类类型转换机制。同时还对类型转换运算符进行讨论,为我们的类自定义转换成其他类型的行为。
0x1 将其他类型转换为类类型
如下假设我们实现了一个 Person 类,该类有三个构造函数,分别是默认构造函数,接受两个参数 name 和 address 的构造函数,还有接受一个参数 name 的构造函数,对于第三个构造函数,由于其只接收一个构造函数,所以它给 Person 类带来了一种 string 到 Person 的转换机制。代码如下:
// g++环境:gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person() : name("Unknown"), address("Unknown") { cout << "default constructor\n"; };
Person(string n, string addr) : name(n), address(addr) { cout << "direct constructor 1\n"; }
Person(string n) : name(n), address("Unknown") { cout << "direct constructor 2\n"; }
private:
string name, address;
};
int main()
{
string name = "Alice";
Person Alice;
Alice = name;
return 0;
}
输出如下:
可见,main 函数中的第二行代码,通过默认构造函数初始化一个 Person 对象 Alice。而第三行将一个 string 对象赋值给一个 Person 对象 Alice,按道理来说,两个类型不同的对象是无法赋值的,而此处之所以可以赋值就是因为就发生了隐式类型转换,通过转换构造函数 direct constructor 2 ,编译器先调用该构造函数通过 string 对象 name 生成一个 Person 的临时对象,然后将该临时对象赋值给 Alice 对象。如果类设计者需要接收一个参数的构造函数,但同时又不希望向用户提供这种隐式的转换机制应该怎么办呢,仅仅需要将其构造函数声明成 explicit 即可(C++11),用户则不能通过赋值操作来将 string 对象赋值给 Person 对象,如下给转换构造函数 direct constructor 2 加上 explicit 声明:
explicit Person(string n) : name(n), address("Unknown") { cout << "direct constructor 2\n"; }
则编译的时候将报错:
g++ 显示没有合适的 operator= 函数匹配,其实就是因为没有合适的构造函数将 string 对象转换成 Person 临时对象。
注意,explicit 仅仅屏蔽掉了其隐式转换的属性,此时同样可以使用显式转换机制。
隐式转换还可以发生在函数返回的时候。
将 main 函数改成如下则可以顺利编译通过:
int main()
{
string name = "Alice";
Person Alice;
Alice = static_cast<Person>(name); // 通过static_cast将name显示转换成Person对象
return 0;
}
0x2 将类类型转换成其他类型
通过类型转换运算符可以将类类型转换到其他的类型,其一般形式:operator type() const
,其中 type表示某种类型。假如我们希望 Person 类提供这样的机制,即我们可以通过其对象就能判断其是否具有名字,此时就需要用到类型转换运算符,将第一小节的代码扩展如下:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person() : name("Unknown"), address("Unknown") { cout << "default constructor\n"; };
explicit Person(string n, string addr) : name(n), address(addr) { cout << "direct constructor 1\n"; }
Person(string n) : name(n), address("Unknown") { cout << "direct constructor 2\n"; }
/***************************************************************************/
// 扩展部分
operator bool() const
{
if (name == string("Unknown"))
return false;
return true;
}
/***************************************************************************/
private:
string name, address;
};
int main()
{
string name = "Alice";
Person Alice;
Alice = static_cast<Person>(name);
bool isAliceHasName = Alice;
cout << isAliceHasName << endl; //输出为 1
cout << (Alice << 2) << endl; //输出为 4 ,如果将转换成 bool 的类型转换运算符声明为 explicit ,则编译失败
return 0;
}
通过在 Person 类中定义个 operator bool() const
类型转换运算符即完成了该操作,在语句bool isAliceHasName = Alice;
中,就调用了成员函数 operator bool() const
发生隐式转换,因为 Alice 对象具有名字(成员变量 name 没被赋值为 “Unknown”),所以返回了 true ,输出为 1。注意该类型转换运算符有几个特点:
- 没有显示的返回类型,但是有返回值,且返回值跟 type 必须是同一个类型。
- 没有形参。
- 必须定义成类的成员函数。
- 一般都会用 const 修饰,因为其通常不应该改变待转换对象的内容。
同样的,这种隐式转换会带来很严重的问题,在上述的 Person 类中,以下代码也能通过编译,Alice << 2;
,即使我们没有重载 <<
运算符。其能通过编译的原因就是编译器在识别到该行代码时,因为 Alice 对象可以隐式转换成 bool 类型,所以编译器先将其隐式转换成 bool 类型的 true,bool 类型可以进行移位运算,故被编译器进行整型提升后向左移位 2 位,所以输出的就是 4。显然这种结果是我们及其不愿意看到的,于是在 C++11 标准中,加入了关键字 explicit,将类型转换运算符声明为 explicit ,则可以屏蔽掉隐式转换。同样,类似于第一小节中的用法,即使屏蔽了隐式转换,仍然可以通过显式转换的方式来进行类型转换。但是对于类型转换运算符来说,有一个特殊的意外,就是当其被声明为 explicit 的时候,若表达式是被用作条件,则仍然可以发生隐式转换:
- if 、while 、do 语句的条件部分
- for 语句头的条件表达式
- 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象
- 条件运算符( ? : )的条件表达式
这就给我们带来了很多方便,比如我们在用 cin 作为循环控制的时候,经常会写这种代码:while(std::cin >> value)
,cin 在执行完 >> 运算后,会返回 cin 的引用,此时 cin 这个对象本身作为了 while 语句的条件判断。我们都知道 cin 是一种流对象,为什么这种用户定义的类的对象可以直接作为 while 语句的条件判断呢,我们可以深入源代码:
可见,在 cin 对应的类中,定义了一个转换成 bool 类型的类型转换运算符,该运算符就可以通过调用 fail() 函数,来判断流在进行输入后,状态是否还是正确的,并将结果返回。同时,其被声明成 explicit 来防止被用来作为一般的隐式转换,又因为其可以作为条件判断的特殊性,则可以将 cin 对象用于上述所提及的特例中了。
参考资料: 《C++ Prime》