在 C/C++ 中,不同的数据类型之间可以相互转换。无需用户指明如何转换的称为自动类型转换(隐式类型转换),需要用户显式地指明如何转换的称为强制类型转换。
不管是自动类型转换还是强制类型转换,前提必须是编译器知道如何转换,例如,将小数转换为整数会抹掉小数点后面的数字(由宽变窄),将 int * 转换为 float * 只是简单地复制指针的值,这些规则都是编译器内置的,我们并没有告诉编译器。
C++ 允许我们自定义类型转换规则,用户可以将其它类型转换为当前类类型,也可以将当前类类型转换为其它类型。这种自定义的类型转换规则只能以类的成员函数的形式出现,换句话说,这种转换规则只适用于自定义类。
转换构造函数:
将其它类型转换为当前类类型需要借助转换构造函数(Conversion constructor)。转换构造函数也是一种构造函数,它遵循构造函数的一般规则。转换构造函数只有一个参数。
#include
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<
运行结果:
10 + 20i
25.5 + 0i
Complex(double real) 就是转换构造函数,它的作用是将 double 类型的参数 real 转换成 Complex 类的对象,并将 real 作为复数的实部,将 0 作为复数的虚部。这样一来,a = 25.5 整体上的效果相当于:a.Complex(25.5); 将赋值的过程转换成了函数调用的过程。
在进行数学运算、赋值、拷贝等操作时,如果遇到类型不兼容、需要将 double 类型转换为 Complex 类型时,编译器会检索当前的类是否定义了转换构造函数,如果没有定义的话就转换失败,如果定义了的话就调用转换构造函数。
转换构造函数也是构造函数的一种,它除了可以用来将其它类型转换为当前类类型,还可以用来初始化对象,这是构造函数本来的意义。
需要注意的是,为了获得目标类型,编译器会“不择手段”,会综合使用内置的转换规则和用户自定义的转换规则,并且会进行多级类型转换,例如:
int main(){
Complex c1 = 100; // int --> double --> Complex
cout< int --> double --> Complex
cout< int --> double --> Complex
cout<
运行结果:
100 + 0i
65 + 0i
1 + 0i
113.8 + 0.7i
可以利用构造函数的默认参数实现构造函数个数精简。
#include
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 个实参。转换构造函数就是包含了一个参数的构造函数,恰好能够和其他两个普通的构造函数“融合”在一起。
类型转换函数:
转换构造函数能够将其它类型转换为当前类类型(例如将 double 类型转换为 Complex 类型),但是不能反过来将当前类类型转换为其它类型(例如将 Complex 类型转换为 double 类型)。
C++ 提供了类型转换函数(Type conversion function)来解决这个问题。类型转换函数的作用就是将当前类类型转换为其它类型,它只能以成员函数的形式出现,也就是只能出现在类中。
类型转换函数的语法格式为:
operator 目标类型 type() {
return 目标类型的数据 data;
}
operator 是 C++ 关键字,type 是要转换的目标类型,data 是要返回的 type 类型的数据。
因为要转换的目标类型是 type,所以返回值 data 也必须是 type 类型。既然已经知道了要返回 type 类型的数据,所以没有必要再像普通函数一样明确地给出返回值类型。这样做导致的结果是:类型转换函数看起来没有返回值类型,其实是隐式地指明了返回值类型。
类型转换函数也没有参数,因为要将当前类的对象转换为其它类型,所以参数不言而喻。实际上编译器会把当前对象的地址赋值给 this 指针,这样在函数体内就可以操作当前对象了。
#include
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);
friend Complex operator+(const Complex &c1, const Complex &c2);
operator double() const { return m_real; } //类型转换函数
private:
double m_real; //实部
double m_imag; //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
out << c.m_real <<" + "<< c.m_imag <<"i";;
return out;
}
//重载+运算符
Complex operator+(const Complex &c1, const Complex &c2){
Complex c;
c.m_real = c1.m_real + c2.m_real;
c.m_imag = c1.m_imag + c2.m_imag;
return c;
}
int main(){
Complex c1(24.6, 100);
double f = c1; // 相当于 double f = Complex::operator double(&c1);
cout<<"f = "<
运行结果:
f = 24.6
f = 43.1
n = 43
本例中,类型转换函数非常简单,就是返回成员变量 m_real 的值,所以建议写成 inline 的形式。
类型转换函数和运算符的重载非常相似,都使用 operator 关键字,因此也把类型转换函数称为类型转换运算符。
类型转换函数特别说明:
operator double() const { return m_real; } // 转换为double类型
operator int() const { return (int)m_real; } // 转换为int类型
那么下面的写法就会引发二义性:
Complex c1(24.6, 100);
float f = 12.5 + c1;
编译器可以调用 operator double() 将 c1 转换为 double 类型,也可以调用 operator int() 将 c1 转换为 int 类型,这两种类型都可以跟 12.5 进行加法运算,并且从 Complex 转换为 double 与从 Complex 转化为 int 是平级的,没有谁的优先级更高,所以这个时候编译器就不知道该调用哪个函数了,干脆抛出一个二义性错误,让用户解决。
explicit 关键字:
首先,C++ 中的 explicit 关键字只能用于修饰只有一个参数的类构造函数(转换构造函数),它的作用是表明该构造函数是显式的,而非隐式的,跟它相对应的另一个关键字是 implicit,意思是隐式的,类构造函数默认情况下即声明为 implicit(隐式)。那么显式声明的构造函数和隐式声明的有什么区别呢?来看下面的例子:
class CxString {
public:
char *_pstr;
int _size;
CxString(int size) { // 没有使用 explicit 关键字, 即默认为隐式声明
_size = size; // string 的预设大小
_pstr = (char*)malloc(size + 1); // 分配 string 的内存
memset(_pstr, 0, size + 1);
}
CxString(const char *p) {
int size = strlen(p);
_pstr = (char*)malloc(size + 1); // 分配 string 的内存
strcpy(_pstr, p); // 复制字符串
_size = strlen(_pstr);
}
// 析构函数这里不讨论, 省略...
};
// 下面是调用:
CxString string1(24); // 这样是 OK 的, 为 CxString 预分配24字节的大小的内存
CxString string2 = 10; // 这样是 OK 的, 为 CxString 预分配10字节的大小的内存
CxString string3; // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用
CxString string4("aaaa"); // 这样是 OK 的
CxString string5 = "bbb"; // 这样也是 OK 的, 调用的是 CxString(const char *p)
CxString string6 = 'c'; // 这样也是 OK 的, 其实调用的是 CxString(int size), 且 size 等于'c'的 ascii 码
string1 = 2; // 这样也是 OK 的, 为 CxString 预分配2字节的大小的内存
string2 = 3; // 这样也是 OK 的, 为 CxString 预分配3字节的大小的内存
上面的代码中, CxString string2 = 10;这句为什么是可以的呢?因为 C++ 把只有一个参数的构造函数当作转换构造函数来使用 -- 将对应数据类型转换为该类类型。
但是,上面的代码中的 _size 代表的是字符串内存分配的大小,那么调用的第二句 CxString string2 = 10; 和第六句 CxString string6 = 'c'; 就有了二义性,编译器会把 'char' 转换成 int 也就是 _size,并不是我们想要的结果。有什么办法阻止这种用法呢?答案就是使用 explicit 关键字。把上面的代码修改一下,如下:
class CxString {
public:
char *_pstr;
int _size;
explicit CxString(int size) { // 使用关键字 explicit 声明, 强制显式转换
_size = size;
// 代码同上, 省略...
}
CxString(const char *p) {
// 代码同上, 省略...
}
};
// 下面是调用:
CxString string1(24); // 这样是 OK 的
CxString string2 = 10; // 这样是不行的, 因为 explicit 关键字取消了隐式转换
CxString string3; // 这样是不行的, 因为没有默认构造函数
CxString string4("aaaa"); // 这样是 OK 的
CxString string5 = "bbb"; // 这样也是 OK 的, 调用的是 CxString(const char *p)
CxString string6 = 'c'; // 这样是不行的, 其实调用的是 CxString(int size), 且 _size 等于'c'的 ascii 码, 但 explicit 关键字取消了隐式转换
string1 = 2; // 这样也是不行的, 因为取消了隐式转换
string2 = 3; // 这样也是不行的, 因为取消了隐式转换
explicit 关键字的作用就是防止类构造函数的隐式自动转换(禁止隐式的调用转换构造函数,只能进行强制类型转换,即显式转换。),上面也已经说过了,explicit 关键字只对有一个参数的类构造函数有效,如果类构造函数参数大于或等于两个时,是不会产生隐式转换的,所以 explicit 关键字也就无效了。
但是,也有一个例外,就是当除了第一个参数以外的其他参数都有默认值的时候,explicit 关键字依然有效,此时,当调用构造函数时只传入一个参数,等效于只有一个参数的类构造函数,例子如下:
class CxString {
public:
int _age;
int _size;
// 使用关键字 explicit 声明
explicit CxString(int age, int size = 0) {
_age = age;
_size = size;
// 代码同上, 省略...
}
CxString(const char *p) {
// 代码同上, 省略...
}
};
// 下面是调用:
CxString string1(24); // 这样是 OK 的
CxString string2 = 10; // 这样是不行的, 因为 explicit 关键字取消了隐式转换
CxString string3; // 这样是不行的, 因为没有默认构造函数
string1 = 2; // 这样也是不行的, 因为取消了隐式转换
string2 = 3; // 这样也是不行的, 因为取消了隐式转换
string3 = string1; // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符 "=" 的重载
explicit 关键字用于禁止隐式类型转换。
C++ 隐式类型转换:
C++ 语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换(implicit conversion)。
在下面这些情况下, 编译器会自动地转换运算对象的类型:
算术转换(arithmetic conversion),其含义是把一种算术类型转换成另外一种算术类型。
算术转换的规则定义了一套类型转换的层次,其中 运算符的运算对象将转换成最宽的类型
以上是《Primer C++》中的原话。我原以为最宽的类型指的是在机器中所占比特数最多的意思,但是后面还有一句话:
例如,如果一个运算对象的类型是 long double,那么不论另外一个运算对象的类型是什么 都会转换成long double。还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将 转换成相应的浮点类型
我们知道各种不同算术类型在机器中所占的比特数:
32位 | 64位 | 是否变化 | |
---|---|---|---|
bool | 1 | ||
char | 1 | 1 | 没有变化 |
* 指针 | 4 | 8 | 变化 |
short int | 2 | 2 | 没有变化 |
int | 4 | 4 | 没有变化 |
unsigned int | 4 | 4 | 没有变化 |
float | 4 | 4 | 没有变化 |
double | 8 | 8 | 没有变化 |
long | 4 | 8 | 变化 |
unsigned long | 4 | 8 | 变化 |
long long | 8 | 8 | 没有变化 |
string | 32 | ||
void | 1 | 1 | 没有变化 |
除了 * 指针与 long 随操作系统字长变化而变化外。其它的都固定不变(32位和64相比)。
如果把宽度解释为比特数,显然当表达式中既有浮点类型也有整数类型时,不应该全部都转换成浮点类型(比如 long long 所占的比特数比 float 要多,不需要转换成浮点数)。因此这种解释是错误的。
所以这个宽度到底指什么?
整型提升:
整型提升(integral promotion)负责把小整数类型转换成较大的整数类型(同样的,这里小和大的含义也存疑,最后会总结)。在算术转换中的优先级最高。
对于 bool、char、signed char、unsigned char、short 和 unsigned short 等类型来说,只要它们所有可能的值都能存在 int 里,它们就会提升成 int 类型。否则,提升成 unsigned int 类型。就如我们所熟知的
尽管 int 被称作“大类型”,但这用数据类型的比特数也能解释得通,并不能得到一些有用的信息。
除此之外,我想“转换后的类型要能容纳原类型所有可能的值”这样一个前提可以一定程度上说明C++ 在类型转换上的思路,即尽可能少的丢失原有类型的信息。
无符号类型的运算对象
如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
像往常一样,首先执行整型提升。如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的)运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型(同样不知道小和大的含义)。
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是unsigned int和int,则int类型的运算对象转换成unsigned int类型。需要注意的是,如果int型的值恰好为负值,其结果将以"2.1.2节(第32页)"介绍的方法转换,并带来该节描述的所有"副作用"。
如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现 异常结果,这是因为 带符号数会自动地转换成无符号数。例如,在一个形如 a*b的式子中,如果a=-1,b=1,而且a和b都是int,则表达式的值显然为-1。然而,如果a是int,而b是unsigned,则结果须 视在当前机器上int所占位数而定。在我们的环境里,结果是4294967295。
剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。
例如,如果两个运算对象的类型分别是long和unsigned int,并且int和long的大小相同,则long类型的运算对象转换成unsigned int类型:如果long类型占用的空间比int更多,则unsigned int类型的运算对象转换成long类型。
(可以看到在假设中,int和long的相对大小是有变化的,所以类型的大小就更不可能指的是比特数。这里用了“占用的空间”这一说法,我们都知道数据类型在内存中占用的空间是固定的,那么这里占用的空间就应该有另一种解释。)<-这是我的错误想法,因为我后来想起来long在32位和64位机器中比特数会有变化。
结论:
思来想去,感觉数据类型的大小应该确实就是指内存中占用的空间大小。
而数据类型的宽度。给的例子里只有整型全部会转换成浮点型这样一个信息,以及尽可能少的丢失原有类型的信息这样一个思路。既然如此,真相最后就只有一个(眼镜反光),这个宽度指的是数据在屏幕上的宽度,而且整数和小数部分分开来算(我猜的)。
“当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型”整型的小数部分宽度都是0,比浮点数小,所以要转换成浮点数。
“较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long 和 unsigned long long中 最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值”也可以解释,只要整数部分的宽度更大,那必然可以容纳所有的值。
按理说靠推理(猜)是不行的,但我在搜索引擎上实在找不到谁有解释这个宽度的,要从更底层的角度理解暂时水平也不够,以后学得多了可能会填坑吧,现在暂时就当是为了方便自己理解。
隐式类型转换图: