c++类型转换

目录

C语言中的类型转换

为什么C++需要四种类型转换

C++强制类型转换

static_cast

reinterpret_cast

const_cast

dynamic_cast

explicit

RTTI

常见面试题


C语言中的类型转换

C语言和C++都是强类型语言,如果赋值运算符左右两侧变量的类型不同,或形参与实参的类型不匹配,或返回值类型与接收返回值的变量类型不一致,那么就需要进行类型转换。

C语言中有两种形式的类型转换,分别是隐式类型转换和显式类型转换:

  • 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。

  • 显式类型转换 :需要用户自己处理,以(指定类型)变量的方式进行类型转换。

需要注意的是,只有相近类型之间才能发生隐式类型转换,比如intdouble表示的都是数值,只不过它们表示的范围和精度不同。而指针类型表示的是地址编号,因此整型和指针类型之间不会进行隐式类型转换,如果需要转换则只能进行显式类型转换。比如:

cpp 复制代码
int main()
{
	//隐式类型转换
	int i = 1;
	double d = i;
	cout << i << endl;
	cout << d << endl;

	//显式类型转换
	int* p = &i;
	int address = (int)p;
	cout << p << endl;
	cout << address << endl;
	return 0;
}

C语言的强制类型转换存在两个主要问题:

一是转换的语法形式统一为圆括号加类型,代码中一旦出现大量类型转换,很难分辨具体意图(是精度转换、指针转换还是去const转换);

二是转换的安全性完全交给程序员,编译器不做检查,容易引发隐藏bug。这也是C++引入四种命名的强制类型转换的根本原因。


为什么C++需要四种类型转换

C风格的转换格式虽然很简单,但也有很多缺点:

  • 隐式类型转换在某些情况下可能会出问题,比如数据精度丢失。

  • 显式类型转换将所有情况混合在一起,转换的可视性比较差。

因此C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符,分别是static_castreinterpret_castconst_castdynamic_cast

这四种转换分别对应不同的使用场景,通过名字即可直观了解转换的意图,有助于代码阅读和维护。此外,C++风格的转换在编译期可以做更多的类型检查(如dynamic_cast的运行时检查),比C风格更安全。


C++强制类型转换

static_cast

static_cast用于相近类型之间的转换 ,编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关类型之间转换。比如:

cpp 复制代码
int main()
{
	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;

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

static_cast可以用于:

  • 数值类型之间的转换(intdoubledoubleint等)

  • 基类和派生类之间的向上转型(派生类指针/引用转基类,安全且无需类型检查)

  • 空指针转换为目标类型指针(void*Type*

  • 任何隐式转换的反向转换(如intenum

    但不能用于不相关类型(如指针→整数,除非使用reinterpret_cast)。另外,static_cast不进行运行时类型检查,因此向下转型(基类→派生类)是不安全的,应使用dynamic_cast

reinterpret_cast

reinterpret_cast用于两个不相关类型之间的转换。比如:

cpp 复制代码
int main()
{
	int a = 10;
	int* p = &a;
	int address = reinterpret_cast<int>(p);
	cout << address << endl;
	return 0;
}

reinterpret_cast还有一个非常bug的用法,比如在下面的代码中将带参带返回值的函数指针转换成了无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数。

cpp 复制代码
typedef void(*FUNC)();
int DoSomething(int i)
{
	cout << "DoSomething: " << i << endl;
	return 0;
}
int main()
{
	FUNC f = reinterpret_cast<FUNC>(DoSomething);
	f();
	return 0;
}

说明一下:用转换后的函数指针调用该函数时没有传入参数,因此这里打印出参数i的值是一个随机值。

reinterpret_cast是四种转换中最危险的,它仅仅是对二进制位的重新解释,不进行任何类型检查和数据转换。常见的使用场景包括:

  • 指针与足够大的整数类型之间的相互转换(如int*intptr_t

  • 函数指针之间的转换(如上面示例,但需极度谨慎,调用约定不匹配可能导致崩溃)

  • 在某种类型的内存区域中"放置"另一种类型的对象(底层序列化/反序列化)

    建议除非绝对必要,尽量避免使用reinterpret_cast;如果必须使用,确保你完全清楚底层的内存布局和对齐要求。

const_cast

const_cast用于删除变量的const属性转换后就可以对const变量的值进行修改。比如:

cpp 复制代码
int main()
{
	const int a = 2;
	int* p = const_cast<int*>(&a);
	*p = 3;
	cout << a << endl;  //2
	cout << *p << endl; //3
	return 0;
}

说明一下:

  • 代码中用const_cast删除了变量a的地址的const属性,这时就可以通过这个指针来修改变量a的值。

  • 由于编译器认为const修饰的变量是不会被修改的,因此会将const修饰的变量存放到寄存器当中,当需要读取const变量时就会直接从寄存器中进行读取,而我们修改的实际上是内存中的a的值,因此最终打印出a的值是未修改之前的值。

  • 如果不想让编译器将const变量优化到寄存器当中,可以用volatile关键字对const变量进行修饰,这时当要读取这个const变量时编译器就会从内存中进行读取,即保持了该变量在内存中的可见性。

const_cast最常见的用途是解决第三方库函数参数类型为普通指针,但你只有const指针的情况 ------前提是你确定该函数不会真正修改所指对象 。此外,const_cast也可以用于去除volatile属性。 需要强调:对原本就是const的对象进行修改是未定义行为 (尽管上述例子中修改了内存但读取了寄存器中的值,实际结果不可依赖)。const_cast的**正确用法是针对"原本不是const,但被const指针/引用指向"的对象,去掉const以便临时修改。**例如:

cpp 复制代码
int x = 10;
const int* p = &x;
int* q = const_cast<int*>(p);
*q = 20; // 合法,因为x本身不是const

dynamic_cast

dynamic_cast用于将父类的指针(或引用)转换成子类的指针(或引用)。

向上转型与向下转型

  • 向上转型:子类的指针(或引用)→ 父类的指针(或引用)。

  • 向下转型:父类的指针(或引用)→ 子类的指针(或引用)。

其中,向上转型就是所说的切割/切片,是语法天然支持的,不需要进行转换,向下转型是语法不支持的,需要进行强制类型转换。

向下转型的安全问题

向下转型分为两种情况:

  1. 如果父类的指针(或引用)指向的是一个父类对象,那么将其转换为子类的指针(或引用)是不安全的,因为转换后可能会访问到子类的资源,而这个资源是父类对象所没有的。

  2. 如果父类的指针(或引用)指向的是一个子类对象,那么将其转换为子类的指针(或引用)则是安全的。

使用C风格的强制类型转换进行向下转型是不安全的,因为此时无论父类的指针(或引用)指向的是父类对象还是子类对象都会进行转换。而使用dynamic_cast进行向下转型则是安全的,如果父类的指针(或引用)指向的是子类对象那么dynamic_cast会转换成功,但如果父类的指针(或引用)指向的是父类对象那么dynamic_cast会转换失败并返回一个空指针。比如:

cpp 复制代码
class A
{
public:
	virtual void f()
	{}
};
class B : public A
{};
void func(A* pa)
{
	B* pb1 = (B*)pa;               //不安全
	B* pb2 = dynamic_cast<B*>(pa); //安全

	cout << "pb1: " << pb1 << endl;
	cout << "pb2: " << pb2 << endl;
}
int main()
{
	A a;
	B b;
	func(&a);
	func(&b);
	return 0;
}

上述代码中,如果传入func函数的是子类对象的地址,那么在转换后pb1pb2都会有对应的地址,但如果传入func函数的是父类对象的地址,那么转换后pb1会有对应的地址,而pb2则是一个空指针。

说明一下:dynamic_cast只能用于含有虚函数的类因为运行时类型检查需要运行时的类型信息,而这个信息是存储在虚函数表中的,只有定义了虚函数的类才有虚函数表。

dynamic_cast对引用的处理与指针不同:**转换失败时指针版本返回nullptr,而引用版本会抛出std::bad_cast异常。因此使用引用版本时需要捕获异常。**示例:

cpp 复制代码
void func(A& ra) {
    try {
        B& rb = dynamic_cast<B&>(ra);
        // 转换成功
    } catch (std::bad_cast&) {
        // 转换失败
    }
}

另外,dynamic_cast有一定的运行时开销,不建议在性能敏感且频繁调用的代码中使用。如果能够通过设计避免向下转型(例如使用虚函数),则是更好的选择。

explicit

**explicit用来修饰构造函数,从而禁止单参数构造函数的隐式转换。**比如:

cpp 复制代码
class A
{
public:
	explicit A(int a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
private:
	int _a;
};
int main()
{
	A a1(1);
	//A a2 = 1; //error
	return 0;
}

在语法上,代码中的A a2 = 1等价于以下两句代码:

cpp 复制代码
A tmp(1);  //先构造
A a2(tmp); //再拷贝构造

所以在早期的编译器中,当编译器遇到A a2 = 1这句代码时,会先构造一个临时对象,再用这个临时对象拷贝构造a2 。但是现在的编译器已经做了优化,当遇到A a2 = 1这句代码时,会直接按照A a2(1)的方式进行处理,这也叫做隐式类型转换。

但对于单参数的自定义类型来说,A a2 = 1这种代码的可读性不是很好,因此可以explicit修饰单参数的构造函数,从而禁止单参数构造函数的隐式转换。

除了单参数构造函数,explicit在C++11之后还可以用于多参数构造函数 (只要除第一个参数外都有默认值),以及转换操作符explicit operator bool()等)。例如:

cpp 复制代码
class B {
public:
    explicit B(int a, int b = 0) {} // 防止 B = {1} 或 B = 1 的隐式转换
    explicit operator bool() const { return true; } // 防止 if(b) 的隐式转换,但允许显式 static_cast<bool>(b)
};

使用explicit可以提高代码的清晰度,避免因隐式转换导致的意外构造。


RTTI

RTTI(Run-Time Type Identification) 就是运行时类型识别。

C++通过以下几种方式来支持RTTI:

  • typeid:在运行时识别出一个对象的类型。

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

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

typeid操作符返回一个type_info对象的引用 ,可以用于比较类型或者获取类型名称(name())。但typeid对于指针类型,如果指针是nullptr且指向一个多态类型,会抛出std::bad_typeid异常。decltype则是在编译期推导类型不涉及运行时,严格说并不属于RTTI,但C++标准常将其归类为类型推导机制。注意:RTTI需要开启编译器相关选项(默认开启),会增加一些运行时开销。


常见面试题

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

分别是static_castreinterpret_castconst_castdynamic_cast

2、说说4种类型转换的应用场景。

  • static_cast用于相近类型的类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast

  • reinterpret_cast用于两个不相关类型之间的转换。

  • const_cast用于删除变量的const属性,方便赋值。

  • dynamic_cast用于安全的将父类的指针(或引用)转换成子类的指针(或引用)。

3、什么时候使用dynamic_cast代替static_cast进行向下转型?

答:当无法确定父类指针实际指向的是父类对象还是子类对象时,应使用dynamic_cast进行安全的向下转型;如果确定一定是子类对象(例如通过虚函数已经获知类型),可以使用static_cast以减少运行时开销。

4、使用const_cast修改原本就是const的对象会发生什么?

答:未定义行为。编译器可能将变量优化到寄存器,导致修改内存后读取仍为原值,甚至程序崩溃。仅当对象本身不是const时才应该使用const_cast去除const限制。

5、C++中的类型转换有哪些潜在风险?如何避免?

答:潜在风险包括:精度丢失(doubleint)、未定义行为(const_cast修改真正const对象)、不安全的重解释(reinterpret_cast导致对齐错误)、运行时异常(dynamic_cast引用转换失败)。避免方法:优先使用C++命名的转换操作符,减少C风格转换;尽量设计避免类型转换的接口;对必要的转换添加注释说明原因;使用编译器的警告选项(如-Wconversion)帮助发现隐式转换问题。

相关推荐
智者知已应修善业1 小时前
【51单片机用T0定时器方式1,实现0.5S的时间间隔实现第一次一个灯亮、第二次二个灯亮,直到全部灯亮,然后重复整个过程】2023-12-29
c++·经验分享·笔记·算法·51单片机
智者知已应修善业2 小时前
【51单片机4位静态数码管显示1234】2023-11-14
c++·经验分享·笔记·算法·51单片机
抓虾爪2 小时前
ST意法代理商粤科源兴丨LSM6DS3全系列现货库存,LSM6DS3TR-C当天可发
c++
妙为2 小时前
unreal engine5.7.4,创建ThirdPerson第三人称模版,类型是c++崩溃
c++·ue5·虚幻·unreal engine5
郝学胜_神的一滴2 小时前
Qt 高级开发 021:零基础吃透 QVBoxLayout 垂直布局
c++·qt
Boom_Shu2 小时前
长方形的关系
数据结构·c++·算法
思麟呀3 小时前
C++11并发编程:call_once一次性执行+atomic原子类型+CAS无锁编程+自旋锁
linux·开发语言·jvm·c++·windows
Lumbrologist3 小时前
【C++】零基础入门 · 第 13 节:类与对象基础
java·c++·算法
吴可可1235 小时前
CAD2004自定义实体开发环境配置
c++·算法