两个小时教你明白C++中类型转换关系

1、C语言中的类型转换

首先我们回忆下在C语言中类型是怎么转换的,我们通过下面的例子来具体说一下。

double a = 4.53532674; 
int b = a;		 
double c = b;	 
float d = a;	 
float e = 100; 

printf("a--> %.8f\n", a);       //a--> 4.53532674 
printf("b--> %d\n", b);         //b--> 4 
printf("c--> %f\n", c);         //c--> 4.000000 
printf("d--> %.8f\n", d);       //d--> 4.53532696 
printf("d--> %f\n", e);         //d--> 100.000000 

在这里插入图片描述
例如上面的 float e = 100; 100是int类型,需要先转换为 float 类型才能赋值给变量 e。再比如:int b = a; a 是double类型,需要先转换为int类型,才能赋值给变量 b。

上面的这些我们可以总结为是在将一种类型的数据赋值给另外一种类型的变量时自动进行了类型转换。如上面这样的转换方式,转换的过程程序员完全无感、不需要干预的方式称作自动类型转换,是编译器隐式地、偷偷转换。

同时我们也能够明确的看到,在进行转换的过程中可能会导致数据的失真或者精度的降低。因此,自动类型转换不一定是安全的,对于一般这样的转换,编译器会出现警告。

如果我们的表达式中包含了多种类型的混合运算,这个时候编译器也是会先将所有的类型转换为同一种类型,然后进行计算。当然这种转换一般是有规则的:

  • 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。简单的说,比如int 和 double 同时参与计算时,会先将int类型转换为double类型再进行计算。
  • 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
  • char 和 short 参与运算时,必须先转换成 int 类型。

据图可以总结为下图的模式。
在这里插入图片描述
我们再来看下下面的例子。

float PI = 3.14159; 
int r = 5; 
int s1 = r * r * PI;        //s1 = 78 
double s2 = r * r * PI;     //s2 = 78.539749 

上面的 r * r * PI 的过程都是先将 r 和 PI 转换为 double 类型,然后计算,计算的结果再根据赋值运算符左边的类型进行隐式转换。 s1 是 int 类型,所以最后会被自动转换为 int 类型,舍去了小数部分。

虽然自动类型转换能够解决我们很多的问题,自动类型的转换是编译器根据代码的上下文进行一种转换。不需要认为的干预,那么如果我一定要干预呢?一定要告诉编译器怎么转换呢?当然这种方式也是可以的,这样我们就需要在代码中明确的告诉编译器该怎么转换,这种方式称为强制类型转换

类型转换格式如: (type_name) expression , 也就是我们经常看到的 TYPE a = (TYPE) (b);

#include<stdio.h> 

int main() 
{ 
	int count = 113; 
	int n = 6; 
	double avg = count / n;                 //avg = 18.00000000 
	//double avg = (double)count / n;       //avg = 18.83333333 
	printf("%.8f", avg); 
	return 0; 
} 

比如上面的这个例子,我们进行强制转换前后的结果是完全不一样的。为什么呢? count / n count 和 n 都是int类型,因此表达式 count / n 的值类型也是int类型,但是avg是double类型的,能接收小数部分。

但是按照自动转换的规则,会将int类型的值转换为double类型,其小数部分已经被舍弃了。而表达式 (double)count / n,是先将 count 转换为double 类型,再进行计算,其结果也是double类型。

上面的两种类型转换之后,会不会改变原来变量的类型以及数值呢?

#include<stdio.h> 

int main() 
{ 
    double tol_price = 113.43; 
    int count = 6; 
    double avg = tol_price / count; 
    int price = (int)tol_price; 
    int avg_n = price / count; 

    printf("avg--> %.2f \n", avg); 
    printf("tol_price--> %.2f \n", tol_price); 
    printf("avg_n--> %d \n", avg_n); 

     return 0; 
}

在这里插入图片描述

从上面的例子中可以看出,进行类型转换并不会影响原本变量的类型已及数据,只是本次运算过程中产生的临时变量。

在标准C语言中,有些类型转换是不能进行自动转换的,只能进行强制转换。比如 void* 转换为 int*。但是可以自动转换的类型一定可以进行强制转换。自动类型转换一般来说风险较低。但是只能进行强制类型转换的一般风险会比较高。例如:int 到 char * 就是风险极高的一种转换,一般会导致程序崩溃。又如:char * 到 int * 就是很奇怪的一种转换。得到的值也很奇怪。

char* p = "aaa"; 
int* pT = (int*)p;  

printf("pT--> %d \n", *pT);     //pT--> 6381921 
printf("pT--> %d \n", pT);      //pT--> 4210736 printf("pT--> %d \n", &pT);     //pT--> 6487584 

下面的例子,我们借助了void* 将一个指向8字节双精度的指针 pd 转换成了一个指向4字节int型的指针pi。这种发生内存截断的设计缺陷会在转换后进行内存访问时存在安全隐患。

double PI = 3.1415926;   
double *pd = &PI;   
void *temp = pd;   
int *pi = temp; //转换成功  

由上面我们可以看出,隐式转换是比较安全的,显示类型转换是有风险的。

虽然增加了强制类型转换的语法,强调了风险,但这种强调的力度还是不够的。因此在C++中增加了4中强制类型转换的方法。方便追溯问题,更清晰的表明要干什么。增加了4个关键字。

关键字说明
static_cast用于良性转换,一般不会导致意外发生,风险很低
const_cast用于 const 与非 const、volatile 与非 volatile 之间的转换
dynamic_cast借助 RTTI,用于类型安全的向下转型(Downcasting)
reinterpret_cast高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换

2、static_cast

static_cast<type-id>( expression ),静态转换,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。适用于良性转换,这样的转换风险较低。

  • 原有的自动类型转换,例如 short 转 int、int 转 double、const 转非 const、向上转型等;

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
    进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
    进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

  • void 指针和具体类型指针之间的转换,例如void *转int *、char *转void *等;

  • 有转换构造函数或者类型转换函数的类与其它类型之间的转换,例如 double 转 Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。

  • 不能用于无关类型之间的转换,因为这些转换都是有风险的。比如:

  1. 两个具体类型指针之间的转换,int和double等。不同类型的数据存储格式是不一样的,数据长度也是不一样的,将A类型的指针指向B类型后,会按照A类型的方式来处理数据。存在的问题就是,如果读,则可能会读到无用的数据,如果写,会破坏B的数据。
  2. 不能将具体的地址赋值给指针变量,比如:int转换为int*;具体的地址上可能没有读写权限,或者,没有被分配。

下面我们以一个具体的例子来看看static_cast的用法,类似与C语言的转换方式我们就不看了,主要看下为什么说,static_cast进行下行转换时是不安全的。

#include <iostream>
class Base{
public:
	virtual void display()
	{
		std::cout << "base display..." << std::endl;
	}
	
private:
	int a = 0;
};

class Driver : public Base{
public:
	void display2()
	{
		std::cout << "driver display..." << std::endl;
	}	
public:
	int b = 2;
};

int main(int argc, char* argv[])
{
	Base b;
	Base* pb;
	
	Driver d;
	Driver* pd = NULL;
	
	pb = static_cast<Base*>(&d);
	pb->display();
	
	pd = static_cast<Driver*>(&b);
	pd->display2();
	
	std::cout << "pd is :" << &pd << std::endl;
	std::cout << pd->b << std::endl;	//能够正常访问,得到错误数据 0 
	
//	pd = dynamic_cast<Driver*>(&b);
//	std::cout << "pd is :" << &pd << std::endl;
//	std::cout << pd->b << std::endl;	//程序卡死,未崩溃,因为pd实际上是指向Base的指针
	
	if(NULL != pd)
	{
		std::cout << "pd is not null pointer..." << std::endl;	//static_cast进行转换时,打印此句 
	}
	else
	{
		std::cout << "pd is null pointer..." <<std::endl; 	//dynamic_cast进行转换时,打印此句 
	}
	
	//下面的用法是错误的
//	int *p1;
//    float *p3 = static_cast<float*>(p1);  //不能在两个具体类型的指针之间进行转换
//    p3 = static_cast<float*>(0X2DF9);  //不能将整数转换为指针类型
	
	return 0;
}

运行的结果显示如下:
在这里插入图片描述

由上面的结果也能够看出来,static_cast进行转换时,转换之后的派生类指针在访问派生类的成员变量时能够正常访问,只是得到的是不确定的数据,也就是说这个数据是没有意义的。并且在后续的对该指针判空时,该指针不为空,但是使用dynamic_cast进行转换时,该指针为空。也就能够说明,static_cast在进行类的下行类型转换时,未进行动态类型检查。

我们也通过上面的例子简单的和dynamic_cast做下对比发现,dynamic_cast在转换时进行了动态类型检查,并且在编译的时候已经发出警告。运行图片如下所示:
在这里插入图片描述

3、const_cast

const_cast<type-id>( expression ),const_cast是一个基于C语言编程开发的运算符,主要作用是修改变量的const和volatile属性。

我们还是通过例子来说明:

#include<iostream>
int main(int argc, char* argv[])
{
	const int a = 100;
	int* p = const_cast<int*>(&a);
	*p = 200;
	std::cout << "a: " << a << std::endl << "*p: " << *p <<std::endl;
	const int b = static_cast<const int>(*p);
	//b = 300;
	std::cout << "b: " << b <<std::endl;
	 return 0;
}

在这里插入图片描述
通过上面的例子我们可以看到。int 类型常量 a 的值被改变了。const属性被去掉了。同时是不是有一点疑惑,**为什么打印输出的时候a的值还是100?**要解释这个问题,我们就要从C++对常量的处理方式说起了,C++对常量的处理方式说起了,C++的处理方式更像是对编译期的#define一样,在编译期已经完成了置换,也就是说,上面的打印语句被修改为std::cout << "a: " << 100 << std::endl << "*p: " << *p <<std::endl; 这个样子了。

const_cast 运算符主要用来修改类型的const和volatile属性,那么怎么给类型增加属性呢?我们通常是借用static_cast来实现。

class BlockText
{
public:
    BlockText(char* text):pText(text) { }

    const char& operator[](std::size_t position) const
    {
        std::cout << "const char& operator" << std::endl;
        return pText[position];
    }
    
    char& operator[](std::size_t position)
    {
        std::cout << "char& operator" << std::endl;
        return const_cast<char&>(
            static_cast<const BlockText&>(*this)[position]);	//为了方便阅读和理解,进行换行处理
    }
private:   
    std::string pText;
};

上面的这个例子可以理解为:首先将this指针,通过static_cast运算符进行加const属性处理,然后调用成员函数const char& operator[](std::size_t position) const,最后使用const_cast运算符进行去const属性。

4、reinterpret_cast

reinterpret_cast<type-id> (expression),它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。

我们也用一个例子来看下:

#include<iostream>
class Base{
public:
    Base(int a, int b) : m_a(a), m_b(b) { }

private:
    int m_a;
    int m_b;
};

int main(int argc, char* argv[])
{
    const int a = 100;
    
    int* p = const_cast<int*>(&a);
    *p = 200;

    double* d = reinterpret_cast<double*>(p);
    std::cout << "d: " << *d <<std::endl;

    int* b = reinterpret_cast<int*>(new Base(30, 20));
    std::cout << "b: " << *b <<std::endl;	//实际上b指向的是类Base的成员变量m_a的地址 
    
    return 0;
}

在这里插入图片描述

上面的例子中,将int类型的指针p转换为double类型的指针后,其结果为1.88231e - 307,是不是一件比较荒诞的事情。而下面的b指向的是类Base的成员变量m_a的地址,这个可以通过多修改几次构造的值来确定。

这样会发生什么事情?

我们看一下,成员变量m_a是类Base的私有变量,但是经过类型转换之后能够直接访问。这样就击穿了类的封装性。

5、dynamic_cast

dynamic_cast (expression),主要作用:将一个基类对象指针(或引用)转换到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。dynamic_cast是用来检查两者是否有继承关系。

涉及到编译器的属性设置,而且牵扯到的面向对象的多态性跟程序运行时的状态也有关系,所以不能完全的使用传统的转换方式来替代。但是也因此它是最常用,最不可缺少的一个运算符。

dynamic_cast 与 static_cast 是相对的,dynamic_cast 是“动态转换”的意思,static_cast 是“静态转换”的意思。dynamic_cast 会在程序运行期间借助 RTTI 进行类型转换,这就要求基类必须包含虚函数;static_cast 在编译期间完成类型转换,能够更加及时地发现错误。dynamic_cast主要用做类层次间的上行转换和下行转换。

  • 上行转换
    向上转换时,只要待转换的两个类型之间存在继承关系,并且基类里面含有虚函数,满足了转换的条件,则转换时一定能成功。因此,dynamic_cast在上行转换时未做任何运行期的检查,也就是说这个时候的dynamic_cast和static_cast是一样的。
#include <iostream>

class Base{
public:
    Base(int a) : m_a(a){ }

    int data() { return m_a;  }

    virtual void display() 
    {
        std::cout << "base display..." << std::endl;
    }

private:
    int m_a = 0;
};

class Driver : public Base{
public:
    Driver(int a = 0, int b = 0): Base(a), m_b(b){ }

    void display2()
    {
        std::cout << "driver display..." << std::endl;
    }	

public:
    int m_b;
};

int main(int argc, char* argv[])
{
    int n = 100;
    Driver *pd = reinterpret_cast<Driver*>(&n);
    Base *pb = dynamic_cast<Base*>(pd);
    std::cout << "pd = " << pd << std::endl << "pb = " << pb << std::endl;
    std::cout << pb->data() << std::endl;
    pb->display();

    return 0;
}

在这里插入图片描述

通过上面的例子,我们可以看出,当我们将一个int类型的指针转换为Driver类型之后,再进行dynamic_cast上行转换之后,转换前后的两个指针的地址是相同的,就会出问题。主要是因为,转换过程中没有进行类型安全检查,转换之后,指针pb实际指向的地址还是n,并没有指向对象,因此会出问题。

  • 下行转换

在前面说static_cast的时候,我们顺便说了下dynamic_cast的下行转换。我们也是看到了我们下行转换是没有成功的。前面主要展示的是转换指针的形式,下面我们主要来看下引用会发生什么事情。

还是用下面的例子来说明下问题:

#include <iostream>

class Base{
public:
    Base(int a) : m_a(a) { }

    int data() { return m_a; }

    virtual void display()
    {
        std::cout << "base display..." << std::endl;
    }

private:
    int m_a;
};

class Driver : public Base{
public:
    Driver(int a = 0, int b = 0): Base(a), m_b(b){ }

    void display2()
    {
        std::cout << "driver display..." << std::endl;
    }	

public:
    int m_b;
};

int main(int argc, char* argv[])
{
    Base b(1);
    Base* pb;
    Base& b1 = b;

    Driver d(1, 2);
    Driver* pd = NULL;

    pb = dynamic_cast<Base*>(&d);
    pb->display();

    pd = dynamic_cast<Driver*>(&b);
    pd->display2();

    std::cout << "pd is :" << &pd << std::endl;

    Driver& d1 = dynamic_cast<Driver&>(b1);
    d1.display();

    if(NULL != pd)
    {
        std::cout << "pd is not null pointer..." << std::endl;
    }
    else
    {
        std::cout << "pd is null pointer..." <<std::endl;
    }

    return 0;
}

在这里插入图片描述
编译之后运行发生了什么?抛出了std::bad_cast异常。因此,要牢记,进行动态类型转换的时候,如果转换前后的类型都是引用的话,一定不要忘记接收异常,否则程序会崩溃。

现在还有一个疑问,就是我们按照正常的下行转换,为什么会失败,不论是指针还是引用的形式,都会失败?为什么呢?

我们接着看下面这个例子:

#include <iostream>

class Base{
public:
    Base(int a) : m_a(a) { }

    int data() { return m_a; }

    virtual void display()
    {
        std::cout << "base display..." << std::endl;
    }

private:
    int m_a;
};

class Driver : public Base{
public:
    Driver(int a = 0, int b = 0): Base(a), m_b(b){ }

    void display2()
    {
        std::cout << "driver display..." << std::endl;
    }	

public:
    int m_b;
};

class Driver2 : public Driver{
public:	
    void display3()
    {
        std::cout << "driver2 display..." << std::endl;
    }	
};

int main(int argc, char* argv[])
{
    Base b(1);
    Base* pb;
    Base& b1 = b;

    Driver d(1, 2);
    Driver* pd = NULL;

    pb = dynamic_cast<Base*>(&d);
    pb->display();

    pd = dynamic_cast<Driver*>(&b);
    pd->display2();

    std::cout << "pd is :" << &pd << std::endl;

    pb = new Driver2();
    pd = dynamic_cast<Driver*>(pb);

    if(NULL != pd)
    {
        std::cout << "pd is not null pointer..." << std::endl;
    }
    else
    {
        std::cout << "pd is null pointer..." <<std::endl;
    }

    return 0;
}

在这里插入图片描述
这个时候,就能够正常转换。为什么呢?和前面有什么不同呢?

仔细观察代码,你会发现,我们将Base的指针pb指向了它的派生类的派生类,这样就能够转换成功了。也就是说,如果pb指针指向了基类Base,则转换失败,pb指针指向基类的派生类之后就能转换成功。因此,下面我们将会再说一个概念,就是继承关系。

我们先看下上面这个例子中的继承关系。
在这里插入图片描述
如图,也就是说,在dynamic_cast进行转换时,首先会先找到指针指向的对象,然后向上遍历,如果向上遍历的过程中找到了需要转换的目标类型,纳闷说明这种转换关系是安全的,转换成功,否则就会存在风险,不会转换,dynamic_cast会返回nullptr或者抛出异常。

因此我们可以看出,dynamic_cast 在进行下行转换的时候,实际上也是在进行上行转换。

以上,就是C++中的几种类型转化。值得注意的是,日常中比较常用的static_cast和dynamic_cast这两个。但要注意他们的区别。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值