C语言中的类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与 接收返回值类型不一致时,就需要发生类型转化
C语言中有两种形式的类型转换,分别是隐式类型转换和显式类型转换
- 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
- 显式类型转换:需要用户自己处理,以
(指定类型)变量
的方式进行类型转换。
需要注意的是,只有相近类型之间才能发生隐式类型转换,比如int和double表示的都是数值,只不过它们表示的范围和精度不同。而指针类型表示的是地址编号,因此整型和指针类型之间不会进行隐式类型转换,如果需要转换则只能进行显式类型转换。比如:
#include <iostream>
using namespace std;
int main() {
//隐式类型转换
int i = 1;
double d = i;
cout << i << endl;//1
cout << d << endl;//1
//显式类型转换
int *p = &i;
int address = (int) p;
cout << p << endl; //0x61feac
cout << address << endl;//6422188
return 0;
}
缺陷: 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换
为什么C++需要四种类型转换
C风格的转换格式虽然很简单,但也有很多缺点:
- 隐式类型转换在某些情况下可能会出问题,比如数据精度丢失。
- 显式类型转换将所有情况混合在一起,转换的可视性比较差。
因此C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符,分别是static_cast
、reinterpret_cast
、const_cast
和dynamic_cast
。
C++强制类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast
、reinterpret_cast
、const_cast
、dynamic_cast
static_cast
static_cast
是C++中的一种类型转换运算符。它主要用于基本数据类型之间的转换,例如从int转换为float,或者从double转换为int等。它也可以用于类层次结构中基类和派生类之间的转换。
static_cast
的基本语法如下:
static_cast <new_type> (expression)
new_type
: 这是转换后的数据类型。expression
: 这是要转换的表达式或变量。
static_cast
在编译时执行,所以如果类型不兼容,编译器将报错。这也意味着static_cast
提供的类型检查是在编译时而不是运行时进行的。
下面是一个简单的例子:
float f = 3.14;
int i = static_cast<int>(f);
在这个例子中,我们将一个float类型的变量转换为int类型。转换结果是3,因为当我们从float转到int时,小数部分会被截断。
此外,static_cast
也可以在类的层次结构中使用。例如,如果有一个基类Base
和一个从Base
派生的类Derived
,我们可以将Derived*
类型的指针转换为Base*
类型,反之亦然。但是如果实际的对象不是正确的类型,结果将是未定义的。
例如:
Base* b1 = new Base();
Derived* d = static_cast<Derived*>(b1); // 不安全,因为b1实际上指向的是一个Base对象,而不是一个Derived对象。
注意: 在使用static_cast
进行向下转型时(即从基类到派生类),一定要确保被转换的对象确实是目标类型。否则,结果是未定义的。如果不能确定,应使用dynamic_cast
来进行安全的向下转型,它在运行时检查类型。
static_cast
用于相近类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast
,但它不能用于两个不相关类型之间转换。比如:
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;
}
reinterpret_cast
reinterpret_cast
是一种 C++ 中的类型转换运算符,它提供了从一种指针类型到另一种指针类型,或者从一个整数类型到另一个整数类型的底层二进制转换。它基本上是告诉编译器,将处理的实体当作其它完全不相关的类型来处理。
这种类型的转换没有普通的类型检查,例如,你不能将一个对象转换成一个不相关的类或结构。然而,reinterpret_cast
能做的转换包括将一个指针转换为一个整数,或者将一个整数转换为一个指针。这种操作在操作底层硬件或者执行与特定实现相关的任务时可能有用。
其语法为:
reinterpret_cast <type-name> (expression)
例如,如果你有一个 long
类型的值,你想要查看这个值在内存中的确切表示,你可以使用 reinterpret_cast
将这个 long
类型的指针转换为 char*
类型的指针:
long int li = 1024L;
char* p_char = reinterpret_cast<char*>(&li);
然后,你就可以使用 p_char
来访问 li
的内存表示了。
reinterpret_cast
还有一个非常bug的用法,比如在下面的代码中将带参带返回值的函数指针转换成了无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数。
#include <iostream>
using namespace std;
typedef void (*FUNC)();
int DoSomething(int i) {
cout << "DoSomething: " << i << endl;
return 0;
}
int main() {
FUNC f = reinterpret_cast<FUNC>(DoSomething);
f();//DoSomething: 4200128
return 0;
}
说明一下: 用转换后的函数指针调用该函数时没有传入参数,因此这里打印出参数i
的值是一个随机值。
需要注意的是,reinterpret_cast
的结果是高度依赖于其实现的,所以它的可移植性不高,并且也很容易被误用。因此,除非有足够的理由使用它,否则应尽可能避免使用 reinterpret_cast
。
const_cast
const_cast
是 C++ 中的一个类型转换运算符,它主要用于修改类型的 const 或 volatile 属性。但是,它不能改变原有变量的基本类型,只能改变变量的 cv 限定符,也就是 const 和 volatile。
其语法为:
const_cast<type-name>(expression)
这里的 type-name
必须是目标类型,而 expression
是要转换的值或对象。
例如,你有一个 const 变量,但是你需要传递给一个只接受非 const 参数的函数,那么你可以使用 const_cast
来去除 const 限定:
const int const_var = 10;
int* p_var = const_cast<int*>(&const_var);
在上述代码中,const 变量 const_var
的地址被转换为一个非 const int 的指针 p_var
。
然而,需要注意的是,尽管 const_cast
可以用来去除 const 属性,但它并不意味着你可以通过去除 const 限定后的指针来修改原始 const 变量的值。这是因为 const 变量在程序编译期间可能已经被放入只读存储区,它的值是不能被修改的。如果试图修改 const 变量的值,结果是未定义的。
总的来说,const_cast
的主要用途之一就是在函数的参数为非 const 时,但我们又只有 const 变量可供使用时,可以通过 const_cast
来符合函数的调用要求。但一般来说,如果可以,我们还是应当尽量避免使用这种类型转换运算符。
dynamic_cast
dynamic_cast
用于将父类的指针(或引用)转换成子类的指针(或引用)。
向上转型与向下转型
向上转型: 子类的指针(或引用)→ 父类的指针(或引用)。
向下转型: 父类的指针(或引用)→ 子类的指针(或引用)。
其中,向上转型就是所说的切割/切片,是语法天然支持的,不需要进行转换,而向下转型是语法不支持的,需要进行强制类型转换。
向下转型的安全问题
向下转型分为两种情况:
- 如果父类的指针(或引用)指向的是一个父类对象,那么将其转换为子类的指针(或引用)是不安全的,因为转换后可能会访问到子类的资源,而这个资源是父类对象所没有的。
- 如果父类的指针(或引用)指向的是一个子类对象,那么将其转换为子类的指针(或引用)则是安全的。
使用C风格的强制类型转换进行向下转型是不安全的,因为此时无论父类的指针(或引用)指向的是父类对象还是子类对象都会进行转换。而使用dynamic_cast进行向下转型则是安全的,如果父类的指针(或引用)指向的是子类对象那么dynamic_cast会转换成功,但如果父类的指针(或引用)指向的是父类对象那么dynamic_cast会转换失败并返回一个空指针。比如:
#include <iostream>
using namespace std;
class A {
public:
virtual void f() {}
};
class B : public A {
};
void func(A *pa) {
B *pb1 = (B *) pa; //不安全
B *pb2 = dynamic_cast<B *>(pa);//安全
//在第一次调用是。由于a并非B类的实例,这里的dynamic_cast<B*>(pa)将会失败,并返回nullptr。
cout << "pb1: " << pb1 << endl;
cout << "pb2: " << pb2 << endl;
}
int main() {
A a;
B b;
func(&a);
func(&b);
return 0;
}
上述代码中,如果传入func函数的是子类对象的地址,那么在转换后pb1和pb2都会有对应的地址,但如果传入func函数的是父类对象的地址,那么转换后pb1会有对应的地址,而pb2则是一个空指针。
说明一下: dynamic_cast只能用于含有虚函数的类,因为运行时类型检查需要运行时的类型信息,而这个信息是存储在虚函数表中的,只有定义了虚函数的类才有虚函数表。
explicit
explicit用来修饰构造函数,从而禁止单参数构造函数的隐式转换。比如:
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等价于以下两句代码:
A tmp(1); //先构造
A a2(tmp); //再拷贝构造
所以在早期的编译器中,当编译器遇到A a2 = 1这句代码时,会先构造一个临时对象,再用这个临时对象拷贝构造a2。但是现在的编译器已经做了优化,当遇到A a2 = 1这句代码时,会直接按照A a2(1)的方式进行处理,这也叫做隐式类型转换。
但对于单参数的自定义类型来说,A a2 = 1这种代码的可读性不是很好,因此可以用explicit修饰单参数的构造函数,从而禁止单参数构造函数的隐式转换。
RTTI
RTTI(Run-Time Type Identification)是C++提供的一种在运行时识别和使用类型信息的机制。RTTI可以让你了解某个基类指针或引用所绑定对象的真实派生类型。RTTI提供了两个主要的功能:dynamic_cast
和 typeid
操作符。
- dynamic_cast:主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
- 上行转换(Upcasting):把派生类对象的指针或引用转换为基类指针或引用。这种转换是安全的,因为派生类对象包含基类部分。
- 下行转换(Downcasting):把基类对象的指针或引用转换为派生类指针或引用。这种转换是不安全的,因为我们试图让基类对象做派生类对象能做的事情,但是基类对象并不是派生类对象。
- typeid:这是一个运算符,用于查询对象的类型。typeid返回一个类型为std::type_info的常量对象的引用。这个类位于头文件中。
注意:使用RTTI会有一定性能开销,如果一个类或者类的基类中没有任何虚函数,那么这个类类型的对象就无法进行dynamic_cast运算。
示例:
#include <iostream>
#include <typeinfo> //包含 typeid 使用所需的头文件
class Base {
public:
virtual void f() {}
};
class Derived : public Base {
public:
void f() {}
};
int main() {
Base* basePtr = new Base;
Base* derivedPtr = new Derived;
// 判断 basePtr 实际指向的类型
if(typeid(*basePtr) == typeid(Base)) {
std::cout << "basePtr points to Base object.\n";
} else if(typeid(*basePtr) == typeid(Derived)) {
std::cout << "basePtr points to Derived object.\n";
}
// 判断 derivedPtr 实际指向的类型
if(typeid(*derivedPtr) == typeid(Base)) {
std::cout << "derivedPtr points to Base object.\n";
} else if(typeid(*derivedPtr) == typeid(Derived)) {
std::cout << "derivedPtr points to Derived object.\n";
}
delete basePtr;
delete derivedPtr;
return 0;
}
typeid
用于确定动态类型(即运行时的实际类型)。注意,为了得到正确的结果,基类 Base
必须包含至少一个虚函数,因为在没有虚函数的情况下,typeid
不能正确地确定动态类型。