C/C++中的数据类型转换static_cast/dynamic_cast/const_cast/reinterpret_cast

前言

C/C++属于静态语言,也就是在编译时变量的数据类型即可确定的强类型语言。当不同的数据类型在一起运算或者相互赋值的时候,就需要进行数据类型转换。不同数据类型占用的内存空间不同,而各种数据类型的转换时有规则的,一种通用的规则就是“小转大”自动进行,“大转小”需要强制执行。这里的“大”和“小”指的是数据范围。

为什么会有数据范围大小的区别呢?这就和饭店里的盘子一样,不同的菜肴通常使用不同的盘子,盘子有大有小,如果把小盘子里的菜装到大盘子里没有问题,但是把大盘子里的菜放到小盘子中就会溢出来,假设都使用大盘子就不会产生溢出的问题,但是这样会产生空间的浪费。而C/C++中不同类型的变量占用的内存空间不同与这些盘子非常相似,当范围小的变量赋值给范围大的变量时没有问题,但是反过来也会出现溢出。

数据类型自动转换

当不同类型的变量同时运算时就会发生数据类型的自动转换,以常见的 charshortintlongfloatdouble 这些类型为例,如果 charint 两个类型的变量相加时,就会把 char 先转换成 int 再进行加法运算,如果是 intdouble 类型的变量相乘就会把 int 转换成 double 再进行运算。

自动转换的行为如下图所示,方向是从左往右自动进行:

1
2
3
4
5
6
graph LR
A((char))-->C(unsigned/int)
B((short))-->C(unsigned/int)
C(unsigned/int)-->D(unsigned/long)
D(unsigned/long)-->F[double]
E(float)-->F[double]

C语言中的强制类型转换

前面说了自动转换,从这里开始聊聊强制类型转换,需要强制类型转换往往程序不那么智能了,需要人工进行干预。比如把一个int 类型的变量赋值给 char 类型的变量,或者说把两个 int 相乘时可能会得到一个很大的数,所以需要先把 int 强制转换成 double 计算防止溢出。

强制类型转换的格式为:(new_type_name) expression,其中 new_type_name 为新类型名称,expression为表达式。例如:

1
2
3

int val = 65535;
char ch = (char)val;

或者

1
2
int m = 2147483647, n = 100;
double result = (double)m * n;

无论是自动的类型转换还是强制类型转换,都只是为了本次操作或运算而进行的临时转换,转换的结果也会保存到临时的内存空间内,不会改变数据本来的类型或者具体的值。

有些强制类型转换是对原有数据的重新解释,比如:

1
2
3
4
5
void test(void* p)
{
char* buffer = (char*)p;
// ...
}

void* 类型的变量p,经过强制类型转换以后变成了char类型的指针,此后就可以把这段内存空间当成字符数组来处理了。

C++中的强制类型转换

在C++语言中新增了四个用于强制类型转换的关键字,分别是 static_castdynamic_cast, const_cast、 和 reinterpret_cast,使用语法为 xxxx_cast<new_type_name>(expression)

相比于C语言中使用小括号()来完成强制类型转换,C++中这几个关键字的引入能更清晰的表明它要完成强制类型转换的意图,容易暴露出隐藏的问题。

其实很长一段时间以来,我对于这四种强转方式区分的不是很清晰,其中 const_cast 的功能还比较容易辨别,但是另外3种经常混作一团,所以才有了这篇总结,而仔细学习后才发现,这4种强转关键字的区别就在他们的名字上,下面逐个来看一下。

static_cast

这个关键字的作用主要表现在 static 上,是一种静态的转换,在编译期就能确定的转换,可以完成C语言中的强制类型转换中的大部分工作,但需要注意的是,它不能转换掉表达式的 constvolitale 或者 __unaligned 属性。

它主要有以下几种用法:

  1. 用于基本数据类型之间的转换,如把int转换成char,把int转换成double等。

    1
    2
    3
    int val = 110119;
    char c = static_cast<char>(val);
    double d = static_cast<double>(val);
  2. 将表达式转换成void类型,并将转换后的结果丢弃

    1
    2
    int val = 110119;
    static_cast<void>(val);
  3. 可以用于void* 和其他指针类类型之间的转换,但是不能用于两个无关指针类型的直接转换

    1
    2
    3
    4
    5
    6
    7
    // 正常转换
    int *p = new int;
    void* p1 = static_cast<void*>(p);
    char* p2 = static_cast<char*>(p1);

    // 编译失败 //error: invalid static_cast from type ‘int*’ to type ‘char*’
    char* p3 = static_cast<char*>(p);
  4. 可以用于类继承结构中基类和派生类之间指针或引用的转换,向上转型安全,向下转型由于没有动态类型检查,是不安全的。

    1
    2
    3
    4
    5
    6
    struct B { };
    struct D : B { };

    D d;
    B& rb = d;
    D& rd = static_cast<D&>(rb);
  5. 如果涉及左值到右值、数组到指针或函数到指针的转换,也可以通过static_cast显式执行。

    1
    2
    3
    4
    template<typename _Tp>
    inline typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t)
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

dynamic_cast

从名字上看,这个关键字与 static_cast 的静态转换是对立的,这是一个“动态”转换函数,只能对指针和引用的进行转换,并且只用于类继承结构中基类和派生类之间指针或引用的转换,可以进行向上、向下,或者横向的转换。

相比于 static_cast 的编译时转换, dynamic_cast 的转换还会在运行时进行类型检查,转换的条件也比较苛刻,必须有继承关系的类之间才能转换,并且在基类中有虚函数才可以,有一种特殊的情况就是可以把类指针转换成 void* 类型。

关于使用中的常见问题,参考以下几种情况:

  1. 普通类型的指针无法转换

    1
    2
    3
    4
    5
    int val = 100;
    int *p = &val;

    // 编译失败 //error: cannot dynamic_cast ‘p’ (of type ‘int*’) to type ‘char*’ (target is not pointer or reference to class)
    char* pc = dynamic_cast<char*>(p);
  2. 继承结构中基类里面没有虚函数无法转换

    1
    2
    3
    4
    5
    6
    7
    8
    struct B { };
    struct D : B { };

    D d;
    B* pb = &d;

    // 编译失败 //error: cannot dynamic_cast ‘pb’ (of type ‘struct test1()::B*’) to type ‘struct test1()::D*’ (source type is not polymorphic)
    D* pd = dynamic_cast<D*>(pb)
  3. 指针或引用转换的类型不是正确的类型,如果参数类型是指针会返回目标类型空指针,如果参数类型是引用则会抛出 std::bad_cast 异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct B { virtual void test() {} };
    struct D : B { };

    B d;
    B* pb = &d;
    D* pd = dynamic_cast<D*>(pb);

    // 编译成功,但是pb指针指向的类型是 B,向下转型失败,输出结果是0,也就是空指针
    std::cout << pd << std::endl;
  4. 一个正常转换的例子,包含向上、向下、横向转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct B { virtual void test() {} };
    struct D1 : virtual B { };
    struct D2 : virtual B { };
    struct MD : D1, D2 { };


    D1* pd1 = new MD();
    std::cout << pd1 << std::endl;

    // 向上转型
    B* pb = dynamic_cast<B*>(pd1);
    std::cout << pb << std::endl;

    // 向下转型
    MD* pmd = dynamic_cast<MD*>(pd1);
    std::cout << pmd << std::endl;

    // 横向转型
    D2* pd2 = dynamic_cast<D2*>(pd1);
    std::cout << pd2 << std::endl;

    运行结果如下,在横向转换时指针发生了变化,可以看出 dynamic_cast 不是简单的数据强转,还进行了指针的偏移:

    1
    2
    3
    4
    5
    6
    albert@home-pc:/mnt/d/testconvert$ g++ cppconvert.cpp
    albert@home-pc:/mnt/d/testconvert$ ./a.out
    0x15c0c40
    0x15c0c40
    0x15c0c40
    0x15c0c48

const_cast

在C/C++中,const限定符通常被用来限定变量,用于表示该变量的值不能被修改,这种限定可以避免程序员犯一些初级错误,但同时也造成了一些不便,比如一些已有函数要求非常量指针,但是掉用这些函数的接口函数中都传递了常量指针,这时候就要对指针类型去常量化。

但需要特别注意的是 const_cast 不能去除变量的常量性,只能用来去除指向常数对象的指针或引用的常量性,且去除常量性的对象必须为指针或引用。

常量指针被转化成非常量指针,并且仍然指向原来的对象,常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象可能被转换成非常量对象。

  1. 尝试去除非指针和引用的类型的常量性会编译失败

    1
    2
    3
    4
    const int i = 6;

    // 编译错误 //
    int j = const_cast<int>(i);
  2. 企图用一个指针来修改常量:

1
2
3
4
const int val = 6;

//编译错误 //error: invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]
int* cp = &val;
  1. 修改一个指针的常量性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const int val = 6;

    std::cout << "&val=" << &val << ", val=" << val << std::endl;

    const int* cp = &val;
    int *p = const_cast<int*>(cp);
    *p = 2;

    std::cout << "&val=" << &val << ", val=" << val << std::endl;
    std::cout << "p=" << p << ", *p=" << *p << std::endl;

运行结果如下:

1
2
3
&val=0x7ffff7446bd4, val=6
&val=0x7ffff7446bd4, val=6
p=0x7ffff7446bd4, *p=2

运行之后,变量 p 指向了变量val地址,并改变了地址所指向的内存数据,但是打印 val 的值并没有发生变化,这是因为 val 作为常量在编译期使用它的地方就进行了替换,接下来再看另一种情况。

1
2
3
4
5
6
7
8
9
10
11
int init = 6;
const int val = init;

std::cout << "&val=" << &val << ", val=" << val << std::endl;

const int* cp = &val;
int *p = const_cast<int*>(cp);
*p = 2;

std::cout << "&val=" << &val << ", val=" << val << std::endl;
std::cout << "p=" << p << ", *p=" << *p << std::endl;

代码逻辑不变,只在开始的位置使用 init 这个变量来代替 6 这个常数,运行结果如下:

1
2
3
val=0x7fffe8c71fa0, val=6
&val=0x7fffe8c71fa0, val=2
p=0x7fffe8c71fa0, *p=2

运行之后 val 本身的变化也应用到了使用它的地方,这里的编译器替换已经不起作用了。

实际上,使用const_cast通常是一种无奈之举,利用const_cast去掉指针或引用的常量性并且去修改原始变量的数值,这是一种非常不好的行为,如果可以的话,尽可能在程序设计阶段就规避这种情况。

reinterpret_cast

它被用于不同类型指针或引用之间的转换,或者指针和整数之间的转换,是对比特位的简单拷贝并重新解释,因此在使用过程中需要特别谨慎,比如前面提到的一个例子,static_cast 不能将 int* 直接强转成 char*,使用reinterpret_cast就可以办到。

  1. 不同基础类型指针类型之间转换:

    1
    2
    3
    4
    5
    6
    7
    int *p = new int;

    // 编译失败 //error: invalid static_cast from type ‘int*’ to type ‘char*’
    char* p1 = static_cast<char*>(p);

    // 编译成功
    char* p2 = reinterpret_cast<char*>(p1);
  2. 基础类型指针与类对象指针之间的转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct B { int val;};

    B b{100};

    std::cout << "b.val=" << b.val << std::endl;

    int* p = reinterpret_cast<int*>(&b);

    std::cout << "*p=" << *p << std::endl;

运行之后可以得到 *p 的值为100,也就是重新解释了变量 b 的地址为整型指针。

  1. 将地址值转换成整数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct B { int val;};

    B b{101};

    std::cout << "&b=" << &b << std::endl;

    long addr = reinterpret_cast<long>(&b);

    std::cout << "addr=" << addr << std::endl;

运行结果如下:

1
2
&b=0x7ffffdc4f270
addr=140737450930800

这里的地址 0x7ffffdc4f270 被解释成了整数 140737450930800,因为涉及到字节序,这也是很多文章提到的 reinterpret_cast 不具备一致性的问题,我们需要知道这一个点,只要代码不依赖主机字节序就没有问题。

强转关键字的选择

好几个关键字,并且有些功能还是重复的,那么究竟该选哪一个呢?这个真得按照经验来选,我建议使用排除法,按照 const_cast -> dynamic_cast -> reinterpret_cast -> static_cast 的顺序带入选择。

  1. 先看是不是要去掉指针或引用的常量属性,如果是只能选择 const_cast

  2. 再看转换的是不是继承体系下的多态结构,如果是这种结构下的指针和引用的转换最好使用 dynamic_cast

  3. 接着看是不是偏底层的代码,需要将无关类型指针进行转换,或者指针与整数之间进行转换,如果是则选择 reinterpret_cast

  4. 前三种情况都不满足,那就只能使用 static_cast

总结

  • C/C++中不同数据类型进行运算或者赋值的时候会发生数据转换,这种转换有些是自动进行的,有些需要进行显示的强制类型转换
  • 在C语言中强制类型转换写成(new_type_name) expression的形式,new_type_name 是要转换的目标类型,expression 是待转换的表达式
  • 在C++中强制类型转换通过更明显的关键字来完成,分别是static_castdynamic_cast, const_cast、 和 reinterpret_cast
  • static_cast 是静态转换,在编译期完成完成转换,与C语言中的强制类型转换重合度最高
  • dynamic_cast 是动态转换,在运行时转换会进行检查,必须用在有继承关系的多态结构中
  • const_cast 是常量转换,用于取出指针或引用的常量属性,但是尽量通过设计杜绝它的使用场景
  • reinterpret_cast 是一种内存数据的重新解释,比较原始,开发者使用它的时候应该明确的知道自己在做什么

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

今夜的雨,好美~

2021-7-12 00:18:13

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客