C++类型转换之reinterpret_cast

118 篇文章 8 订阅

C++类型转换之reinterpret_cast

C++标准转换运算符之 reinterpret_cast

===============================

C++类型转换之reinterpret_cast

C++是兼容C的,因此C语言中的强制类型转换在C++中同样适用,具体使用方法可以参照下面的代码示例:

float valueA = 3.0f;
int valueB = (int) valueA;

可以看到,C语言中强制类型转换的一般格式为:

(类型说明符)表达式

实现的功能就是把表达式的值强制转换为类型说明符表示的类型。除了这种强制类型转换方法外,C++还提供了四种类型转换方法,分别为

  • static_cast<类型说明符>(表达式)
  • dynamic_cast<类型说明符>(表达式)
  • const_cast<类型说明符>(表达式)
  • reinterpret_cast<类型说明符>(表达式)

下面在比较它们的异同时,按照适用范围从窄到宽的顺序介绍,先从使用频率比较低的reinterpret_cast开始,然后依次是const_cast,dynamic_cast,最后介绍static_cast。

reinterpret_cast

首先从英文字面的意思理解,interpret是“解释,诠释”的意思,加上前缀“re”,就是“重新诠释”的意思;

cast在这里可以翻译成“转型”(在侯捷大大翻译的《深度探索C++对象模型》、《Effective C++(第三版)》中,cast都被翻译成了转型),这样整个词顺下来就是“重新诠释的转型”。

我们知道变量在内存中是以“…0101…”二进制格式存储的,一个int型变量一般占用32个位(bit),参考下面的代码

#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
	int num = 0x00636261;//用16进制表示32位int,0x61是字符'a'的ASCII码
	int * pnum = &num;
	char * pstr = reinterpret_cast<char *>(pnum);
	cout<<"pnum指针的值: "<<pnum<<endl;
	cout<<"pstr指针的值: "<<static_cast<void *>(pstr)<<endl;//直接输出pstr会输出其指向的字符串,这里的类型转换是为了保证输出pstr的值
	cout<<"pnum指向的内容: "<<hex<<*pnum<<endl;
	cout<<"pstr指向的内容: "<<pstr<<endl;
	return 0;
}

。。。。。。省略。。。。。。

C++标准转换运算符之 reinterpret_cast

reinterpret_cast 转换

通过重新解释底层位模式在类型间转换。
语法

reinterpret_cast < 新类型 > ( 表达式 )
解释

与 static_cast 不同,但与 const_cast 类似,reinterpret_cast 表达式不会编译成任何 CPU 指令(除非在整数和指针间转换,或在指针表示依赖其类型的不明架构上)。它纯粹是一个编译时指令,指示编译器将 表达式 视为如同具有 新类型 类型一样处理。

唯有下列转换能用 reinterpret_cast 进行,但若转换会转型走常量性或易变性则亦不允许。

1) 整型、枚举、指针或成员指针类型的表达式可转换到其自身的类型。产生的值与 表达式 的相同。(C++11 起)

2) 指针能转换成大小足以保有其类型所有值的任何整型类型(例如转换成 std::uintptr_t)

3) 任何整型或枚举类型的值可转换到指针类型。指针转换到有足够大小的整数再转换回同一指针类型后,保证拥有其原值,否则结果指针无法安全地解引用(不保证相反方向的往返转换;相同指针可拥有多种整数表示)。不保证空指针常量 NULL 或整数零生成目标类型的空指针值;为此目的应该用 static_cast 或隐式转换。

4) 任何 std::nullptr_t 类型的值,包含 nullptr,可转换成任何整型类型,如同它是 (void*)0 一样,但没有值能转换成 std::nullptr_t,甚至 nullptr 也不行:为该目的应该用 static_cast。(C++11 起)

5) 任何对象指针类型 T1* 可转换成指向对象指针类型 cv T2 。这严格等价于 static_cast<cv T2*>(static_cast<cv void*>(表达式))(这意味着,若 T2 的对齐要求不比 T1 的更严格,则指针值不改变,且将结果指针转换回原类型将生成其原值)。任何情况下,只有类型别名化(type aliasing)规则允许(见下文)时,结果指针才可以安全地解引用

6) T1 类型的左值表达式可转换成到另一个类型 T2 的引用。结果是与原左值指代同一对象,但有不同类型的左值或亡值。不创建临时量,不进行复制,不调用构造函数或转换函数。只有类型别名化(type aliasing)规则允许(见下文)时,结果指针才可以安全地解引用

7) 任何函数指针可转换成指向不同函数类型的指针。通过指向不同函数类型的指针调用函数是未定义的,但将这种指针转换回指向原函数类型的指针将生成指向原函数的指针值。

8) 一些实现上(特别是在任何 POSIX 兼容的系统上,即基于 dlsym 的要求),函数指针可以转换成 void* 或任何其他对象指针,反之亦然。若实现支持双向的转换,则转换回原类型将生成原值,否则结果指针不能安全地解引用或调用。

9) 任何指针类型的空指针值可转换成任何其他指针类型,产生该类型的空指针值。注意不能用 reinterpret_cast 将空指针常量 nullptr 或任何其他 std::nullptr_t 类型的值转换为指针:为此目的应该使用隐式转换或 static_cast。

10) 成员函数指针可转换成指向不同类型的不同成员函数的指针。转换回原类型将生成原值,否则结果指针不能安全使用。

11) 指向某类 T1 的成员对象的指针可转换成指向另一个类 T2 的另一个成员对象的指针。若 T2 的对齐不比 T1 更严格,则转换回原类型 T1 将生成原值,否则不能安全地使用结果指针。

同所有转型表达式,结果是:

    左值,若 new_type 是左值引用或到函数类型的右值引用;
    亡值,若 new_type 是到对象类型的右值引用;
    否则为纯右值。

类型别名化

凡在试图通过 AliasedType 类型的泛左值读或修改类型为 DynamicType 的对象的值时,行为未定义,除非下列之一为真:

    AliasedType 与 DynamicType 相似。
    AliasedType 是 DynamicType 的(可有 cv 限定的)有符号或无符号变体。
    AliasedType 为 std::byte、 (C++17 起)char 或 unsigned char:这容许将任何对象的对象表示作为一个字节数组加以检验。

非正式地说,忽略顶层 cv 限定性,若两个类型符合下列条件,则它们相似:

    它们是同一类型;或
    它们都是指针,且被指向的类型相似;或
    它们都是指向相同类的成员指针,且被指向的成员类型相似;或

    它们都是大小相同的数组或都是未知边界数组,且数组元素类型相似。

    (C++20 前)

    它们都是数组,大小相同或至少一个是未知边界数组,且数组元素类型相似。

    (C++20 起)

例如:

    const int * volatile * 与 int * * const 相似;
    const int (* volatile S::* const)[20] 与 int (* const S::* volatile)[20] 相似;
    int (* const *)(int *) 与 int (* volatile *)(int *) 相似;
    int (S::*)() const 与 int (S::*)() 不相似;
    int (*)(int *) 与 int (*)(const int *) 不相似;
    const int (*)(int *) 与 int (*)(int *) 不相似;
    int (*)(int * const) 与 int (*)(int *) 相似(它们是同一类型);
    std::pair<int, int> 与 std::pair<const int, int> 不相似。

此规则允许进行基于类型的别名分析,即编译器假设通过一个类型的泛左值读取的值,不会被通过不同类型的泛左值的写入所修改(依据上述例外情况)。

注意,许多 C++ 编译器作为非标准语言扩展放松此规则,以允许通过 union 的不活跃成员的进行类型错误的访问(这种访问在 C 中并不是未定义的)。
注解

标准中定义严格别名化规则的段落含有两条额外条例,部分地是从 C 继承而来:

    AliasedType 为聚合类型或 union 类型,它保有前述各类型之一作为其元素或非静态成员(递归地包含子聚合体的元素和被包含的联合体的非静态数据成员)。
    AliasedType 为 DynamicType 的(可有 cv 限定的)基类。

这些条例所描述的情况不可能出现于 C++,从而从上面的讨论中省略。在 C 中,聚合复制和赋值将聚合体对象作为整体访问。但 C++ 中始终通过成员函数调用进行这种行动,这会访问单独的子对象而非整个对象(或在联合体的情况下,复制对象表示,即经由 unsigned char)。见核心问题 2051。

假设符合对齐要求,则 reinterpret_cast 在处理指针可互相转换对象的少数受限情况外,不更改指针的值:

    struct S1 { int a; } s1;
    struct S2 { int a; private: int b; } s2; // 非标准布局
    union U { int a; double b; } u = {0};
    int arr[2];
     
    int* p1 = reinterpret_cast<int*>(&s1); // p1 的值为“指向 s1.a 的指针”
                                           // 因为 s1.a 与 s1 为指针可互转换
     
    int* p2 = reinterpret_cast<int*>(&s2); // reinterpret_cast 不更改 p2 的值为“指向 s2 的指针”。
     
    int* p3 = reinterpret_cast<int*>(&u);  // p3 的值为“指向 u.a 的指针”:u.a 与 u 指针可互转换
     
    double* p4 = reinterpret_cast<double*>(p3); // p4 的指针为“指向 u.b 的指针”:u.a 与 u.b
                                                // 指针可互转换,因为都与 u 指针可互转换
     
    int* p5 = reinterpret_cast<int*>(&arr); // reinterpret_cast 不更改 p5 的值为“指向 arr 的指针”

在不实际代表适当类型的对象的泛左值(例如通过 reinterpret_cast 所获得)上,进行代表非静态数据成员或非静态成员函数的成员访问,将导致未定义行为:

    struct S { int x; };
    struct T { int x; int f(); };
    struct S1 : S {}; // 标准布局
    struct ST : S, T {}; // 非标准布局
     
    S s = {};
    auto p = reinterpret_cast<T*>(&s); // p 的值为“指向 s 的指针”
    auto i = p->x; // 类成员访问表达式为未定义行为:s 不是 T 对象
    p->x = 1; // 未定义行为
    p->f();   // 未定义行为
     
    S1 s1 = {};
    auto p1 = reinterpret_cast<S*>(&s1); // p1 的值为“指向 S 的 s1 子对象的指针”
    auto i = p1->x; // OK
    p1->x = 1; // OK
     
    ST st = {};
    auto p2 = reinterpret_cast<S*>(&st); // p2 的值为“指向 st 的指针”
    auto i = p2->x; // 未定义行为
    p2->x = 1; // 未定义行为

许多编译器在这种情况下发布“严格别名化”警告,即使在技术上这种构造所违背的并非称为“严格别名化规则”段落的规则。

严格别名化及其相关规则的目的,是启用基于类型的别名分析,若程序能合法地创建一种情形,使得两个指向无关类型的指针(例如一个 int* 和一个 float*)能同时存在并可一同用于加载或存储同一内存(见此 SG12 reflector 上的邮件),则别名分析会普遍无效。故任何看起来能够创建这种情形的技巧都必然导致未定义行为。

当需要将对象的字节解释为不同类型的值时,可以使用 std::memcpy 或 std::bit_cast (C++20 起):

    double d = 0.1;
    std::int64_t n;
    static_assert(sizeof n == sizeof d);
    // n = *reinterpret_cast<std::int64_t*>(&d); // 未定义行为
    std::memcpy(&n, &d, sizeof d); // OK
    n = std::bit_cast<std::int64_t>(d); // 亦 OK

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR    应用于    出版时的行为    正确行为
CWG 195    C++98    不允许函数指针和对象指针间的转换    使之为条件性支持
示例

演示 reinterpret_cast 的一些用法:

    #include <cstdint>
    #include <cassert>
    #include <iostream>
    int f() { return 42; }
    int main()
    {
        int i = 7;
     
        // 指针到整数并转回
        std::uintptr_t v1 = reinterpret_cast<std::uintptr_t>(&i); // static_cast 为错误
        std::cout << "The value of &i is 0x" << std::hex << v1 << '\n';
        int* p1 = reinterpret_cast<int*>(v1);
        assert(p1 == &i);
     
        // 到另一函数指针并转回
        void(*fp1)() = reinterpret_cast<void(*)()>(f);
        // fp1(); 未定义行为
        int(*fp2)() = reinterpret_cast<int(*)()>(fp1);
        std::cout << std::dec << fp2() << '\n'; // 安全
     
        // 通过指针的类型别名化
        char* p2 = reinterpret_cast<char*>(&i);
        if(p2[0] == '\x7')
            std::cout << "This system is little-endian\n";
        else
            std::cout << "This system is big-endian\n";
     
        // 通过引用的类型别名化
        reinterpret_cast<unsigned int&>(i) = 42;
        std::cout << i << '\n';
     
        [[maybe_unused]] const int &const_iref = i;
        // int &iref = reinterpret_cast<int&>(const_iref); // 编译错误——不能去除 const
        // 必须用 const_cast 代替:int &iref = const_cast<int&>(const_iref);
    }

可能的输出:

    The value of &i is 0x7fff352c3580
    42
    This system is little-endian
    42

参阅:

原文:reinterpret_cast <new_type> (expression)

reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。

什么是无关类型?我没有弄清楚,没有找到好的文档来说明类型之间到底都有些什么关系(除了类的继承以外)。后半句倒是看出了reinterpret_cast的字面意思:重新解释(类型的比特位)。我们真的可以随意将一个类型值的比特位交给另一个类型作为它的值吗?其实不然。

IBM的C++指南里倒是明确告诉了我们reinterpret_cast可以,或者说应该在什么地方用来作为转换运算符:

    从指针类型到一个足够大的整数类型
    从整数类型或者枚举类型到指针类型
    从一个指向函数的指针到另一个不同类型的指向函数的指针
    从一个指向对象的指针到另一个不同类型的指向对象的指针
    从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
    从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针

不过我在Xcode中测试了一下,事实上reinterpret_cast的使用并不局限在上边所说的几项的,任何类型的指针之间都可以互相转换,都不会得到编译错误。上述列出的几项,可能 是Linux下reinterpret_cast使用的限制,也可能是IBM推荐我们使用reinterpret_cast的方式

所以总结来说:reinterpret_cast用在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。

(所谓"足够大的整数类型",取决于操作系统的参数,如果是32位的操作系统,就需要整形(int)以上的;如果是64位的操作系统,则至少需要长整形(long)。具体大小可以通过sizeof运算符来查看)。
reinterpret_cast有何作用

从上边对reinterpret_cast介绍,可以感觉出reinterpret_cast是个很强大的运算符,因为它可以无视种族隔离,随便搞。但就像生物的准则,不符合自然规律的随意杂交只会得到不能长久生存的物种。随意在不同类型之间使用reinterpret_cast,也之后造成程序的破坏和不能使用。

比如下边的代码
typedef int (*FunctionPointer)(int);
int value = 21;
FunctionPointer funcP;
funcP = reinterpret_cast<FunctionPointer> (&value);
funcP(value);

我先用typedef定义了一个指向函数的指针类型,所指向的函数接受一个int类型作为参数。然后我用reinterpret_cast将一个整型的地址转换成该函数类型并赋值给了相应的变量。最后,我还用该整形变量作为参数交给了指向函数的指针变量。

这个过程编译器都成功的编译通过,不过一旦运行我们就会得到"EXC_BAD_ACCESS"的运行错误,因为我们通过funcP所指的地址找到的并不是函数入口。

由此可知,reinterpret_cast虽然看似强大,作用却没有那么广。IBM的C++指南、C++之父Bjarne Stroustrup的FAQ网页和MSDN的Visual C++也都指出:错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。

这样说起来,reinterpret_cast转换成其它类型的目的只是临时的隐藏自己的什么(做个卧底?),要真想使用那个值,还是需要让其露出真面目才行。那到底它在C++中有其何存在的价值呢?

MSDN的Visual C++ Developer Center 给出了它的使用价值:用来辅助哈希函数。下边是MSNDN上的例子:

                

    // expre_reinterpret_cast_Operator.cpp
    // compile with: /EHsc
    #include <iostream>
    // Returns a hash code based on an address
    unsigned short Hash( void *p ) {
        unsigned int val = reinterpret_cast<unsigned int>( p );
        return ( unsigned short )( val ^ (val >> 16));
    }
     
    using namespace std;
    int main() {
        int a[20];
        for ( int i = 0; i < 20; i++ )
            cout << Hash( a + i ) << endl;
    }
     
    //如果跟我一样是64位的系统,可能需要将unsigned int改成 unsigned long才能运行。


            

这段代码适合体现哈希的思想,暂时不做深究,但至少看Hash函数里面的操作,也能体会到,对整数的操作显然要对地址操作更方便。在集合中存放整形数值,也要比存放地址更具有扩展性(当然如果存void *扩展性也是一样很高的),唯一损失的可能就是存取的时候整形和地址的转换(这完全可以忽略不计)。

不过可读性可能就不高,所以在这种情况下使用的时候,就可以用typedef来定义个指针类型:
typedef unsigned int PointerType;

这样不是更棒,当我们在64位机器上运行的时候,只要改成:
typedef unsigned long PointerType;
当reinterpret_cast面对const

IBM的C++指南指出:reinterpret_cast不能像const_cast那样去除const修饰符。 这是什么意思呢?代码还是最直观的表述:

     
    int main()
    {
        typedef void (*FunctionPointer)(int);
        int value = 21;
        const int* pointer = &value;
        
        //int * pointer_r = reinterpret_cast<int*> (pointer);
        // Error: reinterpret_cast from type 'const int*' to type 'int*' casts away constness
        
        FunctionPointer funcP = reinterpret_cast<FunctionPointer> (pointer);
    }


例子里,我们像前面const_cast一篇举到的例子那样,希望将指向const的指针用运算符转换成非指向const的指针。但是当实用reinterpret_cast的时候,编译器直接报错组织了该过程。这就体现出了const_cast的独特之处。

但是,例子中还有一个转换是将指向const int的指针付给指向函数的指针,编译顺利通过编译,当然结果也会跟前面的例子一样是无意义的。

如果我们换一种角度来看,这似乎也是合理的。因为
const int* p = &value;
int * const q = &value;

这两个语句的含义是不同的,前者是"所指内容不可变",后者则是"指向的地址不可变"(具体参考此处)。因此指向函数的指针默认应该就带有"所指内容不可变"的特性。

毕竟函数在编译之后,其操作过程就固定在那里了,我们唯一能做的就是传递一些参数给指针,而无法改变已编译函数的过程。所以从这个角度来想,上边例子使用reinterpret_cast从const int * 到FunctionPointer转换就变得合理了,因为它并没有去除const限定

参考链接:

https://zh.cppreference.com/w/cpp/language/reinterpret_cast

C++ Language Tutorial - Type Casting

Object Oriented Design

IBM Complilers - XL C/C++ V9.0 for Linux - The reinterpret_cast operator (C++ only)

Bjarne Stroustrup's C++ Style and Technique FAQ

MSDN Visual C++ Developer Center - reinterpret_cast Operator
————————————————
版权声明:本文为CSDN博主「ppipp1109」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/p942005405/article/details/105783090

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值