09 - C++中的类型转换

C++中的类型转换

C++提供了5种特定的强制类型转换:const_cast()static_cast()reinterpret_cast()dynamic_cast()和C++20新出的std::bit_cast()。虽然我们还能用旧的C风格强制类型转换,但那容易出错,推荐使用C++提供的类型转换。

const_cast()

可以使用它 为变量添加或取消const属性,这是5种类型转换种唯一可以消除const属性的转换。

首先是取消const属性的情况。自己的函数接收常量字符串,但第三方函数却不是,此时就要使用const_cast<char*>()为变量取消const属性:

void thirdPartyFunc(char* str);

void myFunc(const char* str)
{
	thirdPartyFunc(const_cast<char*>(str));
}

然后是添加const属性的情况。标准库提供了一个名为std::as_const()的辅助方法,它在<utility>中被定义,该方法接收一个引用参数,并返回它的const&版本,相当于const_cast<const T&>(obj)

std::string str("C++");
const std::string& constStr(std::as_const(str));

static_cast()

可以使用它 执行语言直接支持的显式转换。例如在计算小数除法时,为了避免整数除法,常常使用显式转换:

int i = 3, j = 4;
double res = static_cast<double>(i) / j;

它的另一个用途就是 在继承层次结构中执行向下的强制转换,这些强制类型转换既可以使用指针也能使用引用,它们不使用对象:

class Base
{
public:
	virtual ~Base() = default;
};

class Derived : public Base
{
public:
	virtual ~Derived() = default;
};

int main()
{
	Base* b = new Base();
	Derived* d = new Derived();
	// 对于从子类到父类的转换, 无需使用cast
	b = d;
	// 对于从父类到子类的转换, 需要使用cast
	d = static_cast<Derived*>(b);

	Base base;
	Derived derived;
	Base& br = derived;
	Derived& der = static_cast<Derived&>(base);
}

PS:这种转换需要谨慎使用,注意的点在下方。

使用static_cast()需要注意的是:

  1. 使用static_cast()的强制类型转换不会执行运行时类型检查。就拿刚刚的例子来看,如果要强行使用d,可能会导致潜在的故障,例如在对象边界之外进行内存覆盖(假设Derived类比Base类内容多,那d就要操控Base对象之外的内存)。

    执行运行时类型检查的转换是待会要说的dynamic_cast()

  2. 不能进行不相干类型间的转换,例如不能将指针转换为整数、不能将一种类型的指针转换为另一种不相干类型的指针等。

dynamic_cast()

它提供了对继承层次结构中的强制转换的运行时类型检查,可以使用它强制转换指针/引用。dynamic_cast()在运行时检查底层对象的运行时类型信息,如果强制转换无效,会返回空指针/抛出异常。

Base* b = new Base();
Derived* d = new Derived();
b = d;
d = dynamic_cast<Derived*>(b);

Base base;
Derived derived;
Base& br = derived;
try
{
    Derived& der = static_cast<Derived&>(base);
    // do sth...
} 
catch (const std::bad_cast&)
{
    std::cout << "Bad Cast!!\n";
}

由于运行时的类型信息存储在对象的 vtable 中,要使用dynamic_cast()类中必须至少具有一个虚方法,否则会编译错误。

reinterpret_cast()

这种转换更加底层,功能也更强大,相对的安全性也更低了。可以用它 执行C++类型规则中在技术上不允许的某些强制转换,因为它运行在不执行任何类型检查的情况下进行转换。

在程序员 明知 A类型变量的内存分布后,可以使用该转换将这片内存重新解释成是B类型变量的内存分布。例如,可将一种类型的引用强制转换为对另一种类型的引用、将void*强制转换为某种类型的指针等。

class X {};
class Y {};

int main()
{
	X x;
	Y y;
	X* p_x = &x;
	Y* p_y = &y;

	// 不相干类型指针间的转换
	p_x = reinterpret_cast<X*>(p_y);

	// 具体类型 -> void* 无需显式转换
	void* p = p_x;
	// void* -> 具体类型 需要显式转换
	p_x = reinterpret_cast<X*>(p);
	p_x = static_cast<X*>(p);

	// 不相干类型引用间的转换
	X& r_x = x;
	Y& r_y = reinterpret_cast<Y&>(x);
}

使用reinterpret_cast()也是存在一些限制的:

【待补充】

std::bit_cast()

C++20中引入了std::bit_cast(),定义在<bit>中。这是标准库中唯一的强制类型转换,其他强制转换是C++语言本身的一部分。它与reinterpret_cast()类似,但它会创建一个指定目标类型的新对象,并按位从源对象复制到此新对象。它要求源对象和目标对象的大小相同,且二者均可复制。

例如:

float asFloat = 1.23f;
auto asUint = std::bit_cast<unsigned int>(asFloat);
std::cout << std::format("asUint = {}\nstd::bit<float>(asUint) = {}", asUint, std::bit_cast<float>(asUint));

输出为:

asUint = 1067282596
std::bit<float>(asUint) = 1.23

它通常用于逐字节读文件到内存,然后用它正确地解释从文件中读取的字节。

小结

综上,下边总结了在不同情况下应使用的强制类型转换:

场景类型转换
移除const属性const_cast()
语言支持的显式强制转换(int -> double等)static_cast()
用户定义的构造函数/转换函数支持的显式强制转换static_cast()
类对象转换为另一个(无关的)类对象std::bit_cast()
在同一继承层次结构中,类指针转换为另一个类指针dynamic_cast()
在同一继承层次结构中,类引用转换为另一个类引用dynamic_cast()
指向一种类型的指针转换为指向其他不相干类型的指针reinterpret_cast()
一种类型的引用转换为其他不相干类型的引用reinterpret_cast()
指向函数的指针转换为指向其他函数的指针reinterpret_cast()

参考资料

  • 飘零的落花 - 现代C++详解
  • C++20高级编程