(C++ 成长记录) —— C++强制类型转换运算符(static_cast、reinterpret_cast、const_cast和dynamic_cast)

(C++ 成长记录) —— C++强制类型转换运算符(static_cast、reinterpret_cast、const_cast和dynamic_cast)

C++强制类型转换运算符

附录

专业词汇百科

参考文献

声明: 本文有部分内容直接来自参考文献,侵删。

概述

     我在日常的开发过程中,经常会用到一些类型转换的函数,主要是子类到父类的上行转换或者是父类到子类的下行转换等等,还有一些强制数据类型的转换,这些都是我们日常开发工作中经常遇到的一些常用的内容,那么今天就突发奇想,想要来总结一下关于C++的一些类型转换符的使用的技巧和一些案例,希望能够帮助到他人,也是对自己的知识点的一种记录和回顾。

类型转换

概念介绍

     最初级的原始C样式的类型转换,是将类型名作为强制类型转换运算符的做法是C语言的老式做法,C++ 为保持兼容而予以保留。

C++ 引入了四种功能不同的强制类型转换运算符以进行强制类型转换:static_castreinterpret_castconst_castdynamic_cast

     强制类型转换是有一定风险的,有的转换并不一定安全,例如把int整形数值转换成一个指针类型,把基类指针转换成派生类指针的时候有可能会失败,把一种函数指针转换成另一种函数指针可能会出现不匹配的情况,把常量指针转换成非常量指针可能会导致原始常量被破坏等等并不是很安全的。C++ 引入新的强制类型转换机制,主要是为了克服C语言强制类型转换的以下三个缺点。

  1. 没有从形式上体现转换功能和风险的不同。
  2. 将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。
  3. 难以在程序中寻找到底什么地方进行了强制类型转换。

     例如,将 int 强制转换成 double 是没有风险的,这是将一种简单类型转换为复杂类型,但是将复杂类型转换为简单类型,就有丢失精度的风险了,例如你再想把double转回int,那么是存在精度丢失风险的,而将常量指针转换成非常量指针,将基类指针转换成派生类指针都是风险很高的操作,而且后两者带来的风险不同(即可能引发不同种类的错误),C语言的强制类型转换形式对这些不同并不加以区分。

     很多时候,我们使用强制类型转换配合Assert宏使用,这样可以验证是否是由强制类型转换引发的程序崩溃的问题,因为强制类型转换是经常会引起程序崩溃的一种诱因,那么对于一个程序来讲,如果我们一直习惯于采用C语言的默认的形式的强制类型转换,就是用(int)xxx, 如果遇到了怀疑是强制类型转换导致的bug或者程序崩溃,你想要检查某个转换的正确性,在程序里都不好定位,不知道是哪个转换出的问题,不太好搜索到对应的位置。

     如果转而去采用 C++ 的方式,那么只用查找_cast字符串就可以了。甚至可以根据错误的类型,有针对性地专门查找某一种强制类型转换。例如,怀疑一个错误可能是由于使用了 reinterpret_cast导致的,就可以只查找reinterpret_cast字符串。

     C++ 强制类型转换运算符的用法如下:

强制类型转换运算符 <要转换到的类型> (待转换的表达式)

     举例而言:

double d = static_cast <double> (3 * 5);  //将 3*5 的值转换成实数
float f = static_cast<float> (2 * 3);

class Base 
{
public:
    Base();
    void funcBase();
};

class Child : public Base
{
public:
    Child();
    void funcChild();
}

int main()
{
    Child a;
    Base* b = static_cast<Base*>(b);
    // b->funcChild();  // error b can't use the child method.
    a->funcBase();      // ok
    a->funcChild();     // ok
    Base b1;
    Child c = static_cast<Child*>(b1);
    c->funcBase();      // ok
    // c->funcChild();      // error c can't use the child method. 
}

异同对比

static_cast

static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 Tintint* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。

static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。

static_cast 用法示例如下:

///////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c)2021, Tom Zhao personal. ("TZOpenTools")
// This software is a personal tools project by Tom Zhao.
// Description:
///////////////////////////////////////////////////////////////////////////////////////////

#include <iostream>

using namespace std;

class A
{
public:
    operator int() { return 1; }
    operator char*() { return NULL; }
};

int main()
{
    A a;
    int n;
    const char* p = "This is a str for static_cast";
    n = static_cast <int> (3.14);       // n 的值变为 3
    n = static_cast <int> (a);              // 调用 a.operator int, n 的值变为 1
    p = static_cast <char*> (a);            // 调用 a.operator char*,p 的值变为 NULL
    // n = static_cast <int> (p);           // 编译错误,static_cast不能将指针转换成整型
    // p = static_cast <char*> (n);     // 编译错误,static_cast 不能将整型转换成指针
    return 0;
}

reinterpret_cast

reinterpret_cast 用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换,reinterpret_cast 转换时,执行的过程是逐个比特复制的操作。

     这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。例如,程序员执意要把一个 int* 指针、函数指针或其他类型的指针转换成 string* 类型的指针也是可以的,至于以后用转换后的指针调用 string 类的成员函数引发错误,程序员也只能自行承担查找错误的烦琐工作:(C++ 标准不允许将函数指针转换成对象指针,但有些编译器,如 Visual Studio 2010,则支持这种转换)。

reinterpret_cast 用法示例如下:

///////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c)2021, Tom Zhao personal. ("TZOpenTools")
// This software is a personal tools project by Tom Zhao.
// Description:
///////////////////////////////////////////////////////////////////////////////////////////

#include <iostream>

using namespace std;

class A
{
public:
    int i;
    int j;
    A(int n) :i(n), j(n) { }
};

int main()
{
    A a(100);
    int &r = reinterpret_cast<int&>(a);             // 强行让 r 引用 a
    r = 200;                                                            // 把 a.i 变成了 200
    cout << a.i << "," << a.j << endl;                  // 输出 200,100
    int n = 300;
    A *pa = reinterpret_cast<A*> (&n);              // 强行让 pa 指向 n
    pa->i = 400;                                                    // n 变成 400
    pa->j = 500;                                                    // 此条语句不安全,很可能导致程序崩溃
    cout << n << endl;                                          // 输出 400
    long long la = 0x12345678abcdLL;
    pa = reinterpret_cast<A*>(la);                      // la太长,只取低32位0x5678abcd拷贝给pa
    unsigned int u = reinterpret_cast<unsigned int>(pa);    // pa逐个比特拷贝到u
    cout << hex << u << endl;                               // 输出 5678abcd
    typedef void(*PF1) (int);
    typedef int(*PF2) (int, char *);
    PF1 pf1 = nullptr; 
    PF2 pf2;
    pf2 = reinterpret_cast<PF2>(pf1);                   // 两个不同类型的函数指针之间可以互相转换
}

     运行结果如下:

     在编译的过程中,就会有强行转换的截断提示了。所以不建议强行转换某些类型。

     第 19 行的代码不安全,因为在编译器看来,pa->j 的存放位置就是 n 后面的 4 个字节。 本条语句会向这 4 个字节中写入 500。但这 4 个字节不知道是用来存放什么的,贸然向其中写入可能会导致程序错误甚至崩溃。

     上面程序中的各种转换都没有实际意义,只是为了演示 reinteipret_cast 的用法而已。在编写黑客程序、病毒或反病毒程序时,也许会用到这样怪异的转换。

reinterpret_cast 体现了 C++ 语言的设计思想:用户可以做任何操作,但要为自己的行为负责。

const_cast

     const_cast 运算符仅用于进行去除 const 属性的转换,它也是四个强制类型转换运算符中唯一能够去除 const 属性的运算符。

     将 const 引用转换为同类型的非 const 引用,将 const 指针转换为同类型的非 const 指针时可以使用 const_cast 运算符。例如:

const string s = "Inception";
string& p = const_cast <string&> (s);
string* ps = const_cast <string*> (&s);  // &s 的类型是 const string*
///////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c)2021, Tom Zhao personal. ("TZOpenTools")
// This software is a personal tools project by Tom Zhao.
// Description:
///////////////////////////////////////////////////////////////////////////////////////////

#include <iostream>
#include <string>

using namespace std;

class A
{
public:
    const double i = 5.0;
    const int j = 10;
    const string m_s = "Test String.";
    float f = 2.0f;
};

int main()
{
    A a;
    cout << a.i << '\t' << a.j << '\t' << a.m_s << endl;
    string& p_str = const_cast<string&> (a.m_s);
    p_str = "New Test String!.";
    cout << a.i << '\t' << a.j << '\t' << a.m_s << endl;
    cout << p_str << endl;
    string* ps = const_cast<string*>(&a.m_s);
    *ps = "Point Test String";
    cout << a.i << '\t' << a.j << '\t' << a.m_s << endl;
    cout << ps << '\t' << *ps << endl;
    // int& p_i = const_cast<int&>(a.i); //  不允许修改基础类型的const,只能改类型限定符
    // p_i = 200;

    const A ca;
    A& pa = const_cast<A&>(ca);
    pa.f = 30.0f;
    cout << ca.i << '\t' << ca.j << '\t' << ca.m_s << '\t' << ca.f << endl;
    cout << pa.i << '\t' << pa.j << '\t' << pa.m_s << '\t' << pa.f << endl;
}

dynamic_cast

     用 reinterpret_cast 可以将多态基类(包含虚函数的基类)的指针强制转换为派生类的指针,但是这种转换不检查安全性,即不检查转换后的指针是否确实指向一个派生类对象。dynamic_cast专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 NULL 指针。

dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。

dynamic_cast 示例程序如下:

///////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c)2021, Tom Zhao personal. ("TZOpenTools")
// This software is a personal tools project by Tom Zhao.
// Description:
///////////////////////////////////////////////////////////////////////////////////////////

#include <iostream>
#include <string>

using namespace std;

class Base
{  
//有虚函数,因此是多态基类
public:
    virtual ~Base() {}
};

class Derived : public Base { };

int main()
{
    Base b;
    Derived d;
    Derived* pd;
    pd = reinterpret_cast <Derived*> (&b);
    if (pd == NULL)
        //此处pd不会为 NULL。reinterpret_cast不检查安全性,总是进行转换
        cout << "unsafe reinterpret_cast" << endl; //不会执行
    pd = dynamic_cast <Derived*> (&b);
    if (pd == NULL)  //结果会是NULL,因为 &b 不指向派生类对象,此转换不安全
        cout << "unsafe dynamic_cast1" << endl;  //会执行
    pd = dynamic_cast <Derived*> (&d);  //安全的转换
    if (pd == NULL)  //此处 pd 不会为 NULL
        cout << "unsafe dynamic_cast2" << endl;  //不会执行
    return 0;
}

     第 27 行,通过判断 pd 的值是否为 NULL,就能知道第 26 行进行的转换是否是安全的。第 34 行同理。

     如果上面的程序中出现了下面的语句:

Derived & r = dynamic_cast <Derived &> (b);

那该如何判断该转换是否安全呢?

    不存在空引用,因此不能通过返回值来判断转换是否安全。C++ 的解决办法是:dynamic_cast 在进行引用的强制转换时,如果发现转换不安全,就会拋出一个异常,通过处理异常,就能发现不安全的转换。

小结

     下面的观点主要来自我个人的编码习惯从而得出的结论,仅供参考,可以讨论学习,毕竟我也很菜。 1. 能够不要使用类型转换尽量不要用类型转换,尽可能的只用原本的子类类型。 2. 需要使用类型转换的时候,可以尽量避免使用 reinterpret_cast 因为他过于自由,安全性太差。 3. 能够不去强行把 const_cast 转换的情况就尽量不要去破坏常量的属性。 4. 能用 static_cast 的地方,不要去使用 dynamic_cast

个人格言

用心去感受你自己需要坚持的生活,未来慢慢会给你答案的。

发布于 2021-04-27 15:49