C++ 数据类型转换详解之终极无惑

发布时间 2023-06-20 09:10:52作者: imxiangzi

程序开发环境:VS2017+Win32+Debug

文章目录
1.隐式数据类型转换
2.显示数据类型转换
3.C++ 新式类型转换
3.1 const_cast
3.2 static_cast
3.3 dynamic_cast
3.3.1 向下转换
3.3.2 交叉转换
3.4 reinterpret_cast
4. 重载相关类型转换操作符
4.1 不同类对象的相互转换
4.2 基本数据类型与类对象的相互转换
4.2.1 基本数据类型转换为类对象
4.2.2 类对象转换为基本数据类型
5.总结
参考文献
数据类型在编程中经常遇到,虽然可能存在风险,但我们却乐此不疲地进行着。

1.隐式数据类型转换
隐式数据类型转换,指不显示指明目标数据类型的转换,不需要用户干预,编译器私下进行的类型转换行为。例如:

double d = 4.48;
int i = d; // 报告警告
1
2
实际上,数据类型转换的工作相当于一条函数调用,若有一个函数专门负责从 double 转换到 int(假设函数是 dtoi),则上面的隐式转换等价于i=dtoi(d)。函数dtoi的原型应该是:int dtoi(double)或者是int dtoi(const double&)。有些类型的转换是绝对安全的,所以可以自动进行,编译器不会给出任何警告,如由int型转换成double型。另一些转换会丢失数据,编译器只会给出警告,并不算一个语法错误,如上面的例子。各种基本数据类型(不包括void)之间的转换都属于以上两种情况。

隐式数据类型转换无处不在,主要出现在以下几种情况。
(1)算术运算式中,低类型能够转换为高类型;
(2)赋值表达式中,右边表达式的值自动隐式转换为左边变量的类型,并完成赋值;
(3)函数调用传递参数时,系统隐式地将实参转换为形参的类型后,赋给形参;
(4)函数有返回值时,系统将隐式地将返回表达式类型转换为返回值类型,赋值给调用函数。

编程原则: 请尽量不要使用隐式类型转换,即使是隐式的数据类型转换是安全的,因为隐式类型数据转换降低了程序的可读性。

2.显示数据类型转换
显示数据类型转换指显示指明目标数据类型的转换,首先考察如下程序。

#include <iostream>
using namespace std;

int main(int argc,char* argv[]) {
short arr[]={65,66,67,0};
wchar_t *s;
s=arr;
wcout<<s<<endl;
}
1
2
3
4
5
6
7
8
9
由于 short int 和 wchar_t 是不同的数据类型,直接把 arr 代表的地址赋给 s 会导致一个编译错误:error C2440:“=”:无法从“short[4]”转换为“wchar_t”。

为了解决这种“跨度较大”的数据类型转换,可以使用显示的“强制类型转换”机制,把语句s=arr;改为s=(wchar_t*)arr;就能顺利通过编译,并输出:ABC。

强制类型转换在 C 语言中早已存在,到了 C++ 语言中可以继续使用。在 C 风格的强制类型转换中,目标数据类型被放在一对圆括号中,然后置于源数据类型的表达式前。在 C++ 语言中,允许将目标数据类型当做一个函数来使用,将源数据类型表达式置于一对圆括号中,这就是所谓的“函数风格”的强制类型转换。以上两种强制转换没有本质区别,只是书写形式略有不同。即:

(T)expression // C-style cast
T(expression) // function-style cast
1
2
可将它们称为旧风格的强制类型转换。在上面的程序中,可以用以下两种书写形式实现强制类型转换:

s=(wchar_t*)arr;
typedef wchar_t* WCPTR; s= WCPTR(arr);
1
2
3.C++ 新式类型转换
C++ 增加了四种内置的类型转换符:const_cast、static_cast、dynamic_cast和reinterpret_cast。它们具有统一的语法格式:

type_cast_operator<type>(expresiion)
1
3.1 const_cast
const_cast 主要用于解除常指针和常量的 const 和 volatile 属性。也就是说,把cosnt type*转换成type*类型或将const type&转换成type&类型,但是要注意,一个变量本身被定义为只读变量,那么它永远是常变量。const_cast 取消的是对间接引用时的改写限制(即只针对指针或者引用),而不能改变变量本身的 const 属性。如下面的语句是错误的。

const int i;
int j = const_cast<int>(i); //编译出错
1
2
下面通过 const_cast 取消对间接引用的修改限制。

// 示例 1
void constTest1() {
const int a=5;
int *p=NULL;
p=const_cast<int*>(&a);
(*p)++;
cout<<a<<endl; //输出5
}

// 示例 2
void constTest2() {
int i;
cout<<"please input a integer:"; //输入5
cin>>i;
const int a=i;
int& r=const_cast<int&>(a);
r++;
cout<<a<<endl; //输出6
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在函数constTest1()中输出 5,并不代表常变量 a 的值没有改变,而是编译器在代码优化时将 a 替换为字面常量5,实际上 a 的值已经变成了 6。在函数constTest2()中,由于常变量 a 的值由用户运行时输入决定,编译时无法将 a 转化为对应的字面常量,所以输出结果为修改后的值 6。

3.2 static_cast
static_cast 相当于传统的 C 语言中那些“较为合理”的强制类型转换,较多地用于基本数据类型之间的转换、基类对象指针(或引用)和派生类对象指针(或引用)之间的转换、一般的指针和void*类型的指针之间的转换等。static_cast操作对于类型转换的合理性会作出检查,对于一些过于“无理”的转换会加以拒绝。例如下面的转换:

double d = 3.14;
double* p = static_cast<double*>(d);
1
2
这是一种非常诡异的转换,在编译时会遭到拒绝。另外,对于一些看似合理的转换,也可能被 static_cast 拒绝,这时要考虑别的方法。如下面的程序。

#include <iostream>
using namespace std;

class A {
char ch;
int n;
public:
A(char c,int i):ch(c),n(i){}
};

int main(int argc,char* argv[]) {
A a('s',2);
char* p=static_cast<char*>(&a);
cout<<*p;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个程序无法通过编译,就是说,直接将A*类型转换为char*是不允许的,这时可以通过void*类型作为中介实现转换。修改后的程序如下。

void* q = &a;
char* p = static_cast<char*>(&q);
1
2
这样,程序就可以通过编译,输出 s。可见,如果指针类型之间进行转换,一定要注意转换的合理性,这一点必须由程序员自己负责。指针类型的转换意为对原数据实体内容的重新解释。

虽然 const_cast 是用来去除变量的 const 限定,但是 static_cast 却不是用来去除变量的 static 引用。其实这是很容易理解的,static 决定的是一个变量的作用域和生命周期,比如在一个文件中将变量定义为 static,则说明这个变量只能在当前文件中使用;在方法中定义一个 static 变量,该变量在程序开始时存在,直到程序结束;类中定义一个 static 成员,可以被该类的所有对象使用。对 static 限定的改变必然会造成范围性的影响,而 const 限定的只是变量或对象自身。但无论是哪一个限定,它们都是在变量一出生(完成编译的时候)就决定了变量的特性,所以实际上都是不允许改变的。这点在 const_cast 那部分就已经有体现出来。

在实践中,static_cast 多用于类类型之间的转换。这时,被转换的两种类型之间一定存在派生与继承的关系。见如下程序。

#include <iostream>
using namespace std;

class A{};
class B{};
int main(int argc,char* argv[]) {
A* pa;
B* pb;
A a;
pa=&a;
pb=static_cast<B*>(pa);
}
1
2
3
4
5
6
7
8
9
10
11
12
该程序无法通过编译,原因是类 A 与类 B 没有任何关系。综上所述,使用 static_cast 进行类型转换时要注意如下几点。

(1)static_cast 操作符的语法形式是static_cast<type>(expression),其中 expression 外面的圆括号不能省略,哪怕 expression 是一个简单的变量。

(2)通过 static_cast 只能进行一些相关类型之间的合理转换。如果是类类型之间的转换,源类型和目标类型之间必须存在继承关系,否则会得到编译错误。

(3)static_cast 所进行的是一种静态转换,是在编译时决定的。通过编译后,空间和时间效率实际上等价于 C 方式的强制类型转换。

(4)派生类对象的指针可以隐式转换为基类对象的指针。而要把基类对象的指针转换为派生类对象的指针,需要借助static_cast来完成,其转换的风险是需要程序员自己来承担。当然使用dynamic_cast更为安全。

(5)static_cast 不能转换掉 expression 的 const、volitale 和 __unaligned 属性。

3.3 dynamic_cast
dynamic_cast 是一个完全的动态操作符,只能用于指针或者引用间的转换。dynamic_cast 所操作的指针(引用)指向的对象必须拥有虚成员函数,否则出现编译错误。

原因是 dynamic_cast 牵扯到面向对象的多态性,其作用是在程序运行过程中动态地检查指针或引用指向的实际对象是什么以确定转换是否安全,而 C++ 类的多态性则依赖于类的虚函数。

具体的说,dynamic_cast 可以进行如下的类型转换。

(1)在指向基类的指针(引用)与指向派生类的指针(引用)之间进行的转换。派生类转为基类时为向上转换,被编译器视为安全的类型转换,也可以使用 static_cast 进行转换。基类转换为派生类为向下转换,被编译器视为不安全的类型转换,需要借助 dynamic_cast 的动态的类型检测。当然,static_cast 也可以完成转换,只是存在转换失败的风险。

(2)在多重继承的情况下,派生类的多个基类之间进行转换,称为交叉转换(Crosscasting)。如父类 A1 指针实际上指向的是子类,则可以将 A1 转换为子类的另一个父类 A2 指针。

3.3.1 向下转换
dynamic_cast 在向下转换时(Downcasting),即将父类指针或者引用转换为子类指针或者引用时,会严格检查指针所指的对象的实际类型。参见如下程序。

#include <iostream>
using namespace std;
class A {
public:
int i;
virtual void show(){
cout<<"class A"<<endl;
}
A(){int i=1;}
};

class B:public A {
public:
int j;
void show() {
cout<<"class B"<<endl;
}
B(){j=2;}
};

class C:public B {
public:
int k;
void show() {
cout<<"class C"<<endl;
}
C(){k=3;}
};

int main(int argc,char* argv[]) {
A* pa=NULL;
B b,*pb;
C *pc;
pa=&b;
pb=dynamic_cast<B*>(pa);
if(pb) {
pb->show();
cout<<pb->j<<endl;
} else {
cout<<"Convertion failed"<<endl;
}

pc=dynamic_cast<C*>(pa);
if(pc) {
pc->show();
cout<<pc->k<<endl;
} else {
cout<<"Convertion failed"<<endl;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
程序输出结果是:

class B
2
Convertion failed
1
2
3
由于指针 pa 所指的对象的实际类型是 class B,所以将 pa 转换为B*类型没有问题,而将 pa 转换成C*类型时则失败。当指针转换失败时,返回 NULL。

3.3.2 交叉转换
交叉转换(Crosscast)是在两个“平行”的类对象之间进行。本来它们之间没有什么关系,将其中的一种转换为另一种是不可行的。但是如果类 A 和类 B 都是某个派生类 C 的基类,而指针所指的对象本身就是一个类 C 的对象,那么该对象既可以被视为类 A 的对象,也可以被视为类 B 的对象,类型A*(A&)和B*(B&)之间的转换就成为可能。

#include <iostream>
using namespace std;

class A {
public:
int num;
A(){num=4;}
virtual void funcA(){}
};

class B {
public:
int num;
B(){num=5;}
virtual void funcB(){}
};

class C:public A,public B{};

int main(int argc,char* argv[]) {
C c;
A* pa;
B* pb;
pa=&c;
cout<<pa->num<<endl;
pb=dynamic_cast<B*>(pa);
cout<<"pa="<<pa<<endl;
if(pb) {
cout<<"pb="<<pb<<endl;
cout<<"Conversion succeeded"<<endl;
cout<<pb->num<<endl;
} else {
cout<<"Conversion failed"<<endl;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
程序输出结果是:

4
pa=003BFE8C
pb=003BFE94
Conversion succeeded
5
1
2
3
4
5
可以看出,pa 转换成 pb 之后,其值产生了变化,也就是说,在类 C 的对象中,类 A 的成员和类 B 的成员所占的位置(距离对象首地址的偏移量)是不同的。类 B 的成员要靠后一些,所以将A*转换为B*的时候,要对指针的位置进行调整。如果将程序中的dynamic_cast替换成static_cast,则程序无法通过编译,因为编译器认为类 A 和类 B 是两个“无关”的类。

3.4 reinterpret_cast
reinterpret_cast 是一种最为“狂野”的转换。它在 C++ 四中新的转换操作符中的能力最强,其转换能力不亚于 C 的强制类型转换。正是因为其强大的转换能力,应尽量避免使用 reinterpret_cast。

主要用于转换一个指针为其他类型的指针,也允许将一个指针转换为整数类型,反之亦然。这个操作符能够在非相关的类型之间进行。不过其存在必有其价值,在一些特殊的场合,在确保安全性的情况下,可以适当使用。它一般用于函数指针的转换。见如下程序。

#include <iostream>
using namespace std;

typedef void (*pfunc)();
void func1() {
cout<<"this is func1(),return void"<<endl;
}

int func2() {
cout<<"this is func2(),return int"<<endl;
return 1;
}

int main(int argc,char* argv[]) {
pfunc FuncArray[2];
FuncArray[0]=func1;
FuncArray[1]=reinterpret_cast<pfunc>(func2);
for(int i=0;i<2;++i)
{
(*FuncArray[i])();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
程序输出结果:

this is func1(),return void
this is func2(),return int
1
2
由函数指针类型int(*)()转换为void(*)(),只能通过reinterpret_cast进行,用其他的类型转换方式都会遭到编译器的拒绝。而且从程序的意图来看,这里的转换是“合理”的。不过,C++ 是一种强制类型安全的语言,即使使用 interpret_cast,也不能任意地将某种类型转换为另一种类型, C++ 编译器会设法保证“最低限度”的合理性。

语言内置的类型转换操作符无法胜任的工作需要程序员手动重载相关转换操作符来完成类型转换。

4. 重载相关类型转换操作符
在各种各样的类型转换中,用户自定义的类类型与其他数据类型间的转换要引起注意。这里要重点考察如下两种情况。

4.1 不同类对象的相互转换
由一种类对象转换成另一种类对象。这种转换无法自动进行,必须定义相关的转换函数,其实这种转换函数就是类的构造函数,或者将类类型作为类型转换操作符函数进行重载。此外,还可以利用构造函数完成类对象的相互转换,见如下程序。

#include <iostream>
using namespace std;

class Student {
char name[20];
int age;
public:
Student(){};
Student(char *s, int a) {
strcpy(name,s);
age=a;
}
friend class Team;
};

class Team {
int members;
Student monitor;
public:
Team(){};
Team(const Student& s):monitor(s),members(0){};
void Display() const {
cout<<"members' number :"<<members<<endl;
cout<<"monitor's name :"<<monitor.name<<endl;
cout<<"monitor's age :"<<monitor.age<<endl;
}
};

ostream& operator<<(ostream& out,const Team &t) {
t.Display();
return out;
}

int main(int argc,char* argv[]) {
Student s("阿珂",23);
cout<<s;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
程序输出结果:

members' number :0
monitor's name :阿珂
monitor's age :23
1
2
3
本来,输出操作符operator<<并不接受 Student 类对象作为参数,但由于可通过类 Team 的构造函数将 Student 类对象转换成 Team 类对象,所以输出操作可以成功进行。类的单参数构造函数实际上充当了类型转换函数。

4.2 基本数据类型与类对象的相互转换
4.2.1 基本数据类型转换为类对象
这种转换仍可以借助于类的构造函数进行的。也就是说,在类的若干重载的构造函数中,有一些接受一个基本数据类型作为参数,这样就可以实现从基本数据类型到类对象的转换。

4.2.2 类对象转换为基本数据类型
由于无法为基本数据类型定义构造函数,所以由对象想基本数据类型的转换必须借助于显示的转换函数。这些转换函数名由operator后跟基本数据类型名构成。下面是一个具体的例子。

#include <iostream>
using namespace std;

class A {
public:
operator int() {
return 1;
}
operator double() {
return 0.5;
}
};

int main(int argc,char* argv[]) {
A obj;
cout<<"Treating obj as an interger, its value is: "<<(int)obj<<endl;
cout<<"Treating obj as a double, its value is: "<<(double)obj<<endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
程序输出结果:

Treating obj as an interger, its value is: 1
Treating obj as a double, its value is: 0.5
1
2
在一个类中定义基本类型转换的函数,需要注意以下几点:
(1)类型转换函数只能定义为一个类的成员函数,而不能定义为外部函数。类型转换函数与普通成员函数一样,也可以在类体中声明,在类外定义;
(2)类型转换函数通常是提供给类的客户使用的,所以应将访问权限设置为public,否则无法被显示的调用,隐式的类型转换也无法完成;
(3)类型转换函数既没有参数,也不显示的给出返回类型;
(4)转换函数必须有“return目的类型数据;”的语句,即必将目的类型数据作为函数的返回值;
(5)一个类可以定义多个类型转换函数。C++编译器将根据目标数据类型选择合适的类型转换函数。在可能出现二义性的情况下,应显示地使用类型转换函数进行类型转换。

5.总结
(1)综上所述,数据类型转换相当于一次函数调用。调用的的结果是生成了一个新的数据实体,或者生成一个指向原数据实体但解释方式发生变化的指针(或引用)。

(2)编译器不给出任何警告也不报错的隐式转换总是安全的,否则必须使用显示的转换,必要时还要编写类型转换函数。

(3)使用显示的类型转换,程序猿必须对转换的安全性负责,这一点可以通过两种途径实现:一是利用C++语言提供的数据类型动态检查机制;而是利用程序的内在逻辑保证类型转换的安全性。

(4)dynamic_cast只能用于含有虚函数的类。dynamic_cast用于类层次间的向上转换和向下转换,还可以用于类间的交叉转换。在类层次间进行向上转换,即子类转换为父类,此时完成的功能和static_cast是相同的,因为编译器默认向上转换总是安全的。向下转换时,dynamic_cast具有类型检查的功能,更加安全。类间的交叉转换指的是子类的多个父类之间指针或引用的转换。

dynamic_cast能够实现运行时动态类型检查,依赖于对象的RTTI(Run-Time Type Information),通过虚函数表找到RTTI确定基类指针所指对象的真实类型,来确定能否转换。

(5)interpre_cast类似于C的强制类型转换,多用于指针(和引用)类型间的转换,权利最大,也最危险。static_cast权利较小,但大于dynamic_cast,用于普通的转换。进行类层次间的下行转换如果没有动态类型检查,是不安全的。

(6)const_cast只用于去除指针或者引用类型的const和volatile属性,变量本身的属性不能被去除。

在进行类型转换时,请坚持如下原则:
(1)子类指针(或引用)转换为父类指针(或引用)编译器认为总是是安全的,即向上转换,请使用static_cast,而非dynamic_cast,原因是static_cast效率高于dynamic_cast。

(2)父类指针(或引用)转换为子类指针(或引用)时存在风险,即向下转换,必须使用dynamic_cast进行动态类型检测。

(3)不要使用 C 风格的强制类型转换,使用 C++ 四个类型转换符 static_cast、dynamic_cast、reinterpret_cast、和const_cast。

参考文献
[1] 陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.C1.4 const_cast 的用法.P10-12
————————————————
原文链接:https://blog.csdn.net/k346k346/article/details/47750813