C++新特性之四种类型转换

目录

一、类型转化的四种方式

二、C语言强制类型转换的缺点

三、每个类型转换的适用场景

四、每个类型转换的使用以及注意事项

4.1 const_cast (expression)

4.1.1 const_cast的基本使用,修改类型的const或volatile属性。

4.1.2 const_cast在调用第三方函数中的使用

4.1.3 const_cast去除volatile属性

4.2 static_cast (expression)

4.2.1 static_cast用于基本类型数据转换

4.2.2 static_cast用于类上行和下行转换:

4.2.3 static_cast总结(单向变双向)

4.3 dynamic_cast (expression)

4.3.1 指针类型:

4.3.2 引用类型

4.3.3 转换注意事项

4.4 reinterpret_cast (expression)

4.4.1 reinterpret_cast应用之用来辅助哈希函数

4.4.2 reinterpret_cast的其他应用


一、类型转化的四种方式

1.去常转换:const_cast<new_type>(expression)

2.静态转换:static_cast<new_type>(expression)

3.动态转换:dynamic_cast<new_type>(expression)

4.重新解释:reinterpret_cast<new_type>(expression)

其中,new_type为目标数据类型,expression为变量或者表达式

二、C语言强制类型转换的缺点

1.没有从形式上体现转换功能和风险的不同。

例如,将 int 强制转换成 double 是没有风险的,而将常量指针转换成非常量指针,将基类指针转换成派生类指针都是高风险的,而且后两者带来的风险不同(即可能引发不同种类的错误),C语言的强制类型转换形式对这些不同并不加以区分。

2.将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。

3.难以在程序中寻找到底什么地方进行了强制类型转换。

强制类型转换是引发程序运行时错误的一个原因,因此在程序出错时,可能就会想到是不是有哪些强制类型转换出了问题。

如果采用C语言的老式做法,要在程序中找出所有进行了强制类型转换的地方,显然是很麻烦的,因为这些转换没有统一的格式。

而用 C++ 的方式,则只需要查找_cast字符串就可以了。甚至可以根据错误的类型,有针对性地专门查找某一种强制类型转换。例如,怀疑一个错误可能是由于使用了 reinterpret_cast 导致的,就可以只查找reinterpret_cast字符串。

三、每个类型转换的适用场景

1. dynamic_cast 主要用于执行“安全的向下转型(safe downcasting)”,也就是说,要确定一个对象是否是一个继承体系中的一个特定类型。支持父类指针到子类指针的转换,这种转换时最安全的转换。它 是唯一不能用旧风格语法执行的强制类型转换,也是唯一可能有重大运行时代价的强制转换。

2.static_cast 可以被用于强制隐形转换(例如,non-const对象转换为const对象,int转型为double,等等),它还可以用于很多这样的转换的反向转换 (例如,void*指针转型为有类型指针,基类指针转型为派生类指针),但是它不能将一个const对象转型为non-const对象(只有 const_cast能做到),它最接近于C-style的转换。应用到类的指针上,意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换),同时,也能够执行相反动作:转换父类为它的子类。

3.const_cast 一般用于强制消除对象的常量性。它是唯一能做到这一点的C++风格的强制转型。这个转换能剥离一个对象的const属性,也就是说允许你对常量进行修改。

4.reinterpret_cast 是特意用于底层的强制转型,导致实现依赖(就是说,不可移植)的结果,例如,将一个指针转型为一个整数。这样的强制类型在底层代码以外应该极为罕见。操作 结果只是简单的从一个指针到别的指针的值得二进制拷贝。在类型之间指向的内容不做任何类型的检查和转换。

四、每个类型转换的使用以及注意事项

4.1 const_cast<new_type>(expression)

4.1.1 const_cast的基本使用,修改类型的const或volatile属性。

const_cast <type_name> (expression)

返回值为新类型。主要用于去掉const属性,该运算符用来修改类型的const(唯一有此能力的C+±style转型操作符)或volatile属性.type_name必须是指针*或引用&或成员指针类型。

代码演示

#include<iostream>

struct type {
    int i;

    type(): i(3) {}

    void f(int v) const
    {
        // this->i = v;                 // 编译错误:this 是指向 const 的指针
        const_cast<type *>(this)->i = v; // 只要该对象不是 const 就 OK
    }
};

int main()
{
    int i = 3; // 不声明 i 为 const
    const int &rci = i;
    //rci = 100;  //错误
    const_cast<int &>(rci) = 4; // OK:修改 i
    std::cout << "i = " << i << '\n';
  
    type t; // 如果这是 const type t,那么 t.f(4) 会是未定义行为
    t.f(4);
    std::cout << "type::i = " << t.i << '\n';
  
    const int j = 3; // 声明 j 为 const
    int *pj = const_cast<int *>(&j);
    *pj = 4;      // 未定义行为
    std::cout << "j = " << j << '\n';
    std::cout << "*pj = " << *pj << '\n';
  
    void (type::* pmf)(int) const = &type::f; // 指向成员函数的指针
    //const_cast<void(type::*)(int)>(pmf);   // 编译错误:const_cast 不能用于成员函数指针
}

运行输出:

i = 4
type::i = 4
j = 3
*pj = 4

上述代码介绍了const_cast常用的使用场景和使用的注意事项。其中:

const int j = 3; // 声明 j 为 const
int *pj = const_cast<int *>(&j);
*pj = 4;      // 未定义行为
std::cout << "j = " << j << '\n';    //输出结果:j = 3
std::cout << "*pj = " << *pj << '\n';  //输出结果:*pj = 4

对于&j使用const_cast转换后,进行重新赋值,这种行为在C++语法中是未定义的行为,但实际上是确实可以运行的。但对于打印输出结果却与我们的预期不一致,实际上对于j的值真的没有修改成功吗?下面我们继续做一个实验,代码如下:


#include<iostream>

int main()
{
    //未定义的行为,不提倡使用
    const int j = 3; // 声明 j 为 const
    int *pj = const_cast<int *>(&j);
    *pj = 4; // 未定义行为
    std::cout << "j = " << j << " ,addr(j):" << &j << '\n';
    std::cout << "*pj = " << *pj << " ,addr(*pj):" << pj << '\n';

    //正常的行为
    int j1 = 3;//最初声明为非const
    const int *cpj1  = &j1;
    int *pj1 = const_cast<int *>(cpj1);//cpj1最终指向的值(即j1的值)为非const类型,可以使用const_cast
    *pj1 = 4;
    std::cout << "j1 = " << j1 << " ,addr(j1):" << &j1 << '\n';
    std::cout << "*pj1 = " << *pj1 << " ,addr(*pj1):" << pj1 << '\n';
}

运行结果:

j = 3 ,addr(j):0x30932680c
*pj = 4 ,addr(*pj):0x30932680c
j1 = 4 ,addr(j1):0x3093267fc
*pj1 = 4 ,addr(*pj1):0x3093267fc

从运行结果可以看出,j和*pj的地址相同,j1和*pj1的地址相同,但是j和*pj显示的值却不同,为什么会出现这种结果呢?实际上这就是因为编译器优化结果造成的,因为在声明j的时候,其类型是const int,在编译阶段,编译器认为它就是不变的类型,当编译到std::cout << "j = " << j << " ,addr(j):" << &j << '\n';时,会将j直接替换为常量3,即std::cout << "j = " << 3 << " ,addr(j):" << &j << '\n';,因此打印出来的就过就是3。也正是由于该行为是未定义的行为,才导致输出结果与我们的预期不一致。所以,在我们日常使用中,const_cast可以用用来修改最初声明非const的值,而且应该尽量避免常量转换,除非我们真的需要使用它。

4.1.2 const_cast在调用第三方函数中的使用

const_cast另外一种使用场景就是:在使用第三方库或API时,它们只提供了非const类型的参数的函数,但我们只有const类型的对象。如下例所示。

#include <iostream>
using namespace std;

int third_lib_fun(int *ptr) {
    *ptr = *ptr + 10;
    return (*ptr);
}

int main(void) {
    int val = 10;
    const int *ptr = &val;
    int *ptr1 = const_cast<int *>(ptr);
    third_lib_fun(ptr1);
    cout << val;
    return 0;
}

输出结果:

20

在上例中,我们在使用第三方库和API的时候,我们只能调用,看不到其具体的实现,为了能够调用成功,需要使用const_cast来去除*ptrconst属性,来保证函数的正常调用,但是需要保证*ptr指向的对象在初始化的时候是非const的。

4.1.3 const_cast去除volatile属性

const_cast的另一个作用就是:const_cast可以用于丢弃volatile属性。例如在下面的示例中,通过const_castb1的类型由volatile int*转换为 int*

#include <iostream>
#include <typeinfo>
using namespace std;

int main(void) {
    int a1 = 40;
    volatile int* b1 = &a1;
    cout << "typeid of b1 " << typeid(b1).name() << '\n';
    int* c1 = const_cast<int*>(b1);
    cout << "typeid of c1 " << typeid(c1).name() << '\n';
    return 0;
}

输出结果:

typeid of b1 PVi
typeid of c1 Pi

从输出结果可以看到,b1的typeid为PVi(pointer to a volatile integer,指向volatile类型的int指针),c1的typeid为Pi(Pointer to integer,指向int的指针),使用const_cast去除了b1的volatile的属性。

4.2 static_cast<new_type>(expression)

static_cast相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。

  进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;

  进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。

③把空指针转换成目标类型的空指针。

④把任何类型的表达式转换成void类型。

注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性

4.2.1 static_cast用于基本类型数据转换

char a = 'a';
int b = static_cast<char>(a);//正确,将char型数据转换成int型数据

double *c = new double;
void *d = static_cast<void*>(c);//正确,将double指针转换成void指针

int e = 10;
const int f = static_cast<const int>(e);//正确,将int型数据转换成const int型数据

const int g = 20;
int *h = static_cast<int*>(&g);//编译错误,static_cast不能转换掉g的const属性

4.2.2 static_cast用于类上行和下行转换:

class Base
{};

class Derived : public Base
{}

Base* pB = new Base();
if(Derived* pD = static_cast<Derived*>(pB))
{}//下行转换是不安全的(坚决抵制这种方法)

Derived* pD = new Derived();
if(Base* pB = static_cast<Base*>(pD))
{}//上行转换是安全的

4.2.3 static_cast总结(单向变双向)

转换合法规则, 以下条件只要满足任一条,则转换合法:

  • new_type可被隐式转换为expression所属的类型 ;

  • expression可被隐式转换为new_type所属的类型.

High 是一个基类  ,Low是 High类的一个派生类,有如下代码:

High bar;    //基类
Low  blow;   //派生类
 
...
 
High *pb = static_cast<High *>(&blow);   //blow是派生类,pb是基类的指针,因此这里是向上转换,合法【基类指针本身就可指向派生类指针】
Low *pl  = static_cast<Low *>(&bar);     //bar是基类,pl是派生类指针,因此是向下转换,合法【但是要注意向下转换虽合法,但不安全,因为向下转换,访问到成员变量可能会崩溃,一般使用动态转换】

为什么上述两个转换都是合法的呢?

其实原因就一条,我们都知道,在派生关系中, 基类的指针可以直接指向一个派生类的对象,这个过程不需要显式转换 ,因此结合上述的转换合法原则,我们来分析一下上述两句转换的合法性:

  • High *pb = static_cast<High *>(&blow); // High *是 type_name ,&blow所属的类型是Low * , 因为 High* 可以被隐式的转换为Low* ,这符合转换合法原则的前种情况 " type_name可被隐式转换为expression所属的类型" ,因此转换合法;

  • Low *pl = static_cast<Low *>(&bar); // Low *是 type_name ,&bar所属的类型是High * ,还是因为 High* 可以被隐式的转换为Low* ,这符合转换合法原则的后种情况 "expression可被隐式转换为type_name所属的类型",因此转换合法.

因此,我们可以看到static_cast可以把原来只允许单向转换的场景变成允许双向转换,类似的情况有:

  1.  一般来说,可以把任意的数据类型指针赋值给void *指针 ,但是不能把 void *指针赋值给任意数据类型的指针, 如果使用了static_cast ,那么就可以实现把void * 赋值给 任意数据类型;

  2. 一般来说, 我们可以把int类型赋值给double类型,但是不能把 double类型的赋值给int类型 ,但如果使用了static_cast ,那么就可以把double类型赋值给int类型

  3. 一般来说,我们可以把一个枚举类型enum直接赋值给int ,但是不能把int直接转换为enum,但如果使用了static_cast ,那么就可以实现这种转换

类似的情况,大家可以继续发散..总之 ,原则就一条 :

  能不能合法转换,主要取决于new_type和expression之间要存在任一方向的隐式转换关系.

但是至于转换后的内容是否安全,这是开发需要自行保证的,编译器无法保证.

4.3 dynamic_cast<new_type>(expression)

        dynamic_cast主要用于类层次结构中父类和子类之间指针和引用的转换,由于具有运行时类型检查,因此可以保证下行转换的安全性,何为安全性?即转换成功就返回转换后的正确类型指针,如果转换失败,则返回NULL,之所以说static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL。

dynamic_cast转换方式主要有三种:

dynamic_cast< type* >(e) //type必须是一个类类型且必须是一个有效的指针,类中层次转换最常用的方式

dynamic_cast< type& >(e)//type必须是一个类类型且必须是一个左值

dynamic_cast< type&& >(e)//type必须是一个类类型且必须是一个右值

e的类型必须符合以下三个条件中的任何一个:

  • e是type的公有派生类

  • e是type的共有基类

  • e是type的类型

转换结果:

  • 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。

  • 如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。

  • e也可以是一个空指针,结果是所需类型的空指针。

  • 【比较】static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(cross cast)。

  • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的【父类指针本身就可指向一个子类对象】;

  • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

  • dynamic_cast是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作

代码举例

class Base{//抽象基类
virtual void fun(){}
};
class Derived:public Base{//派生类
};


//********************************
Base *P = new Derived();
Derived *pd1 = static_cast<Derived *>(P);//不建议该方式,可能存在转换不安全情况
Derived *pd2 = dynamic_cast<Derived *>(P);

转换结果:

  • 如果 P指向的确实是子类对象,则dynamic_cast和static_cast都可以转换成功;

  • 如果 P 指向的是父类对象时,static_cast转换不安全;

如果 P 指向的是父类对象时,static_cast转换在编译时不会报错,但也可以返回一个子类对象指针(假想),这样是不安全的,在运行时可能会有问题,因为子类中包含父类中没有的数据和函数成员,这里需要理解转换的字面意思,转换是什么?转换就是把对象从一种类型转换到另一种类型,如果这时用 pd1 去访问子类中有但父类中没有的成员,就会出现访问越界的错误,导致程序崩溃。而dynamic_cast由于具有运行时类型检查功能,它能检查P的类型,由于上述转换是不合理的,所以它返回NULL。

因此,在面向对象编程开发中,我们常常使用 dynamic_cast<Derived *>(p)语法环境来判断当前对象(一般父类基类对象)是否指向当前派生类:

4.3.1 指针类型:

Base为包含至少一个虚函数的基类,Derived是Base的共有派生类,如果有一个指向Base的指针bp,我们可以在运行时将它转换成指向Derived的指针,代码如下:

if(Derived *dp = dynamic_cast<Derived *>(bp)){
//使用dp指向的Derived对象
}
else{
//使用bp指向的Base对象
}

4.3.2 引用类型

因为不存在所谓空引用,所以引用类型的dynamic_cast转换与指针类型不同,在引用转换失败时,会抛出std::bad_cast异常,该异常定义在头文件typeinfo中。

void f(const Base &b){
try{
const Derived &d = dynamic_cast<const Base &>(b);
//使用b引用的Derived对象
}
catch(std::bad_cast){
//处理类型转换失败的情况
}
}

4.3.3 转换注意事项

C++中层次类型转换中无非两种:上行转换和下行转换

  • 对于上行转换,static_cast和dynamic_cast效果一样,都安全;

  • 对于下行转换:你必须确定要转换的数据确实是目标类型的数据,即需要注意要转换的父类类型指针是否真的指向子类对象,常用dynamic_cast进行转换。虽然static_cast和dynamic_cast都能成功,但是不安全,可能会出现访问越界错误,而dynamic_cast在运行时类型检查过程中,判定该过程不能转换,返回NULL。

4.4 reinterpret_cast <new_type> (expression)

reinterpret_cast 运算符并不会改变括号中运算对象的值,而是对该对象从位模式上进行重新解释。reinterpret_cast用在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。

由此可知,reinterpret_cast虽然看似强大,作用却没有那么广。IBM的C++指南、C++之父Bjarne Stroustrup的FAQ网页MSDN的Visual C++也都指出:错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。

4.4.1 reinterpret_cast应用之用来辅助哈希函数

// expre_reinterpret_cast_Operator.cpp
// compile with: /EHsc
#include <iostream>
// Returns a hash code based on an address
unsigned short Hash( void *p ) {
	unsigned int val = reinterpret_cast<unsigned int>( p );
	return ( unsigned short )( val ^ (val >> 16));
}

using namespace std;
int main() {
	int a[20];
	for ( int i = 0; i < 20; i++ )
		cout << Hash( a + i ) << endl;
}

//如果跟我一样是64位的系统,可能需要将unsigned int改成 unsigned long才能运行。

这段代码适合体现哈希的思想,暂时不做深究,但至少看Hash函数里面的操作,也能体会到,对整数的操作显然要对地址操作更方便。在集合中存放整形数值,也要比存放地址更具有扩展性(当然如果存void *扩展性也是一样很高的),唯一损失的可能就是存取的时候整形和地址的转换(这完全可以忽略不计)。

  • 当reinterpret_cast面对const

IBM的C++指南指出:reinterpret_cast不能像const_cast那样去除const修饰符。 这是什么意思呢?代码还是最直观的表述:

int main() 
{
    typedef void (*FunctionPointer)(int);
    int value = 21;
    const int* pointer = &value;
    
    //int * pointer_r = reinterpret_cast<int*> (pointer); 
    // Error: reinterpret_cast from type 'const int*' to type 'int*' casts away constness
    
    FunctionPointer funcP = reinterpret_cast<FunctionPointer> (pointer);
}

例子里,我们像前面const_cast一篇举到的例子那样,希望将指向const的指针用运算符转换成非指向const的指针。但是当实用reinterpret_cast的时候,编译器直接报错组织了该过程。这就体现出了const_cast的独特之处。

但是,例子中还有一个转换是将指向const int的指针付给指向函数的指针,编译顺利通过编译,当然结果也会跟前面的例子一样是无意义的。

如果我们换一种角度来看,这似乎也是合理的。因为
const int* p = &value;
int * const q = &value;

这两个语句的含义是不同的,前者是"所指内容不可变",后者则是"指向的地址不可变"(具体参考此处)。因此指向函数的指针默认应该就带有"所指内容不可变"的特性。

毕竟函数在编译之后,其操作过程就固定在那里了,我们唯一能做的就是传递一些参数给指针,而无法改变已编译函数的过程。所以从这个角度来想,上边例子使用reinterpret_cast从const int * 到FunctionPointer转换就变得合理了,因为它并没有去除const限定

4.4.2 reinterpret_cast的其他应用

// 将指针转换为整数类型
char* p = "example";
std::uintptr_t p_int = reinterpret_cast<std::uintptr_t>(p);

// 将整数类型转换为指针类型
char* p_back = reinterpret_cast<char*>(p_int);

// 将函数指针转换为不同类型的函数指针
void (*func_ptr)() = reinterpret_cast<void (*)()>(some_function);

// 将指向对象的指针转换为不同类型的对象指针
Derived* derived_ptr = reinterpret_cast<Derived*>(base_ptr);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值