C++之类型转换详解

其实,要想说清楚类型转换并不是一件容易的事,类型转换可能会发生在每一个代码中,有时候是无知觉的,总的来说C++的类型转换分为隐式类型转换和显示类型转换。


C数据类型转换

隐式类型转换

隐式类型转化是编译器默默地、隐式地、偷偷地进行的类型转换,这种转换不需要程序员干预,会自动发生。

赋值转换

将一种类型的数据赋值给另外一种类型的变量时会发生隐式类型转换,例如:

1
float f = 100;

100是int类型的数据,需要先转换为float类型才能赋值给变量f,再如:

1
2
int f = 100.1;
int n = f;

f是float类型的变量,需要先转换为int类型才能赋值给变量n。
在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低;所以说,隐式类型转换并不一定是安全的。对于不安全的类型转换,编译器一般会给出警告,实验中发现GCC编译器并没有给出任何警告。

运算类型转换

C语言规定,不同类型的数据需要转换成同一类型后才可进行计算,在整型、实型和字符型数据之间通过类型转换便可以进行混合运算(但不是所有类型之间都可以进行转换) ,转换的规则如下:

  • 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算;
  • 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算;
  • char 和 short 参与运算时,必须先转换成 int 类型。

类型转换


强制类型转换

自动类型转换是编译器默默地、隐式地进行的一种类型转换,不需要在代码中体现出来;强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预。
强制类型转换的格式为:

1
(type_name) expression

例如:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
int sum = 100;
int count = 9;
double average;
average = (double)sum / count;
printf("average is %lf\n", average);
return 0;
}

运行结果:

1
average is 11.111111

注意:对于除法运算,如果除数和被除数都是整形,最后结果也是整形;()的优先级高于/
无论是隐式类型转换还是强制类型转换,都只是为了本次运算而进行的临时转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。


C++数据类型转换

隐式类型转换

C++包括C的隐式类型转换,除此之外,在C++中,将某种类型的对象拷贝到另一种不同类型的对象中时就会发生隐式转型。比如返回值(函数声明的返回值与代码块实际返回值不同的情况下),按值传递异型参数等情况均会发生隐式类型转换。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {};
class B
{
public: B (A a) {}
public: B (int c, int d = 0);
public: operator double() const;
};
A a;
B b1 = a;
B b2 = 10;
B b3;
double d;
d = 10 + b3;

此外C++也不能在转换过程中进行连续的隐式类型转换,例如A可以隐式转换为B,B可以隐式转换为C,但A不能隐式转换为C。
可以通过关键字explict可以来规避可被单参调用的构造函数引起的隐式类型转换。上例中,如果声明 explicit B(A a),b1=a就不会发生隐式类型转换,编译错误。


显示类型转换

C++语言也支持C语言的类型强制类型转换,但这种转换可以在两种不同类型的对象的指针之间进行,这很可能是相当危险的事情。所以 C++ 提供四种转换操作符来细分显式类型转换。

const_cast

  • 1、常量指针被转化成非常量的指针,并且仍然指向原来的对象;
  • 2、常量引用被转换成非常量的引用,并且仍然指向原来的对象;
  • 3、const_cast一般用于修改指针。如const char *p形式;
  • 4、const_cast也可以去除对象的valatile属性。
    例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    #include <stdio.h>
    using namespace std;
    int main() {
    int a = 5;
    const int *p = &a;
    // *p = 6; // error
    int *p2 = const_cast<int*>(p);
    *p2 = 6;
    printf("%d\n", a);
    return 0;
    }

注意:对定义为常量的参数,使用const_cast会出现未定义的行为,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int main() {
const int c_val = 233; //声明为常量类型
int &use_val = const_cast<int&>(c_val); //使用去const 引用
int *ptr_val = const_cast<int*>(&c_val);//使用去const 指针
use_val = 666; //未定义行为
cout << c_val << "\t" << use_val << "\t" << *ptr_val << endl;
*ptr_val = 110; //未定义行为
cout << c_val << "\t" << use_val << "\t" << *ptr_val << endl;
return 0;
}

gcc下输入:

1
2
233 666 666
233 110 110

c_val、use_val和ptr_val的地址是一样的,但是c_val的值并没有发生改变,这点非常令人匪夷所思。

static_cast

  • 1、static_cast 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患;
  • 2、用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。注意:进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  • 3、用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护。
  • 4、static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性。(前两种可以使用const_cast 来去除)
  • 5、c++ 的任何的隐式转换都是使用 static_cast 来实现。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* 常规的使用方法 */
    float f_pi=3.141592f
    int i_pi=static_cast<int>(f_pi); /// i_pi 的值为 3
    /* class 的上下行转换 */
    class Base{
    // something
    };
    class Sub:public Base{
    // something
    }
    // 上行 Sub -> Base
    //编译通过,安全
    Sub sub;
    Base *base_ptr = static_cast<Base*>(&sub);
    // 下行 Base -> Sub
    //编译通过,不安全
    Base base;
    Sub *sub_ptr = static_cast<Sub*>(&base);

dynamic_cast

dynamic_cast运算符是强制类型转换,应该是这四种中最特殊的一个,因为他涉及到面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关.所以不能完全使用C语言的强制转换替代,它也是最常有用的,最不可缺少的一种强制转换。

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
#include <iostream>
using namespace std;
class Parent {
public:
void print() {
cout << "Parent" << endl;
}
virtual void virFunction() {}
};
class Children : public Parent {
public:
void print() {
cout << "Children" << endl;
}
virtual void virFunction() {}
};
int main() {
Children *child = new Children;
child->print();
cout << "Children -> Parent " << endl;
Parent *parent1 = static_cast<Parent*>(child);
if (parent1 != NULL) parent1->print();
Parent *parent2 = (Parent*)child;
if (parent2 != NULL) parent2->print();
Parent *parent3 = dynamic_cast<Parent*>(child);
if (parent3 != NULL) parent3->print();
Parent *parent = new Parent;
parent->print();
cout << "Children -> Parent " << endl;
Children *child1 = static_cast<Children*>(parent);
if (child1 != NULL) child1->print();
Children *child2 = (Children*)parent;
if (child2 != NULL) child2->print();
Children *child3 = reinterpret_cast<Children*>(parent);
if (child3 != NULL) child3->print();
Children *child4 = dynamic_cast<Children*>(parent);
if (child4 != NULL) child4->print();
return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
Children
Children -> Parent
Parent
Parent
Parent
Parent
Parent -> Children
Children
Children
Children

  • 从子类到父类的转换:static_cast和dynamic_cast都可以做到
  • 从父类到子类的转换:static_cast和dynamic_cast也都是成功的,但是static_cast返回了值,而dynamic_cast返回空指针,事实上,父类转换为子类是未定义的结果,而static_cast调用了子类的函数。reinterpret也可以进行类型转换,和static_cast输出是一样的。
    结论:static_cast和reinterpret_cast运算符要么直接被编译器拒绝进行转换,要么就一定会得到相应的目标类型的值。 而dynamic_cast却会进行判别,确定源指针所指的内容,是否真的合适被目标指针接受。如果是否定的,那么dynamic_cast则会返回null。这是通过检查”运行期类型信息”(Runtime type information,RTTI)来判定的,它还受到编译器的影响,有些编译器需要设置开启才能让程序正确运行。
    其次,进行dynamic_cast转换时,必须要提供虚函数,如果上例中没有虚函数,就会发生编译错误。

reinterpret_cast

reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。
IBM的C++指南告诉我们reinterpret_cast可以用在什么地方作为转换运算符:

  • 从指针类型到一个足够大的整数类型
  • 从整数类型或者枚举类型到指针类型
  • 从一个指向函数的指针到另一个不同类型的指向函数的指针
  • 从一个指向对象的指针到另一个不同类型的指向对象的指针
  • 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
  • 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include<iostream>
    #include<stdint.h>
    using namespace std;
    int main() {
    int *ptr = new int(233);
    uint64_t ptr_addr = reinterpret_cast<uint64_t>(ptr);
    cout << "ptr 的地址: " << hex << ptr << endl
    << "ptr_addr 的值(hex): " << hex << ptr_addr << endl;
    delete ptr;
    return 0;
    }

运行结果:

1
2
ptr 的地址: 0x1dc2010
ptr_addr 的值(hex): 1dc2010

参考资料:
C语言数据类型转换
[深入理解C++(一)]类型转换
c++ 四种强制类型转换介绍
C++标准转换运算符dynamic_cast
C++标准转换运算符reinterpret_cast