本篇博客由 CSDN@先搞面包再谈爱 原创,转载请标注清楚,请勿抄袭。
前言
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与
接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型
转换和显式类型转换。
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转化:需要用户自己处理
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风格的转换格式很简单,但是有不少缺点的:
- 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
- 显式类型转换将所有情况混合在一起,代码不够清晰
因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
C++强制类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast
reinterpret_cast
const_cast
dynamic_cast
挨个来说。
static_cast
这个就相当于C中的隐式类型转换,不过要自己动手写:
注意不要陷在C的那一套了,写成 (static_cast)d 那就大错特错了。
reinterpret_cast
这个就相当于显示类型转换。注意要会拼写reinterpret这个单词。
就拿前面int*和int的转换来说,如果用static转换的话就报错:
得用reinterpret来转:
打印的是十进制。
const_cast
这个是用const转非const的。也是类似于强转。
给出如下代码:
这里将 const int* 类型的指针转换成了 int * 类型的指针。
结果是多少呢?
是 2 3 还是 3 3 ?
答案是2 3。
但如果我调试起来的话:
很奇怪,调试窗口中是3 3 ,但是打印的却是2 3。
这是因为编译器对const类型的变量会有优化,而且不同编译器的做法还不一样。有的编译器会认为a是不可变的,就直接将a加载到寄存器中,每次访问的时候直接去寄存器中读就行了,速度更快一点,而不是再从内存中加载到缓存或寄存器中。这里通过p将a改了,改的是内存中的a,而寄存器中的a还是2,当前main函数的这个进程访问的是寄存器中的a,所以打印的就是2。而监视窗口是又开的新进程,访问的是内存中的a,所以就是3。
还有的编译器是像宏替换那样,不去内存或寄存器中取,而是只要访问到a的时候就把a替换成2,vs下就是这种做法,看一下反汇编:
如果不想让编译器优化的话,就加上volatile关键字,就是让编译器不要将a放到寄存器中或是直接替换,而是每次访问都到内存中去找:
同样的,也可以用C。
不加volatile:
加volatile:
简单总结一下上面三个:
1、兼容c隐式类型转换和强制类型转换
2、期望不要用C中的了,多用用规范的C+ +显示强制类型转换。
3、static_cast(隐式类型转换)、reinterpret_cast、 const_cast(两个强制类型转换)
再说最后一个。
dynamic_cast
这个功能是将 父类指针/引用 转为 子类指针/引用 。
点进来的同学应该是对继承熟悉点的,C++语法是天生支持 子类指针/引用 赋值给 父类指针/引用的,这在我前面的博客中也是讲过,如果不熟悉的同学,点传送门:【C++】继承知识点详解。
这里再讲讲这一点。
一切的类型转换中间都会产生临时变量,比如说double和int的转换:
这里double d = a,中间产生一个double类型的临时变量,a将其值传给临时变量中,然后临时变量再将其值传给double。但是临时变量具有常属性,无法被修改,这也是为什么int不能传给double&:
这也是为什么能够赋值给const double& :
但是如果是切片的话,就不存在const还是非const的问题。
设计出如下类:
class A
{
private:
int a = 0;
};
class B : public A
{
private:
int b = 0;
};
测试:
三个都是切片,没有任何问题,不需要加const什么的,语法原生支持,所以这里并没有发生什么类型
的转换。
再来说dynamic_cast:
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
注意:
- dynamic_cast只能用于父类含有虚函数的类
- 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根本就不存在,野指针了:
但是如果pa指向的是子类的对象,没问题,因为子类对象中是有所有的成员的,强转成子类,不会出现野指针的问题:
库中的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;
}
}
注意,父类对象无论如何都是不允许转换成子类对象的,只能是指针或引用:
如果我把虚函数去掉:
就不行了。
再给出如下类:
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;
};
结果是啥?
首先B继承了A1和A2,ptr1和ptr2发生切片,所以ptr1和ptr2肯定是不同的。
那么pb1和pb2相同吗?
答案是相同,可见指向子类的父类指针发生切片后,再传给子类指针会将原先偏移后的指针再偏移到最初子类的地址处。
再看:
这里也是会再偏回去。
【注意】
强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。强烈建议:避免使用强制类型转换。
基本上就讲完了。
再来说最后一个东西:
RTTI(了解)
RTTI:Run-time Type identification的简称,即:运行时类型识别。
C++通过以下方式来支持RTTI:
- typeid运算符
- dynamic_cast运算符
- decltype
1是个操作符,用来识别出某一数据/表达式的类型的类型,后面跟个.name()就能显示类型。
2本篇都讲过了。
3我在前面的博客中也是说过的,不懂得同学点传送门:【C++】C++11中比较重要的内容介绍。
注意这里的RTTI要和RAII区别开。
RTTI是运行时类型识别。
RAII是将释放一份资源的责任托管给了一个对象。如果不懂,传送门: 【C++】智能指针。
总结
本篇常见面试题:
- C++中的4中类型转化分别是:_________、_________、_________、_________
- 说说4中类型转化的应用场景。
到此结束。。。