【C++】类型转换

类型转换

  • 前言
  • 正式开始
    • 为什么C++需要四种类型转换
    • C++强制类型转换
      • static_cast
      • reinterpret_cast
      • const_cast
      • dynamic_cast
    • RTTI(了解)
  • 总结

【C++】类型转换_第1张图片

前言

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

  1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
  2. 显式类型转化:需要用户自己处理
void Test ()
{
	int i = 1;
	// 隐式类型转换
	double d = i; // int 转 double
	printf("%d, %.2f\n" , i, d);
	
	int* p = &i;
	// 显示的强制类型转换 
	int address = (int) p; // 指针转整形
	printf("%x, %d\n" , p, address);
}

那么什么情况下应该用隐式类型转换,什么情况下用显示类型转换呢?

隐式类型转换是针对意义相近的类型,比如说整形和浮点数,二者都用来表示数据的大小。

但是指针和整形的话,二者之间没什么特别相关的,指针表示的是地址,整形表示的是大小。这就要用显示类型转化。

但是C中有些地方用到隐式类型转换的时候会很难让人发现,比如说我现在写一个简易版的顺序表的插入:

void Insert(size_t pos, char ch)
{
	size_t _size = 5;
	//....
	size_t end = _size - 1;
	while (end >= pos) // end隐式类型转换
	{
		//_str[end + 1] = _str[end];
		--end;
	}
}

这里各位能看出来有什么问题吗?

end的类型是size_t,当pos传值为0的时候,会陷入死循环中。因为size_t是一定会大于等于0的。
那我把end改为int可以吗?

void Insert(size_t pos, char ch)
{
	size_t _size = 5;
	//....
	int end = _size - 1;
	while (end >= pos) // end隐式类型转换
	{
		//_str[end + 1] = _str[end];
		--end;
	}
}

答案是还是不行,因为int和size_t比较的时候,int会变成size_t,又就变成了上面的情况。

所以这里的逻辑要改改,变成这样:

void Insert(size_t pos, char ch)
{
	size_t _size = 5;
	//....
	int end = _size - 1;
	while (end > pos) // >= 改成 > 不会死循环
	{
		_str[end] = _str[end - 1];
		--end;
	}
}

正式开始

为什么C++需要四种类型转换

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

  1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
  2. 显式类型转换将所有情况混合在一起,代码不够清晰
    因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的
    转化风格。

C++强制类型转换

标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:

static_cast
reinterpret_cast
const_cast
dynamic_cast

挨个来说。

static_cast

这个就相当于C中的隐式类型转换,不过要自己动手写:
【C++】类型转换_第2张图片

注意不要陷在C的那一套了,写成 (static_cast)d 那就大错特错了。

reinterpret_cast

这个就相当于显示类型转换。注意要会拼写reinterpret这个单词。

就拿前面int*和int的转换来说,如果用static转换的话就报错:
【C++】类型转换_第3张图片

得用reinterpret来转:
【C++】类型转换_第4张图片

打印的是十进制。

const_cast

这个是用来普通的转const的。也是类似于强转。

给出如下代码:
【C++】类型转换_第5张图片
这里将 const int* 类型的指针转换成了 int * 类型的指针。

结果是多少呢?
是 2 3 还是 3 3 ?
【C++】类型转换_第6张图片
答案是2 3。

但如果我调试起来的话:
【C++】类型转换_第7张图片
很奇怪,调试窗口中是3 3 ,但是打印的却是2 3。

这是因为编译器对const类型的变量会有优化,而且不同编译器的做法还不一样。有的编译器会认为a是不可变的,就直接将a加载到寄存器中,每次访问的时候直接去寄存器中读就行了,速度更快一点,而不是再从内存中加载到缓存或寄存器中。这里通过p将a改了,改的是内存中的a,而寄存器中的a还是2,当前main函数的这个进程访问的是寄存器中的a,所以打印的就是2。而监视窗口是又开的新进程,访问的是内存中的a,所以就是3。

还有的编译器是像宏替换那样,不去内存或寄存器中取,而是只要访问到a的时候就把a替换成2,vs下就是这种做法,看一下反汇编:
【C++】类型转换_第8张图片

如果不想让编译器优化的话,就加上volatile关键字,就是让编译器不要将a放到寄存器中或是直接替换,而是每次访问都到内存中去找:
【C++】类型转换_第9张图片

同样的,也可以用C。

不加volatile:
【C++】类型转换_第10张图片

加volatile:
【C++】类型转换_第11张图片

简单总结一下上面三个:
1、兼容c隐式类型转换和强制类型转换
2、期望不要用C中的了,多用用规范的C+ +显示强制类型转换。
3、static_cast(隐式类型转换)、reinterpret_cast、 const_cast(两个强制类型转换)

再说最后一个。

dynamic_cast

这个功能是将 父类指针/引用 转为 子类指针/引用 。

点进来的同学应该是对继承熟悉点的,C++语法是天生支持 子类指针/引用 赋值给 父类指针/引用的,这在我前面的博客中也是讲过,如果不熟悉的同学,点传送门:【C++】继承知识点详解。
这里再讲讲这一点。

一切的类型转换中间都会产生临时变量,比如说double和int的转换:
【C++】类型转换_第12张图片

这里double d = a,中间产生一个double类型的临时变量,a将其值传给临时变量中,然后临时变量再将其值传给double。但是临时变量具有常属性,无法被修改,这也是为什么int不能传给double&:
【C++】类型转换_第13张图片

这也是为什么能够赋值给const double& :
【C++】类型转换_第14张图片

但是如果是切片的话,就不存在const还是非const的问题。

设计出如下类:

class A
{
private:
	int a = 0;
};

class B : public A
{
private:
	int b = 0;
};

测试:

【C++】类型转换_第15张图片

三个都是切片,没有任何问题,不需要加const什么的,语法原生支持,所以这里并没有发生什么类型
的转换。

再来说dynamic_cast:

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

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

注意:

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

来看看怎么用。

两个类:

class A
{
public:
	virtual void f(){}
public:
	int _a = 0;
};

class B : public A
{
public:
	int _b = 1;
};

再来个函数:

// A*指针pa有可能指向父类,有可能指向子类
void fun(A* pa)
{
	// 如果pa是指向子类,那么可以转换
	// 如果pa是指向父类,那么不能转换	
	B* pb = (B*)pa;
}

讲一下为什么能/不能。

当pa指向的是父类,那么父类中是不包含子类中的内容的。比如说上面的A类对象,是不可能产生_b成员的,当pa强转成子类的时候,地址虽然能转过去,但是一访问_b就会崩掉,因为_b根本就不存在,野指针了:
【C++】类型转换_第16张图片

但是如果pa指向的是子类的对象,没问题,因为子类对象中是有所有的成员的,强转成子类,不会出现野指针的问题:
【C++】类型转换_第17张图片

库中的dynamic_cast也是这样区分的。
如果pa是指向子类,那么可以转换,转换表达式dynamic_cast返回正确的地址。
如果pa是指向父类,那么不能转换,转换表达式dynamic_cast返回nullptr。

搞个函数看看:

// A*指针pa有可能指向父类,有可能指向子类
void fun(A* pa)
{
	// 如果pa是指向子类,那么可以转换,转换表达式返回正确的地址
	// 如果pa是指向父类,那么不能转换,转换表达式返回nullptr
	B* pb = dynamic_cast<B*>(pa); // 安全的
	//B* pb = (B*)pa;             // 不安全
	if (pb)
	{
		cout << "转换成功" << endl;
		pb->_a++;
		pb->_b++;
		cout << pb->_a << ":" << pb->_b << endl;
	}
	else
	{
		cout << "转换失败" << endl;
		pa->_a++;
		cout << pa->_a << endl;
	}
}

【C++】类型转换_第18张图片

注意,父类对象无论如何都是不允许转换成子类对象的,只能是指针或引用:
【C++】类型转换_第19张图片

如果我把虚函数去掉:
【C++】类型转换_第20张图片
就不行了。

再给出如下类:

class A1
{
public:
	virtual void f(){}
public:
	int _a1 = 0;
};

class A2
{
public:
	virtual void f(){}
public:
	int _a2 = 0;
};

class B : public A1, public A2
{
public:
	int _b = 1;
};

【C++】类型转换_第21张图片

结果是啥?

首先B继承了A1和A2,ptr1和ptr2发生切片,所以ptr1和ptr2肯定是不同的。

那么pb1和pb2相同吗?
【C++】类型转换_第22张图片

答案是相同,可见指向子类的父类指针发生切片后,再传给子类指针会将原先偏移后的指针再偏移到最初子类的地址处。

再看:
【C++】类型转换_第23张图片

这里也是会再偏回去。

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

基本上就讲完了。

再来说最后一个东西:

RTTI(了解)

RTTI:Run-time Type identification的简称,即:运行时类型识别。
C++通过以下方式来支持RTTI:

  1. typeid运算符
  2. dynamic_cast运算符
  3. decltype

1是个操作符,用来识别出某一数据/表达式的类型的类型,后面跟个.name()就能显示类型。
2本篇都讲过了。
3我在前面的博客中也是说过的,不懂得同学点传送门:【C++】C++11中比较重要的内容介绍。

注意这里的RTTI要和RAII区别开。

RTTI是运行时类型识别。
RAII是将释放一份资源的责任托管给了一个对象。如果不懂,传送门: 【C++】智能指针。

总结

本篇常见面试题:

  1. C++中的4中类型转化分别是:_________、_________、_________、_________
  2. 说说4中类型转化的应用场景。

到此结束。。。

你可能感兴趣的:(C++,c++)