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

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

文章目录

  • 前言
  • 数据类型自动转换
  • C语言中的强制类型转换
  • C++中的强制类型转换
    • static_cast
    • dynamic_cast
    • const_cast
    • reinterpret_cast
    • 强转关键字的选择
  • 总结

前言

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

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

数据类型自动转换

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

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

char
unsigned/int
short
unsigned/long
double
float

C语言中的强制类型转换

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

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


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

或者

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

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

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

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等。
    int val = 110119;
    char c = static_cast<char>(val);
    double d = static_cast<double>(val);
  1. 将表达式转换成void类型,并将转换后的结果丢弃
    int val = 110119;
    static_cast<void>(val);
  1. 可以用于void* 和其他指针类类型之间的转换,但是不能用于两个无关指针类型的直接转换
   // 正常转换
   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);
  1. 可以用于类继承结构中基类和派生类之间指针或引用的转换,向上转型安全,向下转型由于没有动态类型检查,是不安全的。
   struct B { };
   struct D : B { };

   D d;
   B& rb = d;
   D& rd = static_cast<D&>(rb);
  1. 如果涉及左值到右值、数组到指针或函数到指针的转换,也可以通过static_cast显式执行。
   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. 普通类型的指针无法转换
   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);
  1. 继承结构中基类里面没有虚函数无法转换
   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)
  1. 指针或引用转换的类型不是正确的类型,如果参数类型是指针会返回目标类型空指针,如果参数类型是引用则会抛出 std::bad_cast 异常。
   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;
  1. 一个正常转换的例子,包含向上、向下、横向转换
   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 不是简单的数据强转,还进行了指针的偏移:

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. 尝试去除非指针和引用的类型的常量性会编译失败
   const int i = 6;

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

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

运行结果如下:

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

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

   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 这个常数,运行结果如下:

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. 不同基础类型指针类型之间转换:
   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);
  1. 基础类型指针与类对象指针之间的转换:
   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. 将地址值转换成整数
   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;

运行结果如下:

&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 是一种内存数据的重新解释,比较原始,开发者使用它的时候应该明确的知道自己在做什么

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

今夜的雨,好美~

相关文章:

  • C++11中std::move和std::forward到底干了啥
  • 使用box2dweb做一个下落的小球,宝宝玩的不亦乐乎
  • C++中使用std::sort自定义排序规则时要注意的崩溃问题
  • 从一个小题中的应用来体会下std::tie的便利之处
  • Floyd-Warshall——仅用4行代码就能解决多源最短路径问题的算法
  • Dijkstra——通过不断松弛来解决单源最短路径问题的算法
  • C++11中的std::atomic保证的原子性是什么
  • .bat批处理(十):从路径字符串中截取盘符、文件名、后缀名等信息
  • linux环境下从路径字符串中截取目录和文件名信息
  • MD5是用来加密的吗?BCrypt又是什么呢
  • 树的带权路径长度和哈夫曼树
  • 完全图与强连通图的那些坑
  • linux环境下恢复rm误删的文件
  • 记一次使用Valgrind查找解决内存问题的玄幻旅程
  • 网络工具nc的常见功能和用法
  • 002-读书笔记-JavaScript高级程序设计 在HTML中使用JavaScript
  • CSS实用技巧干货
  • Django 博客开发教程 8 - 博客文章详情页
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • Java|序列化异常StreamCorruptedException的解决方法
  • k8s 面向应用开发者的基础命令
  • k个最大的数及变种小结
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • React 快速上手 - 06 容器组件、展示组件、操作组件
  • redis学习笔记(三):列表、集合、有序集合
  • sessionStorage和localStorage
  • spring-boot List转Page
  • TCP拥塞控制
  • 从零搭建Koa2 Server
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 数据仓库的几种建模方法
  • 学习笔记DL002:AI、机器学习、表示学习、深度学习,第一次大衰退
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • ​linux启动进程的方式
  • ​第20课 在Android Native开发中加入新的C++类
  • ​软考-高级-系统架构设计师教程(清华第2版)【第9章 软件可靠性基础知识(P320~344)-思维导图】​
  • #pragma 指令
  • #我与虚拟机的故事#连载20:周志明虚拟机第 3 版:到底值不值得买?
  • (二)linux使用docker容器运行mysql
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (论文阅读30/100)Convolutional Pose Machines
  • (原創) 如何刪除Windows Live Writer留在本機的文章? (Web) (Windows Live Writer)
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转) Android中ViewStub组件使用
  • (转)Oracle存储过程编写经验和优化措施
  • (转载)从 Java 代码到 Java 堆
  • (最全解法)输入一个整数,输出该数二进制表示中1的个数。
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • *2 echo、printf、mkdir命令的应用
  • .net core 6 集成和使用 mongodb
  • .NET 中使用 Mutex 进行跨越进程边界的同步
  • .Net开发笔记(二十)创建一个需要授权的第三方组件
  • @Autowired 与@Resource的区别
  • [ C++ ] STL_list 使用及其模拟实现