学习与实验C++中的类型强制转换操作

目标

在C或C++中,可以使用 “()” 来强制转换一个对象的类型。
在C++中,也有很多后缀为_cast强制转换运算符,例如static_castdynamic_cast等。

本篇的目标是,结合官方文档和自己的实验:

  • 学习他们的基本含义
  • 实验尝试他们的基本用法
  • 实验比较他们行为的区别

static_cast

static_cast 官方文档

定义
static_cast <type-id> ( expression )

Converts an expression to the type of type-id, based only on the types that are present in the expression.
In standard C++, no run-time type check is made to help ensure the safety of the conversion.
In general, you are certain of the data types involved in the conversion.

expression转换为type-id的类型。此转换仅基于expression本身的类型。
在标准的C++中,并没有运行时的检查来确保转换的安全性。
通常情况下,你应该对参与转换的数据类型是确定的。

基本用法1:数值型的转换

In general you use static_cast when you want to convert numeric data types such as enums to ints or ints to floats.

一般情况下,你可以使用static_cast来对数值型的数据类型进行转换。例如:将“枚举型”转换为“整数型”,将“整数型”转换为“浮点数型”。

#include <iostream>

enum Fruit{
    
    
    apple,
    banana,
};

int main()
{
    
    
    Fruit f = Fruit::banana;
    int x = static_cast<int>(f);        //将“枚举”转换为“整数型”
    std::cout << "x: " << x << std::endl;

    int y = 3;
    float z = static_cast<float>(y);    //将“整数型”转换为“浮点数型”
    std::cout << "z: " << z << std::endl;
}

输出:

x: 1
z: 3
基本用法2:将父类指针转换为子类指针

The static_cast operator can be used for operations such as converting a pointer to a base class to a pointer to a derived class. Such conversions are not always safe.

static_cast可以用来将一个父类指针转换为子类指针。但这样的转换并不总是安全的。

#include <iostream>

class Base {
    
    };

class Child : public Base
{
    
    
    int data;   //子类专有的数据
public:
    Child()
    {
    
    
        data = 7;//在构造函数中设定为7
    }
    void TestFunction() //子类专有的函数
    {
    
    
        std::cout << "data: " << data << std::endl;
    }
};

int main()
{
    
    
    Base* pB = new Child(); //虽然“pB”的类型是父类指针,但实际指向的对象是完整的子类对象。(此行代码任何情况下都正确且没有Warning)

    Child* pC = static_cast<Child*>(pB);    //将父类指针转换为子类指针。这里是正确的,因为从逻辑上可确保pB指向的是完整的子类对象
    pC->TestFunction();
}

输出:

data: 7
注意:本质上不安全,需要程序员自己保证正确性

正如官方文档中多次提到的:

not always safe.
It is left to the programmer to verify that the results of a static_cast conversion are safe.

static_cast并不总是安全的,其安全性是程序员自己保证的。

例如对于上例,当转换时指针所指向的对象并不是一个完整的子类对象时,则此种转换是危险的:

int main()
{
    
    
    Base* pB = new Base();  //pB指向了一个基类对象

    Child* pC = static_cast<Child*>(pB);    //将父类指针转换为子类指针。这里是错误的,因为从逻辑上可知pB指向的不是子类对象
    pC->TestFunction();
}

在我的机器上,运行的结果是输出:

data: -33686067

不仅对于父类指针转换为子类指针,对于数值型的转换也需要考虑转换是否符合预期。

例如,将32位的int转换为8位的char,就需要考虑8位的char有可能容纳不下32位的int,此时会被截断:

#include <iostream>

int main()
{
    
    
	int Int1 = 77;			//77在ASCII码中对应“M”
	int Int2 = 256 + 81;	//81在ASCII码中对应“Q”

	char Char1 = static_cast<char>(Int1);
	std::cout << "Char1: " << Char1 << std::endl;
	char Char2 = static_cast<char>(Int2);
	std::cout << "Char2: " << Char2 << std::endl;
}

输出:

Char1: M
Char2: Q

另外,也不难猜到,将“浮点数”转换为“整数”也有小数部分被舍去的问题:

#include <iostream>

int main()
{
    
    
	float a = 3.7;
	int b = static_cast<int>(a);
	std::cout << b << std::endl;
}

输出:

3

这些都需要程序员自己注意。

dynamic_cast

dynamic_cast 官方文档

定义
dynamic_cast < type-id > ( expression )

Converts the operand expression to an object of type type-id.
The type-id must be a pointer or a reference to a previously defined class type or a “pointer to void”. The type of expression must be a pointer if type-id is a pointer, or an l-value if type-id is a reference.

expression转换为type-id类型的对象。
type-id必须是一个已经定义的类(或void)的指针引用。如果type-id指针,则expression也必须是个指针,如果type-id引用,则expression必须是一个“左值”。


这个操作符中的dynamic意思为 “动态的”“运行时的” ,与之相对的概念是static“静态的” 或者说 “编译时的”

由于程序运行时在某种程度上是“不可预测的”(至少在C++中是这样),所以在C++中一个父类型的指针,所指向的对象的具体类型是无法在编译时期进行准确判断的。因此static_cast并不能保证转换的安全性。

然而,dynamic_cast使用了运行时类型信息 (RTTI),所以可以在运行时进行准确的判断,使得之前用static_cast的转换变得安全。

基本用法:安全地对类的指针进行转换

例如一个父类Base有两个子类ChildXChildY。创建一个实际类型为ChildY的对象并让一个父类指针指向它,然后用dynamic_cast尝试将其转换为ChildXChildY类型,观察效果:

#include <iostream>

class Base 
{
    
    
public: 
    virtual int testFunc() {
    
     return 1; }
};

class ChildX : public Base
{
    
    
public:
    virtual int testFunc() override {
    
     return 2; }
};
class ChildY : public Base
{
    
    
public:
    virtual int testFunc() override {
    
     return 3; }
};

int main()
{
    
    
    Base* pB = new ChildY();

    ChildX* pX = dynamic_cast<ChildX*>(pB);
    std::cout << "pX:" << ((pX == nullptr) ? "转换失败" : "转换成功") << std::endl;

    ChildY* pY = dynamic_cast<ChildY*>(pB);
    std::cout << "pY:" << ((pY == nullptr) ? "转换失败" : "转换成功") << std::endl;
}

输出:

pX:转换失败
pY:转换成功

可以看到,它只会转变为正确的类型,对于不符合的类型dynamic_cast将返回nullptr

通过这种方式,可以在运行时判断一个指针所指向对象具体的类型。

注意:只针对于指针或引用,不能对数值类型转换

dynamic_cast操作的对象必须是——已经定义的类(或void)的指针引用

否则,将通不过编译:
在这里插入图片描述

reinterpret_cast

reinterpret_cast 官方文档

定义
reinterpret_cast < type-id > ( expression )

Allows any pointer to be converted into any other pointer type. Also allows any integral type to be converted into any pointer type and vice versa.
Misuse of the reinterpret_castoperator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.

允许任何类型指针转换为其他类型指针。允许任何数值型的值转换为指针类型,反之亦然。
reinterpret_cast的滥用很容易导致不安全的行为。除非你想进行的转换必须在底层,否则你应该使用其他的转换操作符。

特点:朴实无华的底层转换

使用reinterpret_cast可以对任何类型的指针进行转换,比如将float转换为int

#include <iostream>

int main()
{
    
    
	//浮点数的指针:
	float* a = new float(9.625f);

	//转换为整数的指针
	int* b = reinterpret_cast<int*>(a);

	//整数的值:
	std::cout << *b << std::endl;
}

如果是使用static_cast将一个float转换为int,则很显然转换后应该是9。然而此处对于reinterpret_cast的运行结果是:

1092222976

看起来这个数像是个“乱码”,然而它有实际的意义:它是9.625这个浮点数的内存数据,被解释成int得到的结果。

如果感兴趣,可以看下面对上述说法的证明:


首先,先搞明白值为9.625float,其在内存中的二进制数据是什么样的。

这里我参考了《IEEE 754浮点数标准详解》

9.625拆分为二进制的形式:
9.625 = 8 + 1 + 0.5 + 0.125 = 2 3 + 2 0 + 2 − 1 + 2 − 3 = 1 × 2 3 + 0 × 2 2 + 0 × 2 1 + 1 × 2 0 + 1 × 2 − 1 + 0 × 2 − 2 1 × 2 − 3 \begin{aligned} 9.625 & = 8+1+0.5+0.125 \\ & =2^3+2^0+2^{-1}+2^{-3}\\ & = 1\times 2^3+0\times 2^2+0\times2^1+1\times2^0+1\times2^{-1}+0\times 2^{-2}1\times2^{-3}\\ \end{aligned} 9.625=8+1+0.5+0.125=23+20+21+23=1×23+0×22+0×21+1×20+1×21+0×221×23

用二进制表示的话,就是:
1001.101 1001.101 1001.101

规范化(将小数点移动到最左边的1的右边)后,就是:
1.001101 × 2 3 1.001101\times2^3 1.001101×23

对于IEEE 754标准的32位浮点数:

  • 第1位是符号位,这里是正值,所以是0
  • 之后的8位指明指数偏移,这里是+3,所以是127+3 = 130,即二进制的10000010
  • 剩下的23位是数值部分,这里是1.001101,去掉小数点前的1之后是001101,补齐23位后是00110100000000000000000

综上所述,9.625float在内存中的二进制数据是:
0 10000010 00110100000000000000000

通过一些二进制转换为10进制的小工具,可以能计算出:
01000001000110100000000000000000的10进制数值就是1092222976
在这里插入图片描述

const_cast

const_cast 官方文档

定义
const_cast <type-id> (expression)

Removes the const, volatile, and __unaligned attribute(s) from a class.

将一个对象的 constvolatile、和 __unaligned 修饰符去掉。


对于volatile、和 __unaligned我还没有使用经验所以暂时不讨论。
不过对于const来说,似乎就是将const这个修饰去掉。然而实际上情况并不只是这么简单,接下来结合实际代码讨论:

基本用法:去掉const的修饰使得编译通过

例如,下面的代码(改动自官方返范例)是通不过编译的,有两处错误:
在这里插入图片描述
必须使用const_cast才能使得这两处编译通过,代码如下:

#include <iostream>

class TestClass
{
    
    
	int data;
public:

	void SetData(int* x)
	{
    
    
		data = *x;
	}

	void PrintData() const
	{
    
    
		std::cout << "改动前:" << data << std::endl;

		const_cast<TestClass*>(this)->data--;	//使用const_cast去掉const修饰

		std::cout << "改动后:" << data << std::endl;
	}
};

int main()
{
    
    
	const int x = 7;	//常量整数

	TestClass c;

	c.SetData(const_cast<int*>(&x));			//使用const_cast去掉const修饰

	c.PrintData();
}

我能想象到的一种使用情况是:

你需要调用一个接口,此接口的参数不带const,而你手上想给它的参数恰巧是const。你没法改变此接口的定义,因此只能通过const_cast修饰符保证此参数可以通过编译。

注意:不能直接改变一个变量的常量状态

You cannot use the const_cast operator to directly override a constant variable’s constant status.

你不能使用const_cast来直接修改一个变量的常量状态。

这个问题也在《C++标准转换运算符const_cast - Ider - 博客园》这篇文章中有所讨论。

例如,下面的代码虽然可以通过编译:

#include <iostream>

int main()
{
    
    
	const int x = 7;	//常量整数

	int* pX = const_cast<int*>(&x);	//使用const_cast去掉const修饰

	*pX = 3;	//修改其值

	std::cout << x << std::endl;	//观察是否发生改变(应该没有)
}

但是从输出可以看出并没有修改成功:

7

看来,一个常量,没法通过const_cast进行修改。

强制转换运算符“()”

强制转换运算符“()” 的官方文档

定义
unary-expression ( type-name ) cast-expression

A type cast provides a method for explicit conversion of the type of an object in a specific situation.

在特定情况下,将一个类型的对象“显式”地转换为另一种类型。

编译器也在“隐式”地做一些转换,不过有时会发出警告,例如:
在这里插入图片描述

Explicit type casts are constrained by the same rules that determine the effects of implicit conversions

“显式”地转换将遵循“隐式”地转换相同的规则约束。

合法类型强制转换

《类型强制转换的转换 | Microsoft Docs》指出“合法类型强制转换”:
在这里插入图片描述

与“_cast”转换符的区别

我没有找到此方面明确的官方文档,不过我对我上面的例子都做了测试,将他们替换为(),发现:

  • static_cast的实例都可以换为()并得到相同的结果。
  • dynamic_cast不行,看来要使用运行时类型信息 (RTTI)就必须要使用dynamic_cast
  • reinterpret_cast的实例可以替换为()并得到相同的结果。
  • const_cast的实例“部分”通过编译并得到相同结果:成员函数的const修饰符,若将const_cast替换为()会通不过编译。

我目前对其的理解是:

  • ()会首先尝试 static_cast的行为,如果失败则会表现得像reinterpret_cast。牵扯到const时则会表现得像const_cast。不过一定不能替代dynamic_cast的作用。
  • 虽然()很容易通过编译,但是其行为是不好预测的。因此,最好还是明确地使用具体的_cast转换符,这样有助于对程序的行为进行预测,降低错误。

总结

首先,()尽量不使用,而是使用具体的_cast

const_cast主要用来去掉const修饰

下面重点是static_castdynamic_castreinterpret_cast三者的区别,我的总结如下:
在这里插入图片描述

不过以上的总结主要还是这些转换的基本含义和基本用法,实际项目中的情况可能会相对复杂,到时候还是推荐构筑一些实验来明确了解其行为。

猜你喜欢

转载自blog.csdn.net/u013412391/article/details/113779785