当前位置: 首页 > news >正文

【C++】Type punning类型双关、union联合体、C++中的类型转换casting

二十、Type punning类型双关、union联合体、C++中的类型转换casting

1、什么是数据类型?
我们知道C++是一门强类型语言,它有一个数据类型系统,而且类型是强制要求的。
当我们创建变量的时候,我们必须声明变量的类型,因为声明变量类型是由编译器强制执行的,你不声明类型,编译就过不去。

为什么要这么执拗呢,我不声明类型不行嘛?不行!因为类型系统绝对是有其存在的理由的,你绕开类型系统,对这块内存随性操作,是非常容易引发很多潜在的风险和问题的。在我这篇博文 【C++】深度理解C++数据类型:常量、变量、数组、字符串、指针、函数_c++ 字符串常量-CSDN博客 中的开头就长篇大论的介绍了数据类型的存在意义。

2、什么是类型转换?
就是必须在C++可用的类型系统中进行类型转换。比如,如果我把某个对象设置为Int类型,那我得把这个对象当作int类型来操作,不能突然把它当作double或float来处理,除非有一个简单的隐式转换。

隐式转换就是C++将一个对象,从一种类型转换成另外一种类型,这个转换过程中没有数据损失。这个隐式转换是编译器自动执行的,程序员无需care,是安全的。也就是说这个过程对程序员来说是无感的,程序员不需要特别的说明。比如下面的例子:

除了隐式转换,我们也可以显式转换,就是程序员需要告诉C++编译器,命令编译器把某个对象的类型从其原来的类型转化为别的类型。
实现显式类型转换至少有两类途径:
一是C风格的类型转换。就是下面的小标题3:type punning 和 小标题4:union联合体。
二是C++风格的类型转换。就是下面的小标题5,casting,强制转换。

3、类型双关(Type punning)
类型双关是C中的、显式的、程序员强制进行的类型转换方式。C中的Type punning操作是指,绕过类型系统,直接操作内存,以提高性能或实现特定的低级别功能。从另外一个角度说就是,我可以把我拥有的这段内存,当作不同类型的内存来对待。这种做法是在某些低级场景中使用的技巧。这些技巧是要慎重使用的,因为类型系统绝对是有其存在的理由的,你绕开类型系统,对这块内存随性操作,是非常容易引发很多潜在的风险和问题的。建议非不得已才使用,要谨慎使用。

但是假如是这样的场景:现在我们知道有一块内存,并且知道这块内存中存储的是什么数据类型、而且知道这块内存中没有指向其他地方的指针。我现在就是希望把这块内存作为一个字节数组,然后将它流出来(字节流)放到其他地方存储。这种情况下,C的效率是最高的,因为C可以实现这种最原始的、最底层的访问。此时要完成这个任务,我们可以通过Type punning编程技巧,绕过类型系统,直接操作内存,非常高效的完成这个非常底层的工作。

下面就介绍几种类型双关的用法:
(1)通过指针强制转换

上例一是C++的隐式转换,是由类型系统自动完成的。
上例二就是程序员强制的、C风格的转换。用指针拷贝一个内存块,是类型双关的一种使用方式。就是你没有走C++的类型系统,你绕开了类型系统,你自己自作主张把a(占4个字节块的int块)的首地址拿出来,然后把这个首地址强制转化为double*指针。你再解引用这个指针时,编译器看到这是double*指针,自然是从这个指针指的首地址开始往下复制8个字节的二进制数据,然后赋值给变量b。当你cout b时,也就是解析b时,就是把这8个字节流当double类型解析的,那自然就是乱码了。

可见当你通过指针强制转换一个内存块后,再通过解引用指针去拷贝这个内存块到另外一个地方时,你拷贝的就是没有通过类型解析的纯纯的原始二进制字节流,而且拷贝的长度可能都有问题,因为你强制转化了类型了嘛,类型其中一个重要的信息就是这个数据的长度嘛,所以你长度可能都搞错了。当你拷贝完毕后想解析你拷贝的字节流时,系统就以为你拷贝的这个字节流原本就是double块,所以现在也按照double类型来解析,结果就大错特错了。

隐式类型转换是安全的,显式类型转换是有风险的,C语言之所以增加强制类型转换的语法,就是为了强调风险,让程序员意识到自己在做什么。

(2)通过引用转换

C风格的强制类型转换统一使用(),而()在代码中随处可见,所以也不利于使用文本检索工具定位关键代码,风险追溯都很难,所以程序员一定要知道自己在干什么。

(3)利用结构体的特点进行转换

上图中的3:
&e 表示拿到结构体e的首地址;
(char*)&e 表示将e的首地址强制转化为char*指针;
(char*)&e +4 这是指针运算,表示把char*指针往后移动4个字节,也就是变量y的首地址;
(int*)((char*)&e +4) 表示把y的首地址强制转化为int*指针; *(int*)((char*)&e +4) 表示解引用int*指针。
解引用完毕,将值赋值给变量z,打印z也是8。

补充:结构体中的属性对象的存储方式
上面例子的结构体中的两个属性对象x,y都是int类型的,所以可以看成数组。其实情况并不总是这样的,我们看下面这个例子:

从上例可以看成char类型对象竟然也分配了4个字节。其实结构体在内存分配中还有一个内存对齐的操作,所以你在取数据时一定得看清楚,一不小心就错了。关于内存对齐,在我之前写C语言 【C语言学习笔记】八、结构体_c语言结构体笔记-CSDN博客 时也提到过,感兴趣得可以自行查看。

上面的例子都是C的最原始的内存操作,也正是有了这些任性自如的内存操作,使得C、C++成为最为高效的编程语言。但是如果没有极大需求的情况下,上述代码尽量、永远不要出现在实际的应用程序中,除非你使用后有极大的好处。

如果说上面的都是一些骚操作,那类型双关的最安全、最正确道路就是使用联合体C++内置的类型转换。其中联合体见下面的小标题4,C++内置的类型转换见下面小标题5。

4、union(联合体、共用体)
联合体有点像类类型或者结构体类型,只不过它一次只能占用一个成员的内存。比如下图是我在一个联合体对象中,声明了4个不同类型的对象:a,b,c,d,此时这4个对象的大小都是8个字节(以最大的那个对象的长度为长度)。当我修改abcd中的任意一个对象的值,abcd的值都随之改变,因为它们共享一个内存块。
(1)联合体的工作原理:

意思就是这8个字节,我可以有4种处理方法,我可以放int类型,也可以放char类型,也可以放float类型,还可以放double类型。
我们可以像使用类或者结构体一样使用联合体,可以给联合体添加静态函数或者普通函数、方法等等。但是你不能使用虚方法,还是有一些限制的。

(2)union经常是匿名使用的,但是匿名union不能含有成员函数

这就是匿名union,一般和结构体结合使用。上图的结构体就只有4个字节。这4个字节可以存放一个int型,或者存放一个float类型,或者存放一个char类型的。但不管存哪种类型,都是只能存一个。

(3)通常需要类型双关时,我们才使用联合体
就是你想让同一块内存块可以表示不同数据类型时,使用联合体就非常合适。或者说当你想给同一个变量取两个不同类型的名字时,它就真的很有用。

从上面例子可以看出,当你想用多种方法来处理相同的数据时,使用union的可读性就更强。

再比如下图的vector(x, y, z, w),我还想叫它vector(r, g, b),其中r对应的是x和y, g对应z, b对应w。这个需求可以通过union来实现:

5、C++风格的类型转换casting
为了避免C风格简单粗暴的类型转换可能出现的种种问题,为了能安全、可追溯、无损的进行类型转换,虽然会增加一点点开销,还是建议大家使用C++中的类型转换:static_cast、dynamic_cast、const_cast和reinterpret_cast 使用这四个关键字分门别类的进行类型转换。

C++将类型转换细化分成了四类:
static_cast:静态类型转化
dynamic_cast:动态类型转化,仅适用于C++,不能在C中使用。
reinterpret_cast:将内存重新解释为其他的类型,就类似C风格的(),里面写你的新类型,所以C可以做到的type punning,reinterpert_cast都可以。
const_cast:移除或者添加变量的const修饰符。

这种将类型转化继续细化、分类,并分门别类的支持,这种做法的好处是,一是可以进行检查,二是可以在代码库中搜索我到底是在哪里进行了类型转换,可以追溯问题。
所以,C++也并不是在C的基础上添加什么新功能,只是添加了一些语法糖到你的代码中,比如进行编译检查时,如果转换不成功会返回NULL,也所以是会有一点点开销的,就是会让你的程序慢下来一点点。

(1)static_cast

在编译时,编译器对变量a进行类型转换,从double转成int,然后再和5.5相加。此时又会默认把int a再隐式转换成double,再和5.5相加,结果赋值给变量c。

(2)dynamic_cast
dynamic_cast是专门用于沿继承层次结构进行的强制类型转换。dynamic_cast可以在运行时查看转换是否成功,如果转换失败会返回一个NULL指针:

上面例子中,对象d是类Derived的实例,而Derived类又是Base类的子类,所以可以把d赋值给b。
但是要像C那样简单粗暴的将b转化为AnotherClass类型就不行!因为b现在是Base类型,AnotherClass继承了Base,Derived也继承了Base,所以b到底是AnotherClass还是Derived的实例,我们都没法说清楚,所以b是无法通过C风格转化为AnotherClass类型的。
但是b可以被dynamic_cast转化为Derived类型,是因为dynamic_cast存储了所有类型的**运行时类型信息RTTI(runtime type information)**。所以dynamic_cast知道b本来就是d赋值的,所以b是可以转化为Derived类型的。而b是永远无法转化为AnotherClass类型的,所以返回的是NULL指针。

dynamic_cast是在程序运行时进行类型转换。所以可以让我们做动态类型转换之类的事情。但是同时你也要知道,由于dynamic_cast存储着所有类型的运行时类型信息RTTI,所以是一是有一定的运行成本的,二是需要时间开销的,因为它需要检查类型信息是否匹配。如果你对你的代码有绝对的把握,绝对不会在类型转换时出现像崩溃等这种意想不到的事情,你也不想有这些额外的开支,你可关闭RTTI状态:

说明:我们一般是不会关闭RTTI的,因为虽然会带来一些开销,但是减少了像系统崩溃的这种意想不到的问题。

reinterpret_cast和C风格的用法一样。const_cast只是添加或者移除const修饰词,都非常简单,这里就不展开了。总之,这些C++内置的类型转换会让转换更加可靠,因为它们会进行检查,你的代码会更加可靠。而使用C风格的指针,只是强行将指针指向的内容解释成别的东西,非常容易出现风险。


http://www.mrgr.cn/news/60588.html

相关文章:

  • JDK的下载
  • PPT / Powerpoint中利用LaTeX输入公式
  • 微信小程序生成海报 / 两张图片合并生成一张
  • git restore恢复删除文件
  • NSSCTF刷题篇web部分
  • 正则表达式使用举例一(Python下)
  • 手机也能开数电票了,可能我也即将要失业了
  • Codigger桌面模式之Size Look介绍
  • 使用语言模型进行文本摘要的五个级别(llm)
  • 去中心化的概述
  • 机器学习中的谱聚类及实践案例
  • VLAN聚合
  • 1.字节大小
  • Java集合框架中的泛型有什么优点?
  • 第13天小整理
  • 机器学习入门之监督学习
  • 并联 高电压、高电流 放大器实现 2 倍输出电流模块±2A
  • 《学会提问》
  • qt-opensource-windows-x86-5.14.2.rar
  • 聚水潭到畅捷通T+的数据高效集成方案解析
  • OceanBase数据库结合ETLCloud快速实现数据集成
  • GEE 图表:利用CGIAR/SRTM90_V4绘制雷尼尔山登山步道沿途的海拔高度图表
  • DevCon,我们来了|DAOBase 线下活动(曼谷站)
  • python -m pip install --upgrade pip和pip install --upgrade pip有什么区别?
  • python读取CSV文件
  • 算法4之链表