C/C++类型转换(C++四大强制类型转换)
目录
C语言中的类型转换
C++中的类型转换
C++新增的四种强制类型转换
static_cast
dynamic_cast
reinterpret_cast
const_cast
C语言中的类型转换
在下述代码中,我穷举了一些指针与指针之间的隐式类型转换以及基础类型与基础类型的隐式类型转换的例子,在VS2022下运行,并没有产生任何错误,程序可以正常运行,仅仅是爆出不少警告:
//test.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{int a = 0;char b = 'a';double c = 5.5;a = b;a = c;b = c;b = a;c = a;c = b;int* pa = &a;char* pb = &b;double* pc = &c;pa = pb;pa = pc;pb = pa;pb = pc;pc = pa;pc = pb;
}
还有就是,在C语言中,任意指针类型都可以隐式转换成void*类型,而void*类型也可以隐式转换成任意指针类型
在下面的代码中,我又穷举了一些基础类型与指针之间的隐式类型转换的例子,但这次出现了error的例子:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{int a = 0;char b = 'a';double c = 5.5;int* pa = &a;char* pb = &b;double* pc = &c;a = pa;a = pb;a = pc;b = pa;b = pb;b = pc;c = pa;//errorc = pb;//errorc = pc;//errorpa = a;pa = b;pa = c;//errorpb = a;pb = b;pb = c;//errorpc = a;pc = b;pc = c;//error
}
其实仔细观察一下,凡是出现error的例子都是double类型与指针类型相互转换的情况(注意double*和double可没关系奥,double*就是指针类型)。
- 实际上,double,包括float这些小数类型的数据在内存中的存储格式都与普通类型很不一样。编译器此时内心就会os:“逆天,虽然我很随便,但你也不能跨种族给我配对吧!”。
- 在者,在某些位数的操作系统中,指针与小数类型数据的字节数可能不同(比如16位系统下),强行转换会有丢失数据的风险,这是兼容性的考虑(你可能会说:“int类型字节,char类型1字节,照你这么考虑,我觉得他两也不合适!”,但是没办法,人家有类型提升、截断等语法规则保驾护航呢......)。不过在这儿没有很好的体现,因为唯独double类型与指针互相转换时报错,说明编译器并没有把这个算为错误。
所以一般来说它会禁止小数类型数据与指针类型数据的隐式类型转换。实际上就算使用强制类型转换编译也不会通过,总之就是不让你转换。
下面我将简单介绍一下C99引入的uintptr_t和intptr_t:
uintptr_t和intptr_t本质上是内置类型的重命名(也就是typedef),比如uintptr_t是usigned long的重命名,intptr_t可能是long的重命名,所以这两货重命名的对象有什么规律呢?
实际上,uintptr_t和intptr_t总是一个“与指针类型占用字节数相同,却是一个整型类型”的数据类型的重命名,也就是说,在32位系统下(指针大小是4字节),他两分别是usigned long和long;在16位系统(指针大小是2字节)下,他两分别是usigned int 和 int。总的来说,uintptr_t和intptr_t就是与指针大小相同的整型类型。
可以预见,这样的话,想把一个指针转成整型类型时就可以先将其转换成uintptr_t或者intptr_t,如此就可以保证这个指针的完整性(没有截断),至于之后再把它转换成什么类型的整型,那就属于整型类型相互转换的范畴了。而且在我们想把其转换回指针类型时,通过uintptr_t或者intptr_t也可以完整的转换过去(当然前提是你不能已经把它截断或者提升什么的)。
因此当我们想要指针与整型相互转换时可以用uintptr_t和intptr_t过渡,最起码防止你在不经意间丢失了数据,更加安全。
这是关于uintptr_t和intptr_t的文章推荐:【嵌入式】指针与整数的桥梁、跨平台编程的黄金钥匙:揭开 uintptr_t 和 intptr_t 的神秘面纱-CSDN博客
而且,它还可以帮我们钻个空子,让我们可以把double类型数据和指针相互转换:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <stdint.h>
int main()
{int a = 0;char b = 'a';double c = 5.5;int* pa = &a;char* pb = &b;double* pc = &c;// 将指针转换为 double(通过 uintptr_t)c = (double)(uintptr_t)pa;c = (double)(uintptr_t)pb;c = (double)(uintptr_t)pc;// 将 double 转换回指针(通过 uintptr_t)pa = (int*)(uintptr_t)c;pb = (char*)(uintptr_t)c;pc = (double*)(uintptr_t)c;}
- 我指针转uintptr_t可以吧,这可是你语法规定的可以通过它安全转换。那我把uintptr_t转换成double更没问题了吧,基础类型之间可以互相转换。
- 我double转uintptr_t没问题吧,基础类型之间的转换,那我把uintptr_t类型转指针也没问题吧,我现在又不是double类型,凭什么不让我转指针。
总而言之,C语言类型转换还是很随便的。
C++中的类型转换
有了上面的实验经历,相信读者也能独立对c++中的类型转换做实验了,下面我给出所得结论:
- 基础类型(int,double,char...)之间可以隐式类型转换。指针类型之间无法隐式类型转换。指针类型与基础类型之间无法隐式类型转换。
- 指针类型之间可以强制类型转换。指针类型与基础类型之间可以强制类型转换,但是涉及double类型的转换仍是不被允许的。
- 在c++中,任意指针类型都可以隐式转换成void*类型,但是void*类型转换成任意指针类型都需要强制类型转换。
C++新增的四种强制类型转换
四种强制类型转换语法为:
static_cast<new_type> (expression) dynamic_cast<new_type> (expression) const_cast<new_type> (expression) reinterpret_cast<new_type> (expression)
static_cast
static_cast把expression转换为new_type类型,它主要有如下几种用法:
- 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类表示)是安全的,进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
- 用于基础数据类型之间的转换,如把int转换成char。
- 把nullptr转换成目标类型的nullptr(如:int*p = static_cast<int*>(nullptr))。
- 把任何类型的表达式转换成void类型(如static_cast<void>(x)),这主要用于消除“定义了变量而不使用”时爆出的警告。
注意点:
- static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
- static_cast不用于普通指针类型之间的转换,除了上述的特例。
- 使用static_cast进行上行转化时,尤其在多继承时,会自动计算偏移量,使指针正确指向要转化的父类。当然,隐式类型转换时也会发生,在这儿提起主要是与reinterpret_cast做区分。
- 使用static_cast进行下行转化时,则不会有任何额外检查或计算偏移量,直接转换,所以它不安全。
dynamic_cast
dynamic_cast主要用于在类层次关系中(继承关系)向上转换,向下转换和交叉转换(多继承下基类与基类之间的转换),它会在运行时检查转换是否安全。不过dynamic的使用是有条件的:
-
使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。基类中需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。而且,运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见揭秘C++对象模型:虚表与内存布局_c++对象模型详解-CSDN博客)中,只有定义了虚函数的类才有虚函数表。
注意点:
- dynamic_cast如果转换指针失败的话返回的是0,如果转换引用失败会抛出异常
- 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
- 下行转换需要被转换的指针实际指向的对象就是子类对象。而交叉转换需要被转换的指针实际指向子类对象且new_type和expression的类型两个都是该子类的基类类型。
接下来我将举例说明下行转换和交叉转换:
class A
{
public:virtual void func() {}
};
class B
{
public:virtual void func2() {}};class C:public A,public B
{};int main()
{//下行转换A* a = new C;C* ptr = dynamic_cast<C*>(a);if (ptr) { cout << "转换成功" << endl; }//走这儿else { cout << "转换失败" << endl; }A* b = new A;C* ptr1 = dynamic_cast<C*>(b);if (ptr1) { cout << "转换成功" << endl; }else { cout << "转换失败" << endl; }//走这儿,b实际指向的不是子类对象try {C temp;A& c = temp;C& ptr3 = dynamic_cast<C&>(c);}catch (...) {cout << "转换失败-1" << endl;}try{A temp;A& d = temp;C& ptr4 = dynamic_cast<C&>(d);}catch (...){cout << "转换失败-2" << endl;//走这儿,d实际指向的不是子类对象}//交叉转换A* e = new C;B* ptr5 = dynamic_cast<B*>(e);if (ptr) { cout << "转换成功" << endl; }//走这儿else { cout << "转换失败" << endl; }}
reinterpret_cast
在C++语言中,reinterpret_cast没有检查,就是纯重新解释类型,主要有两种强制转换用途:
- 指针类型之间的转换
- 指针类型与基础类型之间的转换(浮点类型与指针类型的转换除外,会报编译错误:无效的类型转换)
int main()
{int x = 95;int* p = &x;int y = reinterpret_cast<int>(p);//指针->整型int * pp = reinterpret_cast<int*>(x);//整型->指针char* ptr = reinterpret_cast<char*>(p);//指针->指针
}
另外,static_cast和reinterpret_cast的区别主要在于多重继承,比如
class A
{public:int m_a;
};class B
{
public:int m_b;
};class C : public A, public B {};int main()
{C c;printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));
}
前两个的输出值是相同的,最后一个则会在原基础上偏移4个字节,这是因为static_cast计算了父子类指针转换的偏移量,并将之转换到正确的地址(c里面有m_a,m_b,转换为B*指针后指到m_b处),而reinterpret_cast却不会做这一层转换,他只是重新解释了类型,别的什么都不管。
const_cast
在C语言中,const限定符通常被用来限定变量,用于表示该变量的值不能被修改。而const_cast则正是用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。除此之外,它还可以去除volatile属性。
常量指针被转化成非常量指针,仍然指向原来的对象。
举个例子:
int main()
{const int x = 10;int* p = const_cast<int*>(&x);*p = 50;cout << p << ":" << &x << endl;cout << *p << ":" << x << endl;
}
可以发现,常量指针被转化成非常量指针,确实仍然指向原来的对象,地址是一样的。但是*p和x打印的值居然不一样,同一个空间存放两个值?
其实x的值是从寄存器取出来的,并不是直接取自内存,这是编译器故意这样做的。因为毕竟x已经定义成了常量,也就表示了不可修改,如果后面我们可以通过const_cast取消常性病随意更改,那么把x定义成常量就没有意义了。所以把x的值存在寄存器,从寄存器取,这样每次访问x都是原来的值,保持其常性,而访问*p就是我们修改的值。
既然不想修改本来是常量的变量的内容,那么const_cast有什么用呢?
const int* Search(const int* a, int n, int val);int main()
{int a[10] = { 0,1,2,3,4,5,6,7,8,9 };int val = 5;int* p;p = const_cast<int*>(Search(a, 10, val));if (p == NULL)cout << "Not found the val in array a" << endl;elsecout << "hvae found the val in array a and the val = " << *p << endl;return 0;
}const int* Search(const int* a, int n, int val)
{int i;for (i = 0; i < n; i++){if (a[i] == val)return &a[i];}return NULL;
}
上述代码中我们定义了一个函数,用于在a数组中寻找val值,如果找到了就返回该值的地址,如果没有找到则返回NULL。函数Search返回值是const指针(假设不能去掉const,这将会导致search返回指针后指针指向的内容在main函数中无法修改)。
当我们在a数组中找到了val值的时候,我们会返回val的地址,而此时我们可以去掉返回的指针的const属性,而且a数组本身在main函数中并不是const,因此我们就可以在main函数中对val值进行修改了。
了解了const_cast的使用场景后,可以知道使用const_cast通常是一种无奈之举,同时也建议大家在今后的C++程序设计过程中一定不要利用const_cast去掉指针或引用的常量性并且去修改原始变量的数值,这是一种非常不好的行为。
参考:
C++ 四种强制类型转换 - 静悟生慧 - 博客园
static_cast, dynamic_cast, reinterpret_cast, const_cast区别比较 - Jerry19880126 - 博客园
C++强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast - SpartacusIn21 - 博客园