C++的四种类型转换

类型转换与虚函数

向下转型时,虚函数表用的是对象实际类型所对应的虚函数表,而不是你转换到的那个类型的虚函数表。​无论进行何种类型转换(向上、向下、平行),对象的虚函数表(vtable)指针
(vptr)在对象创建后就已经固定
,它始终指向对象实际类型的虚函数表。类型转换只是改变了编译器看待这块内存的“视角”(解释方式),而不会改变对象内部的vptr。​所有这些转换操作,都没有触碰或修改对象内部的vptr。它们只是在操作指针的类型,即
改变编译器进行类型检查

决定偏移量
的方式。也可以这样理解,由于虚表指针是固定长度的,且存放在对象内存的首部,那么不管类指针的类型如何转换,不管偏移量偏移多少,它总会保留那段完整的虚表指针(vptr)的地址。


#include <iostream>
#include <typeinfo>

class Base {
public:
	virtual void vfunc() { std::cout << "Base::vfunc()" << std::endl; }
	virtual ~Base() {} // 虚析构函数至关重要
};

class Derived : public Base {
public:
	void vfunc() override { std::cout << "Derived::vfunc()" << std::endl; }
};

int main() {
	// 场景1:正常的向上转型 (Upcasting)
	Derived derived_obj;
	Base* base_ptr = &derived_obj; // 向上转型:Derived* -> Base*

	std::cout << "Calling through Base* pointer (Upcast): ";
	base_ptr->vfunc(); // 输出: Derived::vfunc()

	// 场景2:向下转型 (Downcasting) - 使用 dynamic_cast(安全)
	Derived* derived_ptr1 = dynamic_cast<Derived*>(base_ptr);
	if (derived_ptr1) {
		std::cout << "Downcast (safe) successful. Calling: ";
		derived_ptr1->vfunc(); // 输出: Derived::vfunc()
	}

	// 场景3:向下转型 (Downcasting) - 使用 static_cast(不安全,但此处我们知道实际类型)
	Derived* derived_ptr2 = static_cast<Derived*>(base_ptr);
	std::cout << "Downcast (unsafe) performed. Calling: ";
	derived_ptr2->vfunc(); // 输出: Derived::vfunc()

	Base base1;
	Derived* derived1 = static_cast<Derived*> (&base1);
	std::cout << "Downcast (unsafe) performed. Calling: ";
	derived1->vfunc(); 
	// 输出: Base::vfunc(), 因为在创建对象 base1 时,
	// vptr 就是被设置成 Base 类的构造函数对象内部的 ​vptr, 
	// 无论类型转换导致偏移量是多少,虚表指针永远在

	Derived* derived2 = dynamic_cast<Derived*> (&base1);
	std::cout << "Downcast (unsafe) performed. Calling: ";
	derived2->vfunc(); // 无法输出,因为这里的 derived1 是一个空指针
	return 0;
}

向上转换(子转父)和向下转换(父转子)

用于在不同继承层次中的类指针之间进行转换(应使用dynamic_cast或static_cast)。其实无论是哪种转换,只要保证最初的那个实例对象他有足够的内存偏移量,供后面的类型去偏移,那么就不会出问题。向下转换不成功的原因,其实就是创建的父类对象在转为子类后,访问子类的成员时偏移量不够。
图片[1] - C++的四种类型转换 - 宋马
这里我们创建了一个子类Derived对象,其内存分布大致如上,如果将其类型转换成Base类,那么这个偏移量就缩减到了Base的那块。

类型转换的作用对象只能是指针或者引用!!!

static_cast


static_cast
是c++提供的
编译时

静态
)类型转换,主要用于已知安全的类型转换。它比C风格
(T)value
更安全,具有更明确的语义,并在编译时进行类型检查。

1.1 正确用法:基本类型转换


#include<iostream>
int main(){
	double d = 3.14;
	int a = static_cast<int>(d);	// 将 double 转为 int
	std::cout << "a = " << a << std::endl;
	return 0;
}

1.2 正确用法:基类与派生类之间的向上转换


class Base {
public:
	int a;
public:
	Base(int _a):a(_a){}
	virtual ~Base(){}
};

class Derived : public Base {
public:
	int b;
	Derived(int _b) :b(_b), Base(2*_b){}
};

int main() {
	Derived d(1);
	// 向上转换:从 Derived* 转为 Base*
	Base* b = static_cast<Base*>(&d);
	std::cout << "Base pointer: " << b->a << std::endl;		// 此时 b->b无意义
	return 0;
}

输出:
Base pointer: 2
Q1:此时若是向下转换,即从Base* 转为 Derived,会怎样?

static_cast可以强制将基类指针转换为派生类指针(即使对象并非该派生类实例),但这是未定义行为​(如访问派生类特有成员会崩溃)。static_cast不会进行安全检测,它只会无脑跑。


class Base {
public:
	int a;
public:
	Base(int _a) :a(_a) {}
	virtual ~Base() {}		// 此时必须将基类析构函数声明为虚函数
};

class Derived : public Base {
public:
	int b;
	Derived(int _b) :b(_b), Base(2 * _b) {}
};

int main() {
	Base d(1);
	// 向下转换:从 Base* 转为 Derived*,
	// 此时必须将基类析构函数声明为虚函数, 这样在析构对象b时才会去析构其对应的基类
	Derived* b = static_cast<Derived*>(&d);
	std::cout << "Derived pointer: " << b->a << std::endl;	// 父转子,a可以访问到1
	std::cout << "Derived pointer: " << b->b << std::endl;	// 父转子,子类的b无法访问,没有内存对应
	return 0;
}
输出:
Derived pointer: 1
Derived pointer: -858993460

Q2:那么如何实现安全的向下转换呢?
A2:使用 dynamic_cast


2、dynamic_cast

dynamic_cast 是C++运行时类型转换 (RTTI, Run-Time Type Information)机制的一部分,主要用于基类和派生类之间的转换,尤其是 安全向下转换 (Base->Derived)。另外,
dynamic_cast必须要求基类有虚函数
,因为它需要在运行时检查指针或引用所指向的实际对象的类型。
这个
检查过程
是:

编译器为任何包含虚函数的类生成一个虚函数表(vtable)每个包含虚函数的类对象都有一个隐藏的指针(vptr),指向其对应的 vtable在 vtable 中,除了虚函数地址,还存储了该类的类型信息(RTTI)​​dynamic_cast 在运行时通过查询对象的 vptr -> vtable -> RTTI 来动态确定其真实类型如果转换是安全的(例如,基类指针确实指向一个派生类对象),则转换成功如果转换不安全(例如,基类指针指向的就是一个基类对象),则转换失败(返回 nullptr 或抛出异常)

vptr在创建对象时就确定了自己的指向,例如

Base* bs = new Base();
此时,bs内的vptr指向的是Base类的虚函数表,其内部存储了Base类的类型信息,如果后面使用dynamic_cast进行类型转换(将Base类型的指针bs转换为Derived类型),在调用dynamic_cast时,会根据vptr指向的虚函数表进行类型检查,发现虚函数表内存储的类的类型信息与当前转换后的指针不同,且是向下转换,那么说明转换不安全,转换失败,返回 nullptr 或抛出异常。


Base* bs = new Derived();
此时,bs内的vptr指向的是Derived类的虚函数表,其内部存储了Derived类的类型信息,如果后面使用dynamic_cast进行类型转换(将Base类型的指针bs转换为Derived类型),在调用dynamic_cast时,会根据vptr指向的虚函数表进行类型检查,发现虚函数表内存储的类的类型信息与当前转换后的指针相同,则转换成功。


Derived* de = new Derived();
此时,bs内的vptr指向的是Derived类的虚函数表,其内部存储了Derived类的类型信息,如果后面使用dynamic_cast进行类型转换(将Derived类型的指针bs转换为Base类型),在调用dynamic_cast时,会根据vptr指向的虚函数表进行类型检查,发现虚函数表内存储的类的类型信息与当前转换后的指针为子与父的关系,偏移量足够,转换成功。


Derived* de = new Base();
没有这样的语法!!

这四种情况若是使用static_cast进行类型转换,那么在运行时是不会进行检查的,因为它在编译期就检查完了,只要偏移量足够,那就可以保证转出来的是我们想要的。而如果是调用虚函数的话,那肯定是都能调用的,因为虚表指针位于创建的对象地址的头部,不管如何偏移他都在。

如果没有虚函数会怎样?
如果一个类没有虚函数,意味着:
• 它没有虚函数表(vtable)
• 它的对象中没有隐藏的 vptr
• 因此,编译器无法为其生成和存储 RTTI

在这种情况下,dynamic_cast ​根本无法在运行时获取到类型信息来执行安全检查,所以编译器会直接报错

下面验证上述说的三种情况


class Base {
public:
	virtual void vfunc() { std::cout << "Base::vfunc()" << std::endl; }
	virtual ~Base() {} // 虚析构函数至关重要
};

class Derived : public Base {
public:
	void vfunc() override { std::cout << "Derived::vfunc()" << std::endl; }
};

int main() {
	Base* bs1 = new Base();
	std::cout<< "Base* bs1 = new Base();" << std::endl;
	Derived* de1 = dynamic_cast<Derived*> (bs1);
	if (de1) {
		de1->vfunc();
	}
	else {
		std::cout << "Conversion failed." << std::endl;
	}

	Base* bs2 = new Derived();
	std::cout << "Base* bs2 = new Derived();" << std::endl;
	Derived* de2 = dynamic_cast<Derived*> (bs2);
	if (de2) {
		de2->vfunc();
	}
	else {
		std::cout << "Conversion failed." << std::endl;
	}

	Derived* bs3 = new Derived();
	std::cout << "Derived* bs3 = new Derived();" << std::endl;
	Base* de3 = dynamic_cast<Base*> (bs3);
	if (de3) {
		de3->vfunc();
	}
	else {
		std::cout << "Conversion failed." << std::endl;
	}
}

/* 输出:
Base* bs1 = new Base();
Conversion failed.
Base* bs2 = new Derived();
Derived::vfunc()
Derived* bs3 = new Derived();
Derived::vfunc()
*/

验证成功。


3、const_cast

去除const / volatile 限定符

3.1、正确用法:修改可变数据

如果对象
本身不是const
,才可以通过 const_cast 去除指针的 const 属性。


int main() {
	const char str[] = "hello";
	str[0] = 'H'; // 报错,因为str是const的,但此时它所指向那块内存不是系统认为的const区
	
	// 修改本身是非const的字符数组是安全的
	// 虽然数组被声明为 const,但它实际上位于可写的栈内存中
	// 使用 const_cast 移除 const 限定符后,可以修改栈上的内容
	char* p = const_cast<char*>(str);
	p[0] = 'H';
	// 不会报错,因为此时str指向的是一个存储在栈上的char型数组的首地址,
	// 这个数组内部存储的是 位于只读数据段的字符串字面量 hello 的一份拷贝,
	// 这份拷贝是存放在栈上的,其本身不是const。
	std::cout << "Modified string: " << str << std::endl;
	return 0;
}

存储在栈内存上的const限定修饰的数组,不能直接更改数组值,但是能使用const_cast 移除 const 限定符,然后修改栈上的内容。


void modify(const char* str) {
	
}

int main() {
	const char* str = "hello";
	// 字符串字面量 "hello" 本身存储在只读内存段(.rodata),修改const字符数组是不安全的
	char* p = const_cast<char*>(str);
	p[0] = 'H';
	// 此时会报错,因为 str指向的 hello 存储在 只读数据段,本身就是const类型
	// 所有声明的时候,通常是声明为 const char* str = "hello";
	std::cout << "Modified string: " << str << std::endl;
	return 0;
}

存储在只读数据段的字符串字面量本身是只读的(const的),故不能使用const_cast 移除 const 限定符,不能修改只读数据段上的内容。

可以看到,二者紧紧只是初始化 str 的方式不同,const_cast就无法起作用了。具体的需要搞懂一个字符串字面量以及其指针是如何进行存储的。

3.2、常量基本类型

int main() {
	const int a = 10;
	int* p = const_cast<int*> (&a);
	*p = 20;
	
	std::cout << "Address of a: " << &a << std::endl;	// 000000ED2D53F854
	std::cout << "Address of p: " << p << std::endl;	// 000000ED2D53F854
	
	std::cout << "a = " << a << std::endl;				// 10
	std::cout << "a = " << *(&a) << std::endl;			// 10
	
	std::cout << "a = " << *((int*)(&a)) << std::endl;	// 20
	std::cout << "p = " << *p << std::endl;				// 20
	std::cout << "p = " << *((int*)(p)) << std::endl;	// 20
	return 0;
}

这个输出表明:

• 通过指针 p 确实修改了内存位置的值(变为 20)
p和&a内存地址相同,说明指向的是同一块内存

• 但编译器优化导致直接使用 a 时仍然得到 10
• 内存中的值确实被修改了,但编译器不使用这个值
若想通过a获得修改后的值,那么可以使用
*((int*)(&a))
进行强制转换。

与字符数组的区别
这与之前字符数组的例子不同,因为:

字符数组涉及字符串复制到
上整数常量通常由编译器直接优化字符数组的修改是在栈内存上,而整数常量可能存储在只读段或寄存器中

4、reinterpret_cast (重解释转换)

4.1 正确用法:指针与整数之间转换

用于进行各种不相关类型之间的转换,它允许将任何指针转换为任何其他指针类型,也允许将任何整数类型转换为任何指针类型,以及反向转换。


int main() {
	int a = 42;

	// 将指针转换为整数
	uintptr_t intValue = reinterpret_cast<uintptr_t>(&a);

	cout << "Address of a: " << &a << endl;
	cout << "Integer representation of address: " << intValue << endl;

	// 将整数转换回指针
	int* ptr = reinterpret_cast<int*>(intValue);
	cout << "Value at original address: " << *ptr << endl;

	return 0;
}
/*
Address of a: 0000008A13CFF604
Integer representation of address: 593037882884
Value at original address: 42
*/
4.2 指针类型的转换,int and double

int main() {
	int a = 10;
	int* intPtr = &a;

	// 将 int* 转换为 double*
	double* doublePtr = reinterpret_cast<double*>(intPtr);

	// 注意:这里实际上是不安全的,因为int和double的内存表示不同
	cout << "int value: " << a << endl;
	cout << "double value (reinterpreted): " << *doublePtr << endl; // 输出可能是无意义的数据

	return 0;
}
/*
int value: 10
double value (reinterpreted): -9.25596e+61
*/

在C语言中,我们通常使用强制类型转换来完成类似的操作:


int a = 10;
double* b = (double*)&a;
cout << "double value (no reinterpreted): " << *b << endl; // 输出 

/*
double value (no reinterpreted): -9.25596e+61
*/

然而,在C++中,使用 reinterpret_cast 更加明确,它告诉编译器和代码阅读者,我们正在执行一个低级的、不安全的转换。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容