C++、类型转换检查

在过去的 C++ 标准中,编译器可隐式或显式地对某些类型进行转换,例如将具有不同位宽的整型变量进行隐式转换,或者通过在变量前面加上具体类型来进行显式的强制类型转换,比如改变指针的类型对同一块内存进行不同的解读等等。但是这种转换方式在代码上比较隐晦,类型转换本身也具有较大的风险,在编码的过程中容易留下较多隐患,且不便调试。

C++11 提供了四种显式的类型转换检查,包括 static_cast, const_cast, reinterpret_castdynamic_cast,使用的方式类似于函数,即 xxx_cast<dstType>(src),可用于判断两种类型之间的转换是否有效。前三者作用于编译阶段,后者主要作用于运行阶段,即 RTTI (Run-Time Type Identification)。类型转换检查在对已有的类型转换方式进行细分的基础上,进一步拓展了类型转换的作用范围。通过函数调用的方式,可明确地让开发者知道类型转换所在的位置,以及自己转换的行为是什么,可能造成什么样的后果,避免更多潜在的风险。

1. static_cast

static_cast 必须发生在两种相互关联的数据类型之间。

  1. 对于数值类型如 int, float 等,都是可以进行隐式转换的,因此使用静态转换也是有效的。
  2. 对于指针类型,除了空指针或者 void 类型指针与任意类型指针的静态转换是有效的,其他一律无效(类继承除外)。这是因为不同类型的指针所能管理的内存结构不同,进行指针类型转换很容易造成非法内存存取。
  3. 对于两个具有继承关系的类类型,派生类对象或指针到基类对象或指针的静态转换都是有效的。然而,要将基类对象静态转换到派生类对象,则需要派生类具有以基类对象为输入的拷贝函数,否则这种转换是无效的。
  4. 注意,将基类指针或引用静态转换为派生类指针或引用也是有效的,即便派生类定义了新的数据成员而拥有额外的内存结构,且无论基类有无虚函数。这时相当于把基类对象的内存首地址解释为一个派生类对象的内存首地址,这样就可以以派生类的方法来访问更多的内存,所以开发者应该清醒地知道自己在干什么,避免非法的内存访问。由于虚表指针还是指向基类的虚表,此时派生类指针的动态类型还是基类,调用虚函数时还是调用基类的版本,但对于同名非虚函数或者数据成员,其调用由静态类型决定,所以调用的是派生类的版本。

静态转换一般用于将已指派到基类指针的派生类对象重新指派到派生类指针上,而无需拷贝原始的派生类指针。

      A *a = new A;
      B *b = static_cast<B *>(a);   // ok,此时b调用虚函数调用的还是基类A的版本,但b可以访问派生类的内存范围
      a = new B;
      b = static_cast<B *>(a);      // ok,将派生类对象重新指派到派生类指针上

2. const_cast

const_cast 只能进行指针或引用的转换,不能用于对象实例的转换,通常用于解除常量对象的 const 属性。注意,对于字面常量比如 int 数值等,const 的作用与 define 类似,在编译阶段就已经进行变量替换,所以以下代码中尽管通过指针修改了 a 的值,但在编译阶段所有 a 的位置都已经使用具体数值代替。对于复杂类型比如类类型等,由于编译阶段无法进行字面替换,所以通过 const_cast 之后使用的是新的值。

      const int a = 1;
      int *b = const_cast<int *>(&a);
      *b = 2;
      cout << a << ' ' << *b << endl; // 1 2

3. reinterpret_cast

reinterpret_cast 可以用于对某块内存进行不同类型的解读,或者进行指针与整型数据的转换,与过去标准中直接在变量前面显式地加上需要转换的指针类型类似。这种转换可以灵活地操纵内存,但也带来十分明显的内存非法访问的风险,因此要格外小心。

      int x = 1;
      char *y1 = reinterpret_cast<char *>(&x); 
      char y2 = reinterpret_cast<char &>(x);    // *y1 == y2 == (x & 0xff)
      int y3 = reinterpret_cast<int>(&x);       // y3 == &x

对于具有继承关系的两个类,reinterpret_caststatic_cast 的作用类似,但前者可以处理没有任何联系的两种类型,即便是从普通类型到类类型的转换亦可,因此功能更加强大。注意把两种互不关联的类型进行 reinterpret_cast 后,不能说目标指针的动态类型是源指针本身的动态类型,因为两个类之间没有继承关系,动态类型只是对于具有继承关系的类而言的。

4. dynamic_cast

dynamic_cast 主要在运行时对类型转换的有效性进行检查。当基类指针或引用转换为派生类指针或引用,或者两个无关类的指针或引用进行转换,源类型必须包含虚函数。在编译阶段,源类型和目标类型之间不需要有明确的联系,只要它们具有虚函数即可。

dynamic_cast 可以实现动态地访问非虚函数的目的,因为非虚函数调用的版本取决于指针或引用的静态类型,例如假设派生类中新定义了一个函数,当我们将派生类对象绑定到基类指针或引用上时,基类指针或引用不能访问到这个函数,这时我们可以通过 dynamic_cast 将基类指针或引用重新转换为派生类的指针或引用,从而访问该函数。

  1. 在运行期转换时,如果源指针或引用的静态类型与目标指针或引用的静态类型相同(即类型名相同),即便他们的动态类型不一样,这种转换都是有效的,毕竟这只是同一种静态类型之间的赋值而已。

  2. 当源指针与目标指针的静态类型不同时,只有当两种类型具有继承关系,且源指针的动态类型与目标指针的静态类型相同,或者源指针的动态类型能够自动转换为目标指针的静态类型时(即派生类到基类的自动转换),这种转换才是有效的。也就是说,只有当基类指针或引用的动态类型是派生类类型时,从基类指针或引用到派生类指针或引用的 dynamic_cast 才是有效的,而如果基类指针本身指向的就是基类对象,则其无法通过 dynamic_cast 转换到派生类指针上。

当转换有效时,dynamic_cast 会返回相应的指针或引用;但转换无效时,对于指针会返回NULL,对于引用则抛出 bad_cast 异常。因此,dynamic_cast 是一种相对安全的类型转换,当继承关系较为复杂时,我们可以尝试使用这种方式来自动地将已经指派到基类指针或引用的派生类对象安全地重新指派到派生类指针或引用上,如果无法转换我们也可以根据返回的结果或异常来得知。

然而,通过 static_cast 我们同样也可以将动态类型为派生类的基类指针或引用重新指派回派生类指针或引用上,但 static_cast 本身并不会考虑基类指针或引用的动态类型是否真的为派生类类型,因此相比于 dynamic_cast 具有一定的风险。但是,dynamic_cast 作用在运行期,且需要较大的时间开销,效率较低,而且我们同样需要精心考虑转换失败时的应对策略。所以,原则上开发者在编程时就应该清楚地知道各个类之间的关系,尽量安全地采用静态转换方式。

      // 假设 B 继承自 A,而 C 是独立的类,三者都有虚函数
      A a;  B b;  C c;
      A &a1 = static_cast<A &>(b);        // ok, 派生类到基类的引用,安全
      B &b1 = static_cast<B &>(a);        // ok, 将基类对象解释为派生类引用,有风险
      B &b2 = reinterpret_cast<B &>(c);   // ok, 将无关类对象解释为 B 类引用,有风险

      B &b3 = dynamic_cast<B &>(a1);      // ok, 因为 a1 的动态类型是派生类
      B &b4 = dynamic_cast<B &>(a);       // 抛出异常,因为 a 的动态类型是基类
      B &b5 = dynamic_cast<B &>(b1);      // ok, 尽管 b1 的动态类型是基类,但它的静态类型和目标引用的类型相同
      B &b6 = dynamic_cast<B &>(b2);      // ok, 尽管 b2 实际的内存实际来自 c,但它的静态类型和目标引用的类型相同

猜你喜欢

转载自blog.csdn.net/qq_33552519/article/details/128863023
今日推荐