转换构造函数
由来
在C/C++中,不同的数据类型之间可以相互转换:
- 无需用户指明如何转换的转换叫做自动类型转换(隐式类型转换)
- 需要用户显示的指明如何转换的称为强制类型转换
(1)自动类型转换示例:
int a = 6;
a = 7.5 + a;
编译器对 7.5 是作为 double 类型处理的,在求解表达式时,先将 a 转换为 double 类型,然后与 7.5 相加,得到和为 13.5。在向整型变量 a 赋值时,将 13.5 转换为整数 13,然后赋给 a。整个过程中,我们并没有告诉编译器如何去做,编译器使用内置的规则完成数据类型的转换。
(2)强制类型转换示例:
int n = 100;
int *p1 = &n;
float *p2 = (float*)p1;
p1是int*类型,它指向的内存里面保存的是整数,p2是float*
类型,将p1赋值给p2后,p2也指向了这块内存,并把这块内存中的数据作为小数处理。我们知道,整数和小数的存储格式大相径庭,将整数作为小数处理非常荒诞,可能会引发莫名其妙的错误,所以编译器默认不允许将p1赋值给p2。但是,使用强制类型转换后,编译器就任务我们知道这种风险的存在,并进行了适当的权衡,所以还是允许了这种行为。
不管是自动类型转换还是强制类型转换,前提必须是编译器知道如何转换。比如,将小数转换为整数会抹掉小数点后面的数字,将int *
转换为float*
只是简单的复制指针的值,这些规则都是编译器内置的,我们并没有告诉编译器。
换句话说,如果编译器不知道转换规则就不能转换,使用强制转换也无用,请看下面例子:
#include <iostream>
using namespace std;
//复数类
class Complex{
public:
Complex(): m_real(0.0), m_imag(0.0){ }
Complex(double real, double imag): m_real(real), m_imag(imag){ }
public:
friend ostream & operator<<(ostream &out, Complex &c); //友元函数
private:
double m_real; //实部
double m_imag; //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
out << c.m_real <<" + "<< c.m_imag <<"i";;
return out;
}
int main()
{
Complex a(10.2, 20.0);
a = (Complex) 25.5;
return 0;
}
25.5 是实数,a 是复数,将 25.5 赋值给 a 后,我们期望 a 的实部变为 25.5,而虚部为 0。但是,编译器并不知道这个转换规则,这超出了编译器的处理能力,所以转换失败,即使加上强制类型转换也无用。
幸运的是,C++允许我们自定义类型转换规则,用户可以将其他类型转换为当前类类型,也可以将当前类类型转换为其他类型。这种自定义的类型转换规则只能以类的成员函数的形式出现,换句话说,这种转换规则只适用于类
- 将其它类型转换为当前类类型需要借助转换构造函数
- 将当前类类型转换为其他类型需要借助类型转换函数
定义
-
不以说明符 explicit 声明
且可以单个参数调用 (C++11 前)的构造函数被称为转换构造函数(converting constructor)。- C++11之后,转换构造函数可以有多个参数或者0个参数
- 无explicit 说明符的构造函数就是转换构造函数。
-
可以将其他类型转换为当前类类型的就是转换构造函数。(无explicit 说明符的构造函数就是转换构造函数)
转换构造函数也是一种构造函数,它遵循构造函数的一般规则。
仍然以 Complex 类为例,我们为它添加转换构造函数:
#include <iostream>
using namespace std;
//复数类
class Complex{
public:
Complex(): m_real(0.0), m_imag(0.0){ }
Complex(double real, double imag): m_real(real), m_imag(imag){ }
Complex(double real): m_real(real), m_imag(0.0){ } //转换构造函数
public:
friend ostream & operator<<(ostream &out, Complex &c); //友元函数
private:
double m_real; //实部
double m_imag; //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
out << c.m_real <<" + "<< c.m_imag <<"i";;
return out;
}
int main(){
Complex a(10.0, 20.0);
cout<<a<<endl;
a = 25.5; //调用转换构造函数
cout<<a<<endl;
return 0;
}
Complex(double real);就是转换构造函数,它的作用是将 double 类型的参数 real 转换成 Complex 类的对象,并将 real 作为复数的实部,将 0 作为复数的虚部。这样一来,a = 25.5;整体上的效果相当于:
a.Complex(25.5);
将赋值的过程转换成了函数调用的过程。
在进行数学运算、赋值、拷贝等操作时,如果遇到类型不兼容,需要将double类型转换为Complex类型时,编译器会检索当前的类是否定义了转换构造函数,如果没有定义的话就转换失败,如果定义了就调用转换构造函数
转换构造函数也是构造函数的一种,它除了可以用来将其他类型转换为当前类类型,还可以用于初始化对象,这也是构造函数本来的意义。下面创建对象的方式是正确的:
Complex c1(26.4); //创建具名对象
Complex c2 = 240.3; //以拷贝的方式初始化对象
Complex(15.9); //创建匿名对象
c1 = Complex(46.9); //创建一个匿名对象并将它赋值给 c1
在以拷贝的方式初始化对象时,编译器先调用转换构造函数,将 240.3 转换为 Complex 类型(创建一个 Complex 类的匿名对象),然后再拷贝给 c2。
如果已经对+运算符进行了重载,使之能进行两个 Complex 类对象的相加,那么下面的语句也是正确的:
Complex c1(15.6, 89.9);
Complex c2;
c2 = c1 + 29.6;
cout<<c2<<endl;
在进行加法运算符时,编译器先将 29.6 转换为 Complex 类型(创建一个 Complex 类的匿名对象)再相加。
需要注意的是,为了获得模板类型,编译器会”不折手段“,会综合使用内置的转换规则和用户自定义的转换规则,并且会进行多级转换,比如:
- 编译器会根据内置规则先将 int 转换为 double,再根据用户自定义规则将 double 转换为 Complex(int --> double --> Complex);
- 编译器会根据内置规则先将 char 转换为 int,再将 int 转换为 double,最后根据用户自定义规则将 double 转换为 Complex(char --> int --> double --> Complex)。
从本例看,只要一个类型能转换为 double 类型,就能转换为 Complex 类型。请看下面的例子:
int main(){
Complex c1 = 100; //int --> double --> Complex
cout<<c1<<endl;
c1 = 'A'; //char --> int --> double --> Complex
cout<<c1<<endl;
c1 = true; //bool --> int --> double --> Complex
cout<<c1<<endl;
Complex c2(25.8, 0.7);
//假设已经重载了+运算符
c1 = c2 + 'H' + true + 15; //将char、bool、int都转换为Complex类型再运算
cout<<c1<<endl;
return 0;
}
explicit 与转换构造函数
转换构造函数可以将其他类型转换为本类类型(也叫做隐式类型转换),这个类型是由编译器自动完成的,无需程序设计人员显示转换类型。
隐式类型转换给程序设计人员带来了一定的便利,但是隐式类型转换可能会给我们设计的程序带来一些难以觉察的细微错误。有时候我们为了避免这种错误,我们希望直接强制关闭掉这种隐式类型转换,在C++中,通过关键字explicit可以实现该功能。
- explicit修饰构造器, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。
- 指定构造函数或转换函数 (C++11 起)或推导指引 (C++17 起)为显式,即它不能用于隐式转换和复制初始化。
#include <iostream>
using namespace std;
class Test1
{
public :
Test1(int num):n(num){}
private:
int n;
};
class Test2
{
public :
explicit Test2(int num):n(num){} // explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。
private:
int n;
};
int main()
{
Test1 t1 = 12;
Test2 t2(13);
Test2 t3 = 14;
return 0;
}
struct A
{
A() { } // 转换构造函数 (C++11 起) ,也是无参构造函数
A(int) { } // 转换构造函数
A(int, int) { } // 转换构造函数 (C++11 起)
};
struct B
{
explicit B() { }
explicit B(int) { }
explicit B(int, int) { }
};
int main()
{
A a1 = 1; // OK:复制初始化选择 A::A(int)
A a2(2); // OK:直接初始化选择 A::A(int)
A a3{4, 5}; // OK:直接列表初始化选择 A::A(int, int)
A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int)
A a5 = (A)1; // OK:显式转型进行 static_cast,为直接初始化
// B b1 = 1; // 错误:复制初始化不考虑 B::B(int)
B b2(2); // OK:直接初始化选择 B::B(int)
B b3{4, 5}; // OK:直接列表初始化选择 B::B(int, int)
// B b4 = {4, 5}; // 错误:复制列表初始化选择了 explicit 构造函数 B::B(int, int)
B b5 = (B)1; // OK:显式转型进行 static_cast,为直接初始化
B b6; // OK:默认初始化
B b7{}; // OK:直接列表初始化
// B b8 = {}; // 错误:复制列表初始化选择了 explicit 构造函数 B::B()
}
再谈构造函数
构造函数的本意是在创建对象的时候初始化对象,编译器会根据传递的实参来匹配不同的构造函数:
- 默认构造函数:就是无参构造函数,比如:
Complex(); //没有参数
- 普通构造函数:用户自定义的构造函数,比如:
Complex(double real, double imag); //两个参数
- 拷贝构造函数:以拷贝的方式初始化对象的调用,比如:
Complex(const Complex &c);
- 转换构造函数:将其他类型转换为当前类类型调用,比如:
Complex(double real);
不管是哪一种构造函数,都能够用来初始化对象,这是构造函数的本意。假设 Complex 类定义了以上所有的构造函数,那么下面创建对象的方式都是正确的:
Complex c1(); //调用Complex()
Complex c2(10, 20); //调用Complex(double real, double imag)
Complex c3(c2); //调用Complex(const Complex &c)
Complex c4(25.7); //调用Complex(double real)
这些代码都体现了构造函数的本意,其他情况下也会调用构造函数。比如,以拷贝的方式初始化对象时会调用拷贝构造函数,将其他类型转换为当前类类型的时候会调用转换构造函数。这些在其他情况下调用的构造函数,就成了特殊的构造函数。特殊的构造函数不一定能体现出构造函数的本意
对 Complex 类的进一步精简
上面的 Complex 类中我们定义了三个构造函数,其中包括两个普通的构造函数和一个转换构造函数。其实,借助函数的默认参数(也就是定义函数时可以给形参指定一个默认的值),我们可以将这三个构造函数简化为一个,请看下面的代码:
#include <iostream>
using namespace std;
//复数类
class Complex{
public:
Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
friend ostream & operator<<(ostream &out, Complex &c); //友元函数
private:
double m_real; //实部
double m_imag; //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
out << c.m_real <<" + "<< c.m_imag <<"i";;
return out;
}
int main(){
Complex a(10.0, 20.0); //向构造函数传递 2 个实参,不使用默认参数
Complex b(89.5); //向构造函数传递 1 个实参,使用 1 个默认参数
Complex c; //不向构造函数传递实参,使用全部默认参数
a = 25.5; //调用转换构造函数(向构造函数传递 1 个实参,使用 1 个默认参数)
return 0;
}
精简后的构造函数包含了两个默认参数,在调用它时可以省略部分或者全部实参,也就是可以向它传递 0 个、1 个、2 个实参。转换构造函数就是包含了一个参数的构造函数,恰好能够和其他两个普通的构造函数“融合”在一起
转换构造函数有什么用途
C++转换构造函数(详解版)
面向对象进阶–转换构造函数,重载,类型转换函数
c++构造函数详解(转)
类型转换函数与转换构造函数