【C++ 第二十二章】C++的类型转换

打印 上一主题 下一主题

主题 1791|帖子 1791|积分 5373





1.C语言中的类型转换

在C语言中,如果赋值运算符左右两侧类型不同,或者形到场实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中统共有两种形式的类型转换:隐式类型转换和显式类型转换。

  • 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
  • 显式类型转化:需要用户本身处理
  1. void Test()
  2. {
  3.         int i = 1;
  4.         // 隐式类型转换
  5.         double d = i;
  6.         printf("%d, %.2f\n", i, d);
  7.         int* p = &i;
  8.         // 显示的强制类型转换
  9.         int address = (int)p;
  10.         printf("%x, %d\n", p, address);
  11. }
复制代码



C风格的转换格式很简单,但是有不少缺点的:

  • 隐式类型转化有些情况下可能会出问题:好比数据精度丢失
  • 显式类型转换将所有情况混淆在一起,代码不够清晰



2.C++逼迫类型转换

标准C++为了增强类型转换的可视性,引入了四种定名的逼迫类型转换操作符:
static_cast、reinterpret_cast、const_cast、dynamic_cast
先列个提要:
(1)内置类型 = 内置类型:隐式类型转换 或 逼迫类型转换 使用 static_cast、reinterpret_cast
(2)内置类型 = 自界说类型:自界说类型内部重载 operator int() 这样的函数(int x = A.operator int();)
(3)自界说类型 = 内置类型:隐式类型转换(string s = “hehe”;)
(4)自界说类型 = 自界说类型:自界说类型内部重载构造函数

2.1 static_cast 与 reinterpret_cast

  1. // static_cast:这个就代表 隐式类型转换,目的是为了规范化,提示别人这里存在隐式类型转换
  2. int a1 = 10;
  3. double b1 = static_cast<double>(a1);
  4. // reinterpret_cast:reinterpret 表示重新解释,这里就是 强制类型转换
  5. int a2 = 10;
  6. double* p1 = reinterpret_cast<double*>(a2);
复制代码

2.2 const_cast

这段代码有点诡异!!
  1. int main() {
  2.         // const_cast:将常变量去除常性,使其可以修改
  3.         const int a3 = 5;
  4.         int* p2 = const_cast<int*>(&a3);
  5.         *p2 = 6;
  6.         cout << a3 << '\n';
  7.         cout << *p2 << '\n';
  8.         return 0;
  9. }
复制代码




我们明明通过逼迫类型转换,通过 a3 的地址修改了 a3,但是打印效果:a3 居然还是 5???
甚至我们通过调试窗口查看 a3 的变化:a3 也是变成了 6
为什么打印出来还是 5 ?
这里实在是编译器的优化手段,将 a3 界说成常量后,编译器将 a3 这个符号直接类似界说宏,直接宏替换成 5,之后程序碰到 a3 的地方会直接替换成 5


解决办法:加上 volatile 关键字,意思是去掉编译器这个优化
  1. volatile const int a3 = 5;
复制代码

小结:因此说 const_cast 的修改是有一定风险的,涉及这种直接去掉常性,修改常变量的,一定要加上 volatile


2.3 dynamic_cast

dynamic_cast 用于将一个父类对象的指针/引用 转换 子类对象的指针或引用(动态转换)
注意是父子类指针或引用之间的转换,不是父子类对象之间的转换
(子类对象可以直接赋值给父类对象,父类对象不可以赋值给子类对象)


看表明理解:
  1. class A
  2. {
  3. public:
  4.         //virtual void func() {};
  5.         int _a = 2;
  6. };
  7. class B : public A
  8. {
  9. public:
  10.         int _b = 3;
  11. };
  12. void Func(A* pa) {
  13.         // 父类指针 pa 指向 子类对象,下面将  子类指针转换成子类指针,没问题
  14.         // 父类指针 pa 指向 父类对象,下面将  父类指针转换成子类指针,则该指针就会存在越界访问的风险
  15.         //(比如你父类对象大小为5字节,子类对象大小为5字节, 而你将你父类指针强转为子类指针,那么就是本来只有5字节空间,你却可以访问10字节空间,这显然是有越界访问的风险的)
  16.         B* pb = reinterpret_cast<B*>(pa);
  17.         // 越界读数据:可能不报错
  18.         cout << "pa:" << pa->_a << '\n';
  19.         cout << "pb:" << pb->_b << '\n';
  20.         // 越界写数据:直接报错
  21.         pa->_a++;
  22.         pb->_b++;
  23.         cout << "pa:" << pa->_a << '\n';
  24.         cout << "pb:" << pb->_b << '\n';
  25. }
  26. int main() {
  27.         A a;
  28.         B b;
  29.         Func(&a);
  30.         Func(&b);
  31.         return 0;
  32. }
复制代码



由上面代码演示可知:父类指针转换为子类指针存在风险
因此需要使用 dynamic_cast 查抄是否可以转换成功,若转换失败会返回空指针,以此来防范这个风险



dynamic_cast 的使用要求:
1.dynamic_cast 只能用于父类含有虚函数的类:因此象征性的给父类A加一个 虚函数
2.dynamic_cast 会先查抄是否能转换成功,能成功则转换,不能则返回 0。



  1. class A
  2. {
  3. public:
  4.         virtual void func() {};
  5.         int _a = 2;
  6. };
  7. class B : public A
  8. {
  9. public:
  10.         int _b = 3;
  11. };
  12. void Func(A* pa) {
  13.         // 父类指针 pa 指向 子类对象,下面将  子类指针转换成子类指针,没问题
  14.         // 父类指针 pa 指向 父类对象,下面将  父类指针转换成子类指针,则该指针就会存在越界访问的风险
  15.         //(比如你父类对象大小为5字节,子类对象大小为5字节, 而你将你父类指针强转为子类指针,那么就是本来只有5字节空间,你却可以访问10字节空间,这显然是有越界访问的风险的)
  16.         B* pb = dynamic_cast<B*>(pa);
  17.         if (pb) {
  18.                 // 越界读数据:可能不报错
  19.                 cout << "pa:" << pa->_a << '\n';
  20.                 cout << "pb:" << pb->_b << '\n';
  21.                 // 越界写数据:直接报错
  22.                 pa->_a++;
  23.                 pb->_b++;
  24.                 cout << "pa:" << pa->_a << '\n';
  25.                 cout << "pb:" << pb->_b << '\n';
  26.         }
  27.         else cout << "转换失败" << '\n';
  28. }
  29. int main() {
  30.         A a;
  31.         B b;
  32.         cout << "Func(&a):";
  33.         Func(&a);
  34.         cout << '\n';
  35.         cout << "Func(&b):\n";
  36.         Func(&b);
  37.         return 0;
  38. }
复制代码



为什么只能用于父类含有虚函数的类?
由于 dynamic_cast 本质上是通过虚表来实现的,类似于判断虚表中的 父子类是否有某种标志,判断是否可以转换成功
(这里不重要,可以本身了解)



2.4 内置类型 = 自界说类型

这个实在是在 自界说类型中添加一个函数,若该自界说类型要转换为 int 类型的数据,则添加 operator int() 函数;同理,若要转换为 double类型,则添加 operator double()
  1. class A
  2. {
  3. public:
  4.         operator int() {
  5.                 // 里面写什么东西都是自定义的了
  6.                 return _a;
  7.         }
  8. private:
  9.         int _a = 10;
  10. };
  11. int main() {
  12.         A a;
  13.         int x = a;
  14.         cout << x << '\n';
  15.         return 0;
  16. }
复制代码

智能指针中也有使用相关重载函数:



2.5 自界说类型 = 内置类型

这个实在平时都有运用,本质上是隐式类型转换
  1. int main() {
  2.         // "hehehe" 是内置类型 char*,编译器底层将其隐式类型转换为 string 类型
  3.         string s = "hehehe";
  4.         return 0;
  5. }
复制代码



2.6 自界说类型 = 自界说类型

前面解说过,两个类型若要进行类型转换操作,则这两个对象必须具有某种特定接洽
但是通常,两个不同的自界说类型都不会有直接的接洽,因此不能容易的类型转换
需要在 左边的自界说类型 中重载参数为 右边自界说类型 的构造函数:
将自界说类型 A 的变量 拷贝给 自界说类型 B 的变量,需要在 类B 中重载新的构造函数:


  1. B(const A& a)
  2.         :_b(a.get())
  3. {}
复制代码

  1. #include"List.h"
  2. #include<list>
  3. // 和自定义类型相关的类型转换基本都和构造函数有关
  4. // 类型转换的两者一定要有一定的关联
  5. // 自定义类型的转换,一定要产生某种关联
  6. class A
  7. {
  8. public:
  9.         A(const int& x = 0, const int& y = 0)
  10.                 :_a1(x)
  11.                 , _a2(y)
  12.         {}
  13.          explicit :该关键字是禁止掉 隐式类型转换,但是可以显式类型转换
  14.         //explicit operator int() {
  15.         //        return _a1 + _a2;
  16.         //}
  17.         int get() const {
  18.                 return _a1 + _a2;
  19.         }
  20.          这里写这个函数会报一堆奇怪的错:因为类 B 声明在下面,程序运行时向上查询就找不到类 B,导致报错
  21.         //A(const B& b)
  22.         //        :_a1(b.get())
  23.         //{}
  24. private:
  25.         int _a1;
  26.         int _a2;
  27. };
  28. class B
  29. {
  30. public:
  31.         B(const int& x = 0)
  32.                 :_b(x)
  33.         {}
  34.         // 这里加上 const,为什么 a.get() 不能使用(A类中的 get 没有 const 修饰):这就设计到 const 权限放大的问题
  35.         //先理清一个概念:我们平时通过一个对象或对象指针去调用成员函数,因为成员函数隐含着第一个参数 this 指针,因此本质上都要传递一个this指针过去,
  36.         // 涉及到 this 指针的传参,就要注意 this 指针的权限问题(是否被 const 修饰)
  37.         // B(const A& a)  这里对象 a 的this指针类型:const A*,而 get 函数this指针类型:A* 当然会权限放大!!!
  38.         B(const A& a)
  39.                 :_b(a.get())
  40.         {}
  41.         // 这个也能匹配给 b1 = a1,但是会优先选择上面的构造 B(const A& a)  
  42.         operator int() {
  43.                 cout << "operator int()" << '\n';
  44.                 return _b;
  45.         }
  46. private:
  47.         int _b;
  48. };
  49. int main() {
  50.         B b1 = 10;
  51.         A a1 = 20;
  52.         b1 = a1;
  53.         return 0;
  54. }
复制代码

本质上,实在和自界说类型相关的类型转换基本都和构造函数有关 控制好自界说类型的构造函数,就可以通过不同类型的数据来构造一个自界说类型

【思考题】这里是 const 的权限缩小吗?

有人说,将非const类型的 iterator 赋值给 const 类型的迭代器 const_iterator 是一种 const 的权限缩小???
  1. list<int> lt1 = { 1, 2, 3, 4 };
  2. list<int>::const_iterator it = lt1.begin();  // 这个是 const 权限的缩小?
复制代码

要注意,迭代器是一种类,两个迭代器之间的拷贝赋值,是自界说类型之间的类型转换。 而 const 的权限问题只会出现在 内置类型之间 与 内置类型指针或引用之间



因此,我们直接在自界说类型 list 迭代器类 内部写一个新的构造函数即可
(我们这里的 list 是本身模仿实现的,现在就是演示添加类型转换功能给你看)
(C++STL库中的 list 的迭代器也可以类型转换(可以本身试一试))



由于迭代器类是模板,其中的构造函数参数固定写成 非const类型的迭代器 iterator
  1. // 迭代器就是将指向一个节点的指针 node 封装成一个类
  2. template<class T, class Ref, class Ptr>
  3. struct ListIterator
  4. {
  5.         typedef ListNode<T> Node;
  6.         typedef ListIterator<T, Ref, Ptr> Self;  // 自己这种类型
  7.         Node* _node;
  8.         // 类型转换的构造:这里参数固定写成 iterator
  9.         // 当该模板实例化为 iterator :这里就是拷贝构造
  10.         // 当该模板实例化为 const_iterator :这里就是普通构造(支持 iterator 转换成 const_iterator)
  11.         ListIterator(const ListIterator<T, T&, T*>& it)
  12.                 :_node(it._node)
  13.         {}
  14.         //......其他函数
  15. };
复制代码



测试一下:
  1. int main() {
  2.         // 这里是 const 的权限缩小吗?
  3.         // 不是,这里是 自定义之间类型转换,从 iterator类 转换为 const_iterator类,其本质是在const_iterator类中重载一个参数类型为 iterator 的构造函数
  4.         // 因此,自定义类型 可以转换成 自定义类型,内置类型也可以转换为自定义类型:本质都是自定义类型重载了相关联的构造函数
  5.         my::list<int> lt1 = { 1, 2, 3, 4 };
  6.         my::list<int>::const_iterator it = lt1.begin();
  7.         while (it != lt1.end()) {
  8.                 cout << *it << '\n';
  9.                 it++;
  10.         }
  11.         return 0;
  12. }
复制代码
效果:




2.7 注意事项

逼迫类型转换关闭或挂起了正常的类型查抄,每次使用逼迫类型转换前,程序员应该仔细思量是否另有其他不同的方法到达同一目的,如果非逼迫类型转换不可,则应限定逼迫转换值的作用域,以减少发生错误的机会。
猛烈建议:制止使用逼迫类型转换



3.RTTI(了解)

RTTI:Run-time Type identification的简称,即:运行时类型辨认。

  • typeid运算符
  • dynamic_cast运算符
  • decltype
这几个运算符为什么也是运行时辨认?
实在他们都是编译时就已经得出效果,这里的运行时辨认是语法语义上,在运行时判断类型或做选择,而底层实在就是编译时辨认



4. 常见口试题


  • C++中的 四种 类型转化分别是
  • 说说 四种 类型转化的应用场景。
这些问题上面文章都有解说了




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊雷无声

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表