返回

C++ 中的类型转换

对 C++ 中常用的几种类型转换进行整理。

简介

这篇博客主要对 C++ 中的四种类型转换进行整理:static_castconst_castreinterpret_castconst_cast 的用法和一些使用场景进行整理。

static_cast

static_cast的引用场景比较转换,根据 CppReference,有以下情况可以执行静态类型转换从表达式 eT

普通类型

  1. 对表达式 e 可以由隐式类型转换到 T
  2. 如果从 Te 的类型的标准转换序列存在,静态类型转换可以进行该隐式类型转换序列的逆过程。该转换不包括:左值到右值,数组到指针,函数到指针的转换
  3. 如果从 eT 的转换涉及左值到右值,数组到指针,函数到指针的转换,可以通过静态类型转换显式进行
  4. 如果 Tvoid 类型,静态类型转换会将表达式 e 的值丢弃

一些例子如下所示:

#include <string>

class Base {
public:
    Base(int a) {}
};

int main() {
    // static_cast does a implicit conversion -- 1
    int a = 10;
    Base b = static_cast<Base>(a + 1);

    // static_cast does a inverse of standard conversions through qualification check -- 2
    std::string str = "str";
    char* c = &str[0];
    const char* const* d = &c;
    const char* const* const e = static_cast<const char* const* const>(d);

    // static_cast does a array-to-pointer -- 3
    int f[5] = {1, 2, 3, 4, 5};
    int* g = static_cast<int*>(f);

    // discard value of a only for suppressing unused warning -- 4
    int h;
    static_cast<void>(h);

    return 0;
}
  • 枚举类型
    1. scoped 的枚举可以转为整型或浮点型
    2. 枚举类型或者整型的值可以转换成任何完整的枚举类型

一些例子如下所示:

#include <iostream>

enum class A { aa = 1, bb, cc };

enum B { ee /* = 0*/, ff };

enum C { gg, hh };

int main() {
    // static_cast does a scoped enum to int/double conversion
    A a = A::aa;
    int b = static_cast<int>(a);
    double c = static_cast<double>(a);
    std::cout << b << ", " << c << std::endl;  // 1, 1

    // static_cast does a enum/int conversion
    int d = 0;
    B e = static_cast<B>(d);  // 0 -> B::ee
    C f = static_cast<C>(e);  // B::ee -> C::gg

    return 0;
}
  • 引用类型

    1. 如果类型 T 是右值引用类型,且 e 是一个对象,静态类型转换可以将其转换成对同类型的将亡值,例子为 std::move,见 C++ 中移动语义和完美转发的联系和区别
  • 不安全)指针类型,包含继承关系之间的转换

    1. 如果类型是 T 的引用或指针,且 e 是其基类 B 的左值或者纯右值指针,可以进行下转换(基类转派生类),不保证该内存位置真的有派生类变量
    2. 一个指向派生类 D 成员变量的指针可以通过上转换为它的基类 B 的指针,不确保该成员在基类存在
    3. 一个指向 void 的指针可以转换成任何对象类型的指针

一些不安全的使用例子如下所示:

#include <iostream>
class Base {};

class Derived : public Base {
public:
    int a = 1.0;
};

int main() {
    Base a;
    // static_cast does a down casting in wrong situation
    Base* b = &a;
    Derived* c = static_cast<Derived*>(b);
    Derived& d = static_cast<Derived&>(a);

    // will run, but is random value
    std::cout << d.a << std::endl;

    // static_cast can cast a void* to pointer of any type
    void* e = static_cast<void*>(c);
    Derived* f = static_cast<Derived*>(e);

    // upcast of a pointer to member
    int Derived::*pa = &Derived::a;
    a.*static_cast<int Base::*>(pa);
    return 0;
}

可以看到涉及到继承关系之间的转换,静态类型转换并不安全,因为静态类型只进行编译期检查,如果通过编译期检查,在运行时可能会导致不明确的行为。因此对于这一类转换最好使用动态类型转换。

dynamic_cast

dynamic_cast 可以实现指针或引用在继承关系上向上,向下或者侧向转换。根据 CppReference,有以下情况可以执行动态类型转换:

  1. 如果表达式 eT 类型完全相同,或 T 的常量性比 e 更高,可以进行 eT 的动态类型转换(static_cast 也可以)
  2. 如果表达式 e 是零指针值(nullptr),可以转换为 T 的零指针
  3. 如果 T 为指向基类的指针或引用,表达式 e 是指向派生类的指针和引用,且基类唯一。动态类型转换会将其该派生类对象中按基类成分转换为基类对象的指针(隐式类型转换和 static_cast 可以进行同样转换)
  4. 如果表达式 e 是多态类型,而 T 是指向 void 的指针,动态类型转换会将其转换为派生关系最靠下(most derived) 的指针
  5. 如果表达式 e 是多态基类的指针或引用,而 T 是指向派生类的指针,此时进行动态类型转换会进行运行时检查:
    1. 检查 e 指代的对象的派生层级,如果能够从 e 中得到 T 类型,则该转换可以顺利进行 (downcast)
    2. 如果 e 本身类型不是派生层级某一层类型,但其指代的对象是派生层级中最靠下的类型(most derived),则可以转换为 T(sidecast, 见以下例子)
  6. 如果在构造函数或者析构函数直接或间接使用动态类型转换,如果 e 为正在构建的对象 this,则该对象视为派生关系最靠下的的对象,此时如果 T 不是该对象本身的类型或其基类,该行为是未定义的。

以下是一些例子:

#include <iostream>
class Base {
public:
    virtual void func() {}
};

class MiddleA : public virtual Base {};

class MiddleB : public virtual Base {};

/**
 *       Base
 *      /    \
 *     /      \
 * MiddleA  MiddleB
 *    \       /
 *     \     /
 *     Derived
 */
class CrossDerived : public MiddleA, public MiddleB {};

class Derived : public Base {};

int main() {
    CrossDerived a;
    MiddleB& b = a;

    // down cast
    CrossDerived& c = dynamic_cast<CrossDerived&>(b);
    // side cast
    MiddleA& d = dynamic_cast<MiddleA&>(b);

    // most common use case
    Base* e = new Derived();
    Derived* f = dynamic_cast<Derived*>(e);

    return 0;
}

dynamic_cast 主要用在动态基类指针向派生类指针的转换,由于项目中一般很少会用到菱形继承,side-cast 的情况比较少见。

reinterpret_cast

reinterpret_cast 可以进行任何指针变量之间的转换,一般来说较为少用。使用场景可以有某些按位传输的信息流时,进行数据的打包和解压时使用。如以下例子所示:我们将某个数据类型通过 reinterpret_cast 转换为一个通用的数据类型,再经由某种方式获得数据之后,重新转换为我们需要的数据类型。

#include <iostream>

struct Data {
    char a;  // 1 bytes
    char b;  // 1 bytes
    char c;  // 1 bytes
    char d;  // 1 bytes
};

struct PackedData {
    int data;  // 4 bytes
};

int main() {
    // intialized data
    Data d;

    // packed it and send it for some data stream
    PackedData* pd = reinterpret_cast<PackedData*>(&d);

    // data assign, memory layout for pd->data would be '61 62 63 64' or '64 63 62 61' depending on machine
    pd->data = ('a' << 3 * 8) | ('b' << 2 * 8) | ('c' << 1 * 8) | 'd';
    std::cout << pd->data << std::endl;

    // optional, changes have been made on d
    Data* d2 = reinterpret_cast<Data*>(pd);
    std::cout << d2->a << d2->b << d2->c << d2->d << std::endl;

    return 0;
}

const_cast

const_cast 可以用来移除类型的常量性,一般情况下不建议使用。

#include <iostream>
 
int main() 
{
    int i = 3;                 // i is not declared const
    const int& rci = i; 
    const_cast<int&>(rci) = 4; // OK: modifies i
}

参考

Built with Hugo
Theme Stack designed by Jimmy