C++的类型转换

Posted 森明帮大于黑虎帮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++的类型转换相关的知识,希望对你有一定的参考价值。

文章目录

C中的类型转换

  • 今天提到的这个话题,想必各位在实际使用的过程中定然会时常用到。不过,一般来说,当发生类型转换时,我们一般的处理手段,通常是C风格的这种方案。
void Func1()
    //这种称之为隐式的类型转换,编译器默默地将int类型的变量进行了转换
    int a=1;
    double b=a;
    //或者说,这样稍微显示一些的类型转换
    int c=(int)(5.0);

int main()
    Func1();
;
  • ps:顺带一提,在进行类型转换时,建议进行由低精度向高精度的转换,如果方向相反的话,会造成精度丢失,数据误差。

在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。

  • 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
  • 显式类型转化:需要用户自己处理。

C++中的类型转换

  • 虽然上面的方法很简单粗暴,便于上手,但是在理解方面,可能就不是足够的清晰了,毕竟当某些隐式转换发生时,身为程序员可能会不易差距这一过程的发生。为了避免这一问题,除却类型转换这边,在构造函数中有个很鲜明的例子,那就是**explicit**来避免构造函数的隐式转换。

    class A
    public:
    	explicit A(int a)
    		std::cout<<"A(int a)"<<std::endl;
    	
    	A(const A* a)
    		std::cout<<"A(const A& a)"<<std::endl;
    	
    private:
    	int a_;
    ;
    
    int main(int argc,char* argv[])
    	A a1(1); //隐式转换 -> A tmp(1); A a2(tmp);
    	A a2=1;
    	return 0;
    
    
  • 不过,这不是今天所要聊到的主角了,为了适用于更多的场景,以及避免歧义,C++额外引入了四种新的转换方式:

C风格的转换格式很简单,但是有不少缺点的:

  • 隐式类型转化有些情况下可能会出问题:比如数据精度丢失。
  • 显式类型转换将所有情况混合在一起,代码不够清晰。

因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。

void Func7()
    int i=1;
    //隐式类型转换
    double d=8.88;
    i=d;
    std::cout<<i<<" "<<d<<std::endl;

    int* ptr=&i;
    int address=(int)ptr;
    std::cout<<ptr<<" "<<address<<std::endl;

static_cast<type_id>(expression)

static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换。

void Func8()
    double d=12.34;
    int a=static_cast<int>(d);
    std::cout<<a<<std::endl;

  • 用途1:执行那些基本的内置数据类型转换。

    • float data=static_cast(1) --int类型转换为float类型。
  • 用途2:用于指针之间的类型转换。

    void Func2()
        //C++中的类型转换
        int type_int=10;
        float* float_ptr1=&type_int;                      //隐式类型转换无效
        float* float_ptr2=static_cast<float*>(&type_int); //显示转换有效
        char* char_ptr1=&type_int;                        //隐式类型转换无效
        char& char_ptr2=static_cast<char*>(&type_int);    //不合理的转换显示依然无效
    
        void* void_ptr=&type_int;                         //任何指针都可以隐式转换为void*
        float* float_ptr3=void_ptr;                       //void* ->float* 隐式类型转换无效
        float* float_ptr4=static_cast<float*>(void_ptr);  //void* ->float* 使用static_cast转换成功
        char* char_ptr3=void_ptr;                         //void* ->char* 隐式类型转换无效
        char* char_prt4=static_cast<char*>(void_ptr);     //void* ->char* 使用static_cast转换成功 
    
    
    
    • 针对那些看似不合理的转换,我们采用void *作为中间体,实现我们类型转换的目的。
    • 任何指针都可以隐式转换为void*。
    • static_cast是直接不允许不同类型的引用进行转换的,因为没有void类型引用可以作为中间介质,这点和指针是有相当大区别的。
    • 注意:指针之间无法实现隐式转换`,目的在于安全性保障。
  • 用途3:用于类之间的类型转换。

class A
public:
    int a;
;


class B
public:
    int b;
;

class C:public  A,public B
public:
    int c;
;

void Func3()
    C c;
    A a=static_cast<A>(c);   //子类转换为父类正常,叫做下转上
    B b=static_cast<B>(c);   //子类c转换为父类B正常,叫做下转上

    C c_a=static_cast<C>(a); //父类转换为子类不正常,叫做上转下
    C c_b=static_cast<C>(b); //父类b转换为子类c不正常,叫做上转下

    • 上行转换:子类向基类转换
    • 下行转换:基类向子类转换
    • static_cast会进行类型判断,对于上行转换来说,这是正常的,可以通过编译,而对于下行转换来说的话,编译器则会认为二者是毫无关联的类型。这点从逻辑角度也很容易想的通,从基类转子类,基类少了子类的一些部分,转起来自然会有问题。
    • 解决方案:在子类中重载一个相应类型的构造函数即可。
class A
public:
    int a;
;


class B
public:
    int b;
;

class C:public  A,public B
public:
    C()

    C(const A& a)
        c=a.a;
    
    
    C(const B& b)
        c=b.b;
    
    
private:
    int c;
;

void Func3()
    C c;
    A a=static_cast<A>(c);   //子类转换为父类正常,叫做下转上
    B b=static_cast<B>(c);   //子类c转换为父类B正常,叫做下转上

    C c_a=static_cast<C>(a); //子类C中有对象的构造函数 可以转换
    C c_b=static_cast<C>(b); //子类C中有对象的构造函数 可以转换

  • 用途4:用于没有多态的类实例指针或者引用之间的转换。
void Func4()
    //用途4:用于没有多态的类实例指针或者引用之间的转换。
    
    //上行part
    C c;
    A* a_ptr=static_cast<A*>(&c);        //子类对象的地址转换为父类的指针,下转上正常
    B* b_ptr=static_cast<B*>(&c);        //子类对象的地址转换为父类的指针,下转上正常
    A& a_ref=static_cast<A&>(c);         //子类的对象转换为父类的引用,下转上正常
    B& b_ref=static_cast<B&>(c);         //子类的对象转换为父类的引用,下转上正常

    //下行part
    C* c_ptra=static_cast<C*>(a_ptr);    //正常, 因为在这个函数里面是可逆的
    C* c_ptrb=static_cast<C*>(b_ptr);
    C& c_refa=static_cast<C&>(a_ref);
    C& c_refb=static_cast<C&>(b_ref);

    A* a_prt_fail=static_cast<A*>(b_ptr); //失败 B*转换为A* ,两个无关联的类型无效

    • 看到这里,小伙伴们可能就会有疑问了,上行转换没问题,但……下行转换不是会初始,怎么转换在当对象为指针和引用的时候可以正常执行呢?
    • 答案是,确实会有问题,但是由于没有执行类型检查,所以仍可正常编译通过,但要牢记,此种做法是不安全的,要尽量避免这种方式。此外,这种转换也只是在两个类之间用于继承关系时,才会发生,如若是毫无关系的两个类,编译器会显式的给出错误。
  • 用途5:用于具有多态的类实例指针或引用之间的转换。
class D
public:
    virtual void print()
        std::cout<<"D"<<std::endl;
    
;

class E
public: 
    virtual void print()
        std::cout<<"E"<<std::endl;
    
;

class F:public D,public E
public:
    virtual void print() override
        std::cout<<"F"<<std::endl;
    
;

void Func5()
    F f;
    D* d_ptr=static_cast<D*>(&f);      //子类对象的地址转换成父类的指针,下转上正常
    E* e_ptr=static_cast<E*>(&f);      //子类对象的地址转换成父类的指针,下转上正常
    d_ptr->print();                    //输出F,符合多态的要求
    e_ptr->print();                    //输出F符合多态的要求

    D& d_ref=static_cast<D&>(f);       //子类的对象转换成父类的引用,下转上正常
    E& e_ref=static_cast<E&>(f);       //子类的对象转换成父类的引用,下转上正常
    d_ref.print();                     //输出F,符合多态的要求
    e_ref.print();                     //输出F,符合多态的要求

    //下行part
    F* f_ptrd=static_cast<F*>(d_ptr);  //正常, 因为在这个函数里面是可逆的
    F* f_ptre=static_cast<F*>(e_ptr); //基类对象的地址转换为子类对的指针
    f_ptrd->print();                   //输出F,符合多态的要求
    f_ptre->print();                   //输出F,符合多态的要求

    F& f_refd=static_cast<F&>(d_ref);    //基类的对象转换为子类的引用
    F& f_refe=static_cast<F&>(d_ref);
    f_refd.print();                    //输出F,符合多态的要求
    f_refe.print();                    //输出F,符合多态的要求

    • 这块上行和下行转换都正常了???虽然没进行检查,不过我要说这个例子其实有点极端了,虽然正常归正常了,但说白了只是将对应子类的指针或者引用上行转换完,又进行了下行转换……,着实感觉是啥都没做一样。
    • 接下来,还是聊聊转换错误的例子吧:
void Func6()
    D d;
    E e;
    F* f_ptrd=static_cast<F*>(&d);
    F* f_ptre=static_cast<F*>(&e);
    f_ptrd->print();               //正常输出D
    //f_ptre->print();             //段错误

    F& f_refd=static_cast<F&>(d);
    F& f_refe=static_cast<F&>(e); 
    f_refd.print();               //正常输出D 
    f_refe.print();               //段错误


    • 有小伙伴,可能难免会疑惑,为毛会出现一个正常输出,另一个没有正常输出呢?
    • 原因在于,继承的一些内在机制导致的,其实这种也是不安全,不可靠的,只是转换完的内存布局,刚好使得其拥有了正常的输出调用。

    • 说到这里了,我直接给一个定性的结论好了。static_cast用于下行转换时,是极度不推荐的。 毕竟没有安全检查,如果没有出问题,也只是凑巧没有发生的结果,采用此种方式的代码撰写,可能会埋下极大的安全隐患。

const_cast<type_id>(expression)

const_cast最常用的用途就是删除变量的const属性,方便赋值。

void Func1()
	const int a=10;
	int *b=const_cast<int*>(b);
	*p=20;
	std::cout<<a<<" "<<*P<<std::endl;

  • 用途:用于去除变量的const或者volitale属性。
  • 这个其实没啥可以聊的地方,只要记住,这玩意的功能只有这个就是了,不能用于除此之外的类型转换。顺带一提,虽然这玩意对于我们处理const变量时能方便好不少,毕竟你没法隐式的转换const变量,但是……感觉好不安全,有丢让const的安全性打折扣。

reinterpret_cast<type_id>(expression)

reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型。

  • 用途:这个关键字就很简单粗暴,无论其是否相关,都直接重新解释
  • 注意事项
    • type-id和expression中必须有一个是指针或引用类型(可以两个都是指针或引用,指针引用在一定场景下可以混用,但是建议不要这样做,编译器也会给出相应的警告)。
    • reinterpret_cast的第一种用途是改变指针或引用的类型
    • reinterpret_cast的第二种用途是将指针或引用转换为一个整型,这个整型必须与当前系统指针占的字节数一致
    • reinterpret_cast的第三种用途是将一个整型转换为指针或引用类型
    • 可以先使用reinterpret_cast把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值(由于这个过程中type-id和expression始终有一个参数是整形,所以另一个必须是指针或引用,并且整型所占字节数必须与当前系统环境下指针占的字节数一致)
    • 使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝,和C风格极其相似(但是reinterpret_cast不是全能转换,详见第1点),实际上reinterpret_cast的出现就是为了让编译器强制接受static_cast不允许的类型转换,因此使用的时候要谨而慎之
    • reinterpret_cast同样也不能转换掉expression的const或volitale属性

错误例子:

class A
public:
    int a;
;

class B
public:
    int b;
;

void Func1()
    float type_float=10.1;
    int type_int=reinterpret_cast<int>(type_float);      //出错,type_id和expression中必须要有一个指针或者引用类型
    char type_char=reinterpret_cast<char>(&type_float); //出错,我的是64位的系统,这里的type_id必须是long类型
    double* type_double_ptr=reinterpret_cast<double*>(type_float); //出错,这里的expression只能是整形

    A a;
    B b;
    long type_long=reinterpret_cast<long>(a);       //出错,type_id和expression必须有一个是指针或者引用
    B b1=reinterpret_cast<B>(a);                    //出错,type_id和expression必须有一个是指针或者引用 
    A a1=reinterpret_cast<A>(&b);                   //出错,B* ->A 不允许,64位系统的话type_id只能是long类型   
    A* a_ptr=reinterpret_cast<A*>(b);               //出错,expression只能是整形

正确例子:

void Func2()
    float type_float=10.1;
    long type_long=reinterpret_cast<long>(&type_float); //将引用类型转换为整形

    float* type_float_ptr=reinterpret_cast<float*>(type_long); //将整形转换为指针或者引用类型
    std::cout<<*type_float_ptr<<std::endl; //正确,仍然输出10.1

    long* type_long_ptr=reinterpret_cast<long*>(&type_float); //正确,float* -> long* 

    char type_char='A';
    double& type_double_ptr=reinterpret_cast<double&>(type_char); //正确,char->double

    A a;
    B b;
    long type_long=reinterpret_cast<long>(&a); //将指针类型转换为整形
    A* a_ptr1=reinterpret_cast<A*>(type_long); //将整形转换为指针类型
    A* a_ptr2=reinterpret_cast<A*>(&b);        //B*->A* 两个都是指针或者引用,或者混用


dynamic_cast<type_id>(expression)

dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。

向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则) 向下转型:父类对象指针/引用>子类指针/引用(用dynamic_cast转型是安全的)。

注意: 1. dynamic_cast只能用于含有虚函数的类 2. dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0。

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;

class A
public:
	virtual void f_cast()

	int _a;
;
class B :public A
public:
	int _b;
;
void f_cast(A* pa)
	//如果想区分pa指向父类,还是子类对象?
	//如果pa指向的是子类对象,则转换成功
	//如果pa指向的是父类对象,则转换失败,返回nullptr
	B* pb = dynamic_cast<B*>(pa);
	//B* pb=(B*)pa;   C语言中的强制类型转换不行
	if (pb != nullptr)
		cout << "转换成功:pa指向子类对象!" << endl;
		pb->_a = 1;
		pb->_b = 2;
		
	
	else
		cout << "转换失败:pa指向父类对象!" << endl;
	

int main()
	A a;

	A* pa = &a;

	B b;

	f_cast(pa);

	pa = &b;
	f_cast(pa);
	return 0;


注意 强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。强烈建议:避免使用强制类型转换。

  • 用途:这个关键字的出现就是为了解决static_cast无法进行类型转换之间的类型检查的问题关键,且要注意的一点时,此关键字要求类的转换拥有多态的特性,说的更具体一些,需要类中需要虚函数的存在,因为唯有如此,该关键字才认为此次转换时拥有意义的。否则,毫无价值就。
  • dynamic_cast是运行时处理的,运行时会进行类型检查(这点和static_cast差异较大)。
  • dynamic_cast不能用于内置基本数据类型的强制转换,并且dynamic_cast只能对指针或引用进行强制转换。
  • dynamic_cast如果转换成功的话返回的是指向类的指针或引用,转换失败的话则会返回nullptr。
  • 使用dynamic_cast进行上行转换时,与static_cast的效果是完全一样的。
  • 使用dynamic_cast进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。并且这种情况下dynamic_cast会要求进行转换的类必须具有多态性(即具有虚表,直白来说就是有虚函数或虚继承的类),否则编译不通过。
  • 需要有虚表的原因:类中存在虚表,就说明它有想要让基类指针或引用指向派生类对象的情况,dynamic_cast认为此时转换才有意义(事实也确实如此)。而且dynamic_cast运行时的类型检查需要有运行时类型信息,这个信息是存储在类的虚表中的。
  • 在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。dynamic_cast则可以在运行期对可能产生问题的类型转换进行测试。
#include<iostream>

class A
public:
    virtual void print()
        std::cout<<"A"<<std::endl;
    
;

class B
public:
    virtual void print()
        std::cout<<"A"<<std::endl;
    
;

class C:public A,public B
public:
    virtual void print() override
        std::cout<<"C"<<std::endl;
    
;

void Func1()
    C c;

    //第一组
    A* a_ptr=dynamic_cast<A*>(&c);
    B* b_ptr=dynamic_cast<B*>(&c);
    a_ptr->print();
    b_ptr->print();
    C* c_ptra=dynamic_cast<C*>(a_ptr);  //成功,类C基类具有多态性,可以适用dynamic_cast进行转换
    C* c_ptrb=dynamic_cast<C*>(b_ptr);  //成功,类C具有多态性,可以适用dynamic_cast进行转换
    c_ptra->print();
    c_ptrb->print();

    //以下内容输出一致
    std::cout<<&c<<std::endl; 
    std::cout<<c_ptra<<std::endl;
    std::cout<<c_ptrb<<std::endl;

    //第二组
    A a;
    B b;
    C* c_ptra1=dynamic_cast<C*>(&a); // 编译正常(好的编译器会给你个警告),转换结果为nullptr,说明转换失败
    C* c_ptrb1=dynamic_cast<C*>(&b); // 编译正常(好的编译器会给你个警告),转换结果为nullptr,说明转换失败
    //以下内容输出一致,都是0,说明c_ptra1和c_ptrb1都是nullptr
    std::cout<<c_ptra1<<std::endl;
    std::cout<<c_ptrb1<<std::endl;



int main(int argc,char* argv[])
    Func1();
    return 0;

输出结果:

PS D:\\C++的类型转换\\bin\\Debug> .\\main.exe
C
C
C
C
000000C8D88FFC48
000000C8D88FFC48
000000C8D88FFC48
0000000000000000
0000000000000000
PS D:\\C++的类型转换\\bin\\Debug>
  • 看一个成功的例子吧,通过该关键字实现了下行转换,但即使编译器的没有报错……这次的转换也没有得到有意义的结果,而第一组的测试……显然有些毫无意义……。
  • 故此处再重申一下我的观点,非必要必要进行下行转换,如果非要进行就用dynamic_cast吧,至少会给你一显式的错误。
  • 不过可能有小伙伴会质疑这个关键字的用途。在我现行的理解下,如果基类是个抽象的基类,那么dynamic_cast的下行转换就有了意义,并且能够为你提供可靠的类型检查,让你发现自身的错误。

以上是关于C++的类型转换的主要内容,如果未能解决你的问题,请参考以下文章

C++类型转换

C++将派生类赋值给基类(向上转型)

类型转换

C++:类型转换

C++的类型转换

C++的类型转换