1. 为什么需要类型转换

类型转换是将一个类型的对象转换为其他对象。这样做的目的是为了在不同类型之间传递信息,或者在运算过程中统一数据类型,避免错误。

C 语言和 C++都是强类型语言,如果 = 两边的类型不同或形参接收的实参类型不匹配,以及接收返回值的类型和实际返回值类型不一样,就需要通过类型转换统一数据类型。

2. C 语言的类型转换

C 语言中有两种类型转换:隐式类型转换和显式类型转换。

2.1 隐式类型转换

隐式类型转换,也称自动类型转换,是指不需要用户干预,编译器默认进行的类型转换行为,使得两个变量的数据类型一致,才能进行相关的计算。它通常发生在赋值转换和运算转换两种情况。

  • 赋值转换:将一种类型的数据赋值给另外一种类型的变量时,发生隐式类型转换。例如:
int x = 1.23; // 1.23 是 double 类型,先隐式转换为 int
  • 运算转换:C 语言中不同类型的数据需要转换成同一类型,才可以进行计算。字符型、整型、浮点型之间的变量通过隐式类型转换,可以进行混合运算(不是所有数据类型之间都可以隐式转换)。

总的来说,C 语言的隐式类型转换发生在意义相近的类型之间,例如整型和浮点型家族,它们都是表示数字的规模。将一个数字转化为一个对象这样的操作,显然不是编译器应该自动做的。

2.2 显式类型转换

显式类型转换则是我们人为地进行数据类型的转换,这里可以理解为是一种强制的类型的转换。这种转换将不再遵守上面的转换规则,而是按照我们人为地标明的类型进行转换。就是在我们需要指定类型的变量前加上数据类型,并用圆括号包裹。例如:(int)a(float)b(long)c 等。

2.3 特点

  • 优点:在于它能够帮助我们解决一些特殊情况下的问题。例如,当我们需要将一个浮点数赋值给一个整型变量时,隐式类型转换会自动将浮点数转换为整型,使得程序能够正常运行。显式类型转换也可以用来解决一些特殊情况下的问题。

  • 缺点:隐式类型转换可能会导致数据失真(精度降低),因此不一定是安全的。

常见由隐式类型转换造成的错误:

// 顺序表插入函数
void Insert(size_t pos, int a)
{
    size_t _size = 5; // 假设长度是 5
    // ... 
    int end = _size - 1; // 发生隐式类型转换
    while(end >= pos)
    {
        // ... 数据挪动等操作
        --end;
    }
}

通常,我们用无符号整数类型 size_t 接收长度、位置等非负参数。

因为担心 end 如果是无符号整数,就会导致后面 --end 语句在 end = 0 时再减 1 后变成一个很大的整数,导致死循环,所以特地将 end 的类型设置为 int 类型。但是 int end = _size - 1 中,end 会被隐式类型转换为 size_t,依然导致死循环。

虽然可以再用强制类型转换解决,int end = (int)_size - 1,但这样补救的方式并不符合直觉,且常常容易因为类似的问题而出错。

复习:在此之前,所有的类型转换都会产生临时变量。例如:

int main()
{
    int ia = 0;
    double da = ia;
    //      ↑ __□__ ↓
    const double & rd1 = ia;
    //              ↑ __□__ ↓
    const double& rd2 = static_cast<double>(ia);
    //             ↑ ____ __□__ ____ ____ _______↓
    return 0;
}

3. C++的类型转换

C++兼容 C 语言,所以 C 语言的类型转换在 C++中依然可用。

C++类型转换分为显式类型转换和隐式类型转换。隐式类型转换由编译器自动完成,这里只讨论显式类型转换。C++ 引入了四种功能不同的强制类型转换运算符以进行强制类型转换。

  • const_cast: 运算符用于修改类型的 const / volatile 属性。除了 const 或 volatile 属性之外,目标类型必须与源类型相同。这种类型的转换主要是用来操作所传对象的 const 属性,可以加上 const 属性,也可以去掉 const 属性
  • dynamic_cast: 在运行时执行转换,验证转换的有效性。如果转换未执行,则转换失败,表达式则被判定为 null。

3.1 static_cast

进行普通数据类型转换(如 int m=static_cast(3.14)),用于两个相关类型之间的转换。

int main()
{
    double d = 1.23;
    int a = static_cast<int>(d);
    cout << a << endl;

    int* p = &a;
    //int address = static_cast <int>(p); // error
    return 0;
}

输出:

1

3.2 reinterpret_cast

可以将一个指针类型转换为另一个指针类型,即使转换前后的数据类型不同。它不检查指针类型和指针所指向的数据是否相同。它的字面意思是“重新诠释”,也就是说,它只是改变了编译器对指针的理解方式,而不改变其二进制表示。很像 C 的一般类型转换操作,用于两个不相关类型之间的转换。

int main()
{
    int a = 1;
    int* p = &a;

    int adress = reinterpret_cast<int>(p);

    return 0;
}

注意:在检查严格的编译器(Clion、gcc)中这段代码无法编译通过,在 VS2019 中是合法的。

但是这段代码有一些问题,比如:

  • int* p = &a; 这里 p 是一个指向 int 类型的指针,它存储了变量 a 的地址。
  • int adress = reinterpret_cast<int>(p); 这里 adress 是一个 int 类型的变量,它用 reinterpret_cast 强制转换了 p 的值为 int 类型。这样做可能会导致数据丢失或未定义行为。

reinterpret_cast 有点不讲道理,例如下面这段代码中,reinterpret_cast 将有参数有返回值的函数指针转换成了无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数。

typedef void(*FUNC)();
int func(int i)
{
	cout << "func: " << i << endl;
	return 0;
}
int main()
{
	FUNC F = reinterpret_cast<FUNC>(func);
	F();

	return 0;
}

输出:

func: 13504547

输出的结果是不确定的,只要删除生成的二进制文件重新编译,结果就会改变。VS 编译器可以这么做:

img

造成结果不确定的原因是:
因为它触发了 C++中的一种叫做 undefined behavior 的情况。undefined behavior 是指 C++标准没有定义或者允许编译器自由定义的程序行为。当程序遇到 undefined behavior 时,它可能会产生任何结果,包括正确的结果、错误的结果、异常、崩溃等。这些结果可能会随着平台、编译器、优化选项等因素而变化。

在这段代码中,用 reinterpret_cast 将 func 转换为 FUNC 类型并调用 F()就是一种 undefined behavior,因为 func 和 F 的类型不匹配,而 C++标准没有规定这种情况下应该发生什么。所以,它的结果是不确定的,并且可能会导致程序出错或者崩溃。

reinterpret_cast 可以将任何指针类型转换为任何其他指针类型,但是这种转换是不安全的,因为它不检查转换前后的指针是否兼容。如果转换后的指针被用于调用函数,那么它必须与函数的实际类型匹配,否则可能会导致未定义行为。

在这段代码中,func 函数的实际类型是 int(int),也就是接受一个 int 参数并返回一个 int 值的函数。FUNC 类型是 void(),也就是没有参数也没有返回值的函数指针。用 reinterpret_cast 将 func 转换为 FUNC 类型并赋值给 F 变量,相当于告诉编译器把 F 当作一个 void()类型的函数指针来处理。但是这并不改变 func 函数的实际类型和地址。

当调用 F()时,相当于调用 func(0),因为没有传递任何参数。这个调用可能会成功地打印出 “func: 0” 并返回 0,但是这只是巧合,并不保证在所有平台和编译器上都能正常工作。因为 F 和 func 的类型不匹配,所以调用 F()可能会破坏栈平衡或者引发异常。

总之,reinterpret_cast 可以将有参数有返回值的函数指针转换成无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数,但是这种做法是非常危险和不推荐的。

3.3 const_cast

去除复合类型中 const 和 volatile 属性(没有真正去除)。变量本身的 const 属性是不能去除的,要想修改变量的值,一般是去除指针(或引用)的 const 属性,再进行间接修改。

用法:const_cast<type>(expression)

假设你有一个 const int 类型的变量 a,你想通过一个 int 类型的指针 p 来修改它的值,你可以这样写:

int main()
{
	const int a = 10; // 声明一个 const int 类型的变量 a
	int* p = const_cast<int*>(&a); // 使用 const_cast 去除 a 的地址的 const 属性,赋值给指针 p
	*p = 20; // 通过指针 p 修改 a 的值

	cout << a << endl;
	cout << *p << endl;
	return 0;
}

输出:

10
20

这样就可以实现对 const 变量的间接修改。但是要注意,如果 a 是一个真正的常量(例如用#define 定义),那么这样做可能会导致未定义行为。所以使用 const_cast 要谨慎,并且尽量避免修改原本不应该被修改的数据。

造成未定义行为的原因:

这是因为 C++标准没有明确规定对一个真正的常量进行修改的行为,所以编译器可以自由地处理这种情况。有可能编译器会把常量放在只读内存中,如果你试图修改它,就会触发内存保护错误。或者编译器会对常量进行优化,直接用它的值替换所有的引用,那么你修改的只是一个临时变量,而不是原来的常量。总之,这种操作是不安全和不可预测的,所以应该避免。

为什么输出的结果不同?

这是因为使用 const_cast 去除了 a 的 const 属性,然后修改了它的值,这是一种未定义行为。编译器可能会对 a 进行优化,直接用它的值 10 替换所有的引用,而不是从内存中读取。

也就是说,编译器认为 const 变量是不会被修改的,因此会将 const 变量存放到寄存器中(这是一种优化),但是我们通过指针 p 修改的是内存中的值。所以当输出 a 时,是从 寄存器 中读取的,也就是未修改之前的 10;而当输出*p 时,读取的是 内存 中被修改后的 20。这种操作是不安全和不可预测的,所以应该避免。

如果想避免编译器的优化:

使用 volatile 关键字声明 const 变量,告诉编译器不要优化。如果将上面的代码改成:

volatile const int a = 10;

输出:

20
20

volatile 关键字是一种类型修饰符,用它声明的变量表示可以被多线程或外部因素修改。编译器在遇到 volatile 变量时,不会对它进行优化,而是每次都从 内存 中读取它的值。这样可以保证变量的可见性和一致性。

volatile 关键字可以用于以下类型的变量:

  • 引用类型
  • 指针类型(在不安全的上下文中)
  • 简单类型,如 sbyte, byte, short, ushort, int, uint, char, float 等
  • 枚举类型
  • 带有一个简单类型字段的结构体

3.4 dynamic_cast

dynamic_cast 作用是将一个父类对象指针(或引用)转换为子类指针(或引用)。它的格式是:

dynamic_cast<type_id> (expression)

其中:

  • type_id:类的指针、类的引用或 void*;
  • expression:对应的指针或引用。

它会在 运行时识别 根据父类指针所指向的对象的真实类型来判断是否可以转换:

  • 转换成功:返回目标类型的值;
  • 转换失败且目标类型是指针类型:返回空指针;
  • 转换失败且目标类型是引用类型:抛出一个 std:: bad_cast 异常。

转型

dynamic_cast 它可以在 类层次结构 中进行向上和向下转型:

  • 向上转型(Up Cast)是将子类指针或引用转换为父类指针或引用,这是一种隐式转换,是语法天然支持的,也就是切割/切片(slice)不需要使用 dynamic_cast。
  • 向下转型(Down Cast)是将父类指针或引用转换为子类指针或引用,这是一种显式转换,需要使用 dynamic_cast。dynamic_cast 会检查运行时类型信息(RTTI):
    • 父类指针或引用指向子类对象:转换成功;
    • 父类指针或引用指向父类对象:转换失败,会返回空指针或抛出异常。

也就是说,向下转型的对象不是真正的子类对象,就会转型失败。 是因为向下转型时,需要保证对象的实际类型和目标类型一致或者有继承关系。如果对象的实际类型和目标类型不匹配,那么就会发生类型转换异常。

例如,如果你有一个父类 Fruits 和两个子类 Apple 和 Banana,那么你可以把一个 Apple 对象向下转型为 Fruits 或者 Apple,但是不能把它向下转型为 Banana。因为 Apple 和 Banana 没有继承关系,所以不能相互转换。

按理来说,父类转换成子类是不被允许的,原因是子类中可能有父类没有的成员,这就是内存非法访问,所以要通过某种手段减少这种错误。

向下转型的安全问题

C 语言风格的强制类型转换进行向下转型是不安全的,因为它可能导致运行时异常或数据丢失,也就相当于越界访问(尤其是子类有自定义变量时)。但是,C 语言的强制类型转换父子类型的对象互相转换都是合法的,编译器不会帮我们检查错误。

一个 C++的例子是:

class Pet 
{
public:
  virtual void speak() {
    cout << "Pet" << endl;
  }
};

class Cat : public Pet 
{
public:
  void speak() override {
    cout << "Meow" << endl;
  }
};

int main() 
{
  Pet* pet = new Pet(); // 创建一个 Pet 对象
  Cat* cat = (Cat*)pet; // 强制向下转型为 Cat 对象,但这么做是错误的
  cat->speak(); // 调用 Cat 的 speak 方法
  return 0;
}

输出:

Pet

果然,这段错误的代码会编译通过,但运行时会出现未定义行为。因为 Pet 指针指向的对象并不是 Cat 类型的,所以不能把它强制转换为 Cat 指针。

改进方法是:先向上转型,再向下转型。例如:

Pet* pet = new Cat(); // 创建一个 Cat 对象,并向上转型为 Pet 指针
Cat* cat = (Cat*)pet; // 强制向下转型为 Cat 指针
cat->speak(); // 调用 Cat 的 speak 方法

这段代码就可以正常运行,因为 Pet 指针实际上指向的是一个 Cat 类型的对象,所以可以把它强制转换回来。


dynamic_cast 运算符可以在运行时检查对象的实际类型是否和目标类型匹配,并返回一个合法的指针或引用。如果不匹配,它会返回空指针或者抛出异常。以此实现安全地进行向下转型。

除此之外,实现父类对象向子类对象转型,还必须要求父类具有虚函数。那是因为 dynamic_cast 是 运行时检查 类型是否是真正的子类对象,那编译器如何知道我这个子类对象是真正的子类对象呢——通过虚函数表中存储的类型信息找到真正的子类对象。

虚函数表是一种用于实现 C++多态的机制,它是一个存储了虚函数地址的数组。每个包含虚函数的类或者虚继承的子类,都有一个虚函数表。每个类对象也都有一个隐藏成员,叫做虚表指针(vptr),它指向该类的虚函数表。

当编译器编译一个类时,它会为该类生成一个虚函数表,并将该类的所有虚函数地址按照声明顺序存放在其中。如果该类有基类,那么它会先复制基类的虚函数表,然后再根据自己的情况修改其中的某些项。例如,如果子类重写了某个基类的虚函数,那么子类的虚函数表中对应位置就会替换为子类自己的版本。

当创建一个对象时,编译器会为该对象分配内存,并将其 vptr 指向对应类型的虚函数表。当通过基类指针或引用调用一个虚函数时,编译器会根据 vptr 找到正确类型的虚函数表,并从中取出相应位置的地址来执行。

例子:

// 假设有一个基类 Base 和一个派生类 Derived
class Base
{
public:
    virtual void foo()
    {}
};

class Derived : public Base
{
public:
    void bar()
    {
        cout << "void bar()" << endl;
    }
};
int main()
{
    // 假设有一个基类指针 p,指向一个派生类对象
    Base* p = new Derived();

    // 使用 dynamic_cast 将 p 转换为派生类指针 q
    Derived* q = dynamic_cast<Derived*>(p);

    // 如果转换成功,q 不为空,可以调用派生类的成员函数 bar()
    if (q)
    {
        q->bar();
    }

    return 0;
}

输出:

void bar()

这段代码演示了如何将基类指针转换为派生类指针。func 函数的参数是一个基类指针 pa,它试图用 dynamic_cast 将 pa 转换为派生类指针 pb。如果转换成功,pb 不为空,可以访问派生类的成员变量_a 和_b,并对它们进行自增操作;如果转换失败,pb 为空,不能访问任何成员变量。main 函数中分别传入了基类对象 a 和派生类对象 b 的地址给 func 函数。当传入 a 时,转换失败,输出“转换失败”;当传入 b 时,转换成功,输出“转换成功”和自增后的_a 和_b 的值。

class A
{
public:
    virtual void f()
    {}
    int _a;
};
class B : public A
{
public:
    int _b;
};
void func(A* pa) // 参数是父类指针类型
{
    B* pb = dynamic_cast<B*>(pa); // 向下转型

    if(pb)
    {
        cout << "转换成功" << endl;
        pb->_a++; // 自增继承自父类的_a
        pb->_b++; // 自增子类自己的的_b
        cout << pb->_a << ":" << pb->_b << endl;
    }
    else
    {
        cout << "转换失败" << endl;
        // 失败 pb = nullptr
        //pb->_a++; // error
        //cout << pb->_a << endl; // error
    }
}
int main()
{
    A a; // 创建基类对象
    B b; // 创建子类对象

    func(&a); // 传入基类对象地址
    func(&b); // 传入子类对象地址

    return 0;
}

输出:

转换失败
转换成功
1873164113:2

pb->_a 是随机值,是因为当转换失败时,pb 为空指针,它没有指向任何有效的内存地址。所以,试图访问 pb->_a 会导致未定义行为,可能会输出一个随机值,也可能会引发段错误。这是要检查 dynamic_cast 的返回值是否为空的原因。


补充:用下面的代码验证,当向下转型的对象是真正的子类时,指针的偏移是如何被矫正的:

class A1
{
public:
    virtual void f() {}
};

class A2
{
public:
    virtual void f() {}
};

class B : public A1, public A2
{};

int main()
{
    B b;
    A1* ptr1 = &b;
    A2* ptr2 = &b;
    cout << ptr1 << endl;
    cout << ptr2 << endl << endl;

    B* pb1 = (B*)ptr1;
    B* pb2 = (B*)ptr2;
    cout << pb1 << endl;
    cout << pb2 << endl << endl;

    B* pb3 = dynamic_cast<B*>(ptr1);
    B* pb4 = dynamic_cast<B*>(ptr2);
    cout << pb3 << endl;
    cout << pb4 << endl;

    return 0;
}

B 类继承自 A1 和 A2 两个类。在 main 函数中,创建了一个 B 类的对象 b,并将其地址分别赋给了指向 A1 和 A2 类型的指针 ptr1 和 ptr2。然后,使用强制类型转换将 ptr1 和 ptr2 转换为指向 B 类型的指针 pb1 和 pb2,并打印它们的值。接着,使用 dynamic_cast 进行类型转换并将结果赋给 pb3 和 pb4,最后输出它们的值。

这段代码主要演示了多重继承以及不同类型之间的强制类型转换和 dynamic_cast 的使用。

输出:

0x16dccb738
0x16dccb740

0x16dccb738
0x16dccb738

0x16dccb738
0x16dccb738

这些输出的地址表明,指向 A1 类型的指针 ptr1 和指向 A2 类型的指针 ptr2 分别指向了 B 类对象 b 中不同的内存地址。这是因为 B 类继承自 A1 和 A2 两个类,所以它包含了两个子对象,分别对应于 A1 和 A2。即发生了切片。

而通过强制类型转换或 dynamic_cast 将 ptr1 和 ptr2 转换为指向 B 类型的指针后,它们都指向了 B 类对象 b 的起始地址。这说明了强制类型转换和 dynamic_cast 可以用来在继承关系中进行类型转换。即类型转换(C 语言和 C++风格)能使偏移的指针被矫正。

另外:

class A1
{};

class A2
{};

class B : public A1, public A2
{};

int main()
{
    B b;
    A1* ptr1 = &b;
    A2* ptr2 = &b;
    cout << ptr1 << endl;
    cout << ptr2 << endl << endl;

    B* pb1 = (B*)ptr1;
    B* pb2 = (B*)ptr2;
    cout << pb1 << endl;
    cout << pb2 << endl << endl;

    return 0;
}

这段代码和上一段代码的主要区别在于,A1 和 A2 类中没有定义虚函数。这意味着 B 类不再包含虚函数表指针。

由于 A1 和 A2 类中没有定义虚函数,所以 B 类对象 b 不再包含虚函数表指针。因此,当我们将 B 类对象 b 的地址分别赋给指向 A1 和 A2 类型的指针 ptr1 和 ptr2 时,它们都将指向 B 类对象 b 的起始地址。

输出:

0x16f0df74b
0x16f0df74b

0x16f0df74b
0x16f0df74b

这些输出的地址表明,指向 A1 类型的指针 ptr1 和指向 A2 类型的指针 ptr2 都指向了 B 类对象 b 的起始地址。这是因为在这段代码中,A1 和 A2 类中没有定义虚函数,所以 B 类对象 b 不再包含虚函数表指针。

而通过强制类型转换将 ptr1 和 ptr2 转换为指向 B 类型的指针后,它们仍然都指向了 B 类对象 b 的起始地址。这说明了强制类型转换可以用来在继承关系中进行类型转换。

从这两个例子可以体会:dynamic_cast 的功能主要是区分指针的指向。

3.5 explicit

在 C++中,explicit 关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。

下面是一个简单的例子,它演示了 explicit 关键字的作用:

#include <iostream>
using namespace std;

class MyClass
{
public:
    int _x;
    // 构造函数
    explicit MyClass(int a)
        : _x(a)
        {}
};

int main()
{
    MyClass obj1(10); // 正确:直接初始化
    cout << obj1._x << endl;

    // MyClass obj2 = 20; // 错误:拷贝初始化(隐式转换),被 explicit 阻止
    // cout << obj2._x << endl;

    MyClass obj3 = (MyClass)30; // 正确:强制类型转换(显示转换)
    cout << obj3._x << endl;

    return 0;
}

在这个例子中,我们定义了一个名为 MyClass 的类,它有一个带有 explicit 关键字的构造函数。这意味着我们不能使用拷贝初始化(隐式转换)来创建该类的对象。因此,下面这行代码是错误的:

MyClass obj2 = 20; // 错误:拷贝初始化(隐式转换),被 explicit 阻止

但是,我们仍然可以使用直接初始化或强制类型转换(显示转换)来创建该类的对象。

为什么要用 explicit 关键字修饰构造函数?

对于编译器而言,语句 MyClass obj2 = 20; 等价于 MyClass obj2 = MyClass(20);。这种初始化方式被称为拷贝初始化(隐式转换),它会调用类的构造函数来创建一个临时对象,然后使用拷贝构造函数将临时对象的值复制给 obj2

对于是单参数的构造函数的对象,用括号传参的方式更加直观,像 MyClass obj2 = 20; 这样的语句可读性不是很好,所以用 explicit 修饰单参的构造函数,限制单参构造函数对象的构造方式。

在 C++11 之前,explicit 关键字只能用于修饰只有一个参数的构造函数。但是,在 C++11 及以后的版本中,explicit 关键字也可以用于修饰多参数构造函数,以及转换运算符。

例如,在 C++11 及以后的版本中,我们可以这样定义一个类:

class MyClass {
public:
 int x;
 int y;
 // 多参数构造函数
 explicit MyClass(int a, int b) : x(a), y(b) {}

 // 转换运算符
 explicit operator bool() const {
     return x != 0 || y != 0;
 }
};

在这个例子中,我们定义了一个带有两个参数的构造函数,并且使用了 explicit 关键字。此外,我们还定义了一个转换运算符,并且使用了 explicit 关键字。

main 函数中,我们可以使用直接初始化或列表初始化来构造 MyClass 类的对象。例如:

int main() 
{
 MyClass obj1(10, 20); // 直接初始化
 MyClass obj2{30, 40}; // 列表初始化(C++11 及以后)

 return 0;
}

在这个例子中,我们使用直接初始化和列表初始化两种方式来构造 MyClass 类的对象。注意,由于构造函数被声明为 explicit,所以我们不能使用拷贝初始化(隐式转换)来构造该类的对象。

4. RTTI

RTTI 是运行时类型识别(Runtime Type Identification)的简称,它是 C++中的一种特性,用于在运行时确定对象的实际类型。RTTI 主要包括三个运算符:typeid、dynamic_cast 和 decltype。

  • typeid:返回一个指向 std:: type_info 类的常量对象的引用,该对象包含了类型的信息,如类型名和哈希码。typeid 可以用于任何表达式,不管该表达式是否涉及多态。

  • dynamic_cast:在运行时识别出一个父类的指针(或引用)指向的是父类对象还是子类对象。

  • decltype:在运行时推演出一个表达式或函数返回值的类型。

5. 常见题目

C++中的 4 种类型转换分别是:____ 、____ 、____ 、____。

  • static_cast、reinterpret_cast、const_cast 和 dynamic_cast

4 种类型转换的应用场景?

  • static_cast 用于相近类型的类型之间的转换,编译器隐式执行的任何类型转换都可用 static_cast。
  • reinterpret_cast 用于两个不相关类型之间的转换。
  • const_cast 用于删除变量的 const 属性,方便赋值。
  • dynamic_cast 用于安全的将父类的指针(或引用)转换成子类的指针(或引用)。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐