目录
前言
C语言中的类型转换
C++强制类型转换
static_cast(static静止的)
reinterpret_cast(reinterpret重新解释)
const_cast(const常量)
总结
dynamic_cast(dynamic动态)
RTTI
常见面试题
前言
C++继承了不少C语言的知识,因为C++最开始就是从C语言出来的,于是也就导致了C++将C语言的,好的与不好的,都继承下来了。
有一个东西就是从C语言沿袭过来的,但是其又是不够好的 —— 隐式类型的转换。
C语言中的类型转换
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
- 显式类型转化:需要用户自己处理。
void Test ()
{
int i = 1;
// 隐式类型转换
double d = i;
printf("%d, %.2f\n" , i, d);
int* p = &i;
// 显示的强制类型转换
int address = (int) p;
printf("%x, %d\n" , p, address);
}
#问:什么情况下允许隐式类型转换?什么情况下允许强制类型转换?
(C++继承的C语言的 —— C语言那就当然是针对于内置类型啦)
C语言是意义相近的类型允许隐式类型的准换,如:整形家族和浮点数家族。
- 整形之间转换是:小的可以转大的(提升),大的也可以转小的(截断)。
- 整形和浮点之间转换是:通过补位的方式,整形转浮点补浮点就行因为精度,浮点转整形丢掉精度。
可以说:它们都是用来表示数据的大小,只是空间的大小不一样,表示的范围不一样。还有就是浮点数的存储机制也不一样,其还能表示小数点后的精度。(本质:意义相近)
C语言中对于显示强制类型转换是意义不相近的类型,值转换后有意义。
Note:
整体来说,隐式类型还好,但是显示类型是有巨大的坑的。
在日常中最简单的insert函数实现(数据移动)中,就有问题。
void Insert(size_t pos, char ch) { size_t _size = 5; // …… int end = _size - 1; while(end >= pos) { _str[end + 1] = _str[_end]; --end; } } Insert(3, 'A'); // 没有问题 Insert(0, 'A'); // 有问题
在其中,第二个Insert就存在问题,因为在判断的时候就会发生隐式类型的转换。
在这个判断的时候应该就是end为-1,pos为0然后,退出while循环,但是此处它会悄悄地进入执行,因为在一个操作符的两端的操作数也会发生隐式类型的转换。这个时候会悄悄的产生一个变量将end进行了提升(从无符号提升成有符号),然后就会导致数据的越界。
所以,C语言留下的这个东西也是一个坑。并且没有办法因为为了兼容前面的内容,所以没有办法移除隐式转换,所以我们需要注意隐式转换的坑。
于是,便有了C++的规范化。
#问:为什么C++需要四种类型转换?
- 隐式类型转化有些情况下可能会出问题:比如数据精度丢失。
- 显式类型转换将所有情况混合在一起,代码不够清晰。
Note:因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
C++强制类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
- static_cast
- reinterpret_cast
- const_cast
- dynamic_cast
这个时候C语言的隐式类型转换的一套还是可以使用的,只不过一般编译器会报警告,不影响运送,但是会告诉你,如:浮点数转换为整形精度可能会丢失。
static_cast(static静止的)
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换 - 意义相近的类型。
int main()
{
double d = 12.34;
int a = static_cast<int>(d);
std::cout << a << std::endl;
return 0;
}
不是相近的类型不可以运用其来转换。
int main()
{
int* p = &a;
// 不支持的
int address = static_cast<int>(p);
return 0;
}
其是会报错为,如:"static_cast":无法从"int*"转换为"int",的错误(无效的)。
reinterpret_cast(reinterpret重新解释)
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型 - 意义不相关的类型。
int main()
{
double d = 12.34;
int a = static_cast<int>(d);
int* p = &a;
// 不支持的
//int address = static_cast<int>(p);
int address = reinterpret_cast<int>(p);
return 0;
}
对于const修饰的变量,不能转化,因为C++的规范会进行检查。
int main()
{
const int a = 2;
// 不支持
int* p = reinterpret_cast<int*>(&a)
*p = 3;
return 0;
}
补充:
甚至有一些地方还可以支持一些比较bug的转化。因为其相当于重新解释了,转换成完全另外一个类型。如:其实是一个指针,转换成为了一个数据大小,或者是一个数据大小转换成为了指针。
const_cast(const常量)
const_cast最常用的用途就是删除变量的const属性,方便赋值。
int main()
{
const int a = 2;
int* p = const_cast<int*>(&a)
*p = 3;
return 0;
}
一个奇葩的操作。
int main()
{
const int a = 2;
int* p = const_cast<int*>(&a);
*p = 3;
cout << a << endl;
cout << *p << endl;
return 0;
}
#问:上面的奇葩的操作的输出结果是什么?
简单的来看是不是以为是:3、3?那就错了。
p是a的地址,那*p就是a,这么想上面没错(const对象虽然不能改,但是在C++里面const修饰的变量叫做常变量,是不能被直接的修改,但是可以被间接接的修改)。
修改const变量:
就像上述,就是改const的方法。这种方式很bug,我们取到const修饰的变量的地址,然后通过地址间接的强制去改。本质的原因就是C++的const修饰的变量,并没有存储到常量区当中去,const修饰的变量与其他的变量一样,都存在栈上的,所以使用指针的方式是可以被修改的。
其实运行结果是:2、3!
而且我回很奇特的发现,我们对于数据进行监视数据是:3、3,然而实际打印出的数据却是2,3。
这个的原因是由来于编译器的优化,编译器对于const类型的变量是有优化的。比如说有一些编译器的优化是会将数据放在寄存器的,因为对于编译器来说,这个const修饰的对象,是只会读而并不会进行修改的,于是为了提高运行的效率,于是直接将数存储到了寄存器当中,你要就取。所以虽然内存中的数据被进行了更改,但是在寄存器看来就是没变。
有一些编译器的处理方式,不是放到寄存器,而是直接搞死,如同一个宏。不取直接就给初始化时的值。
所以也就是为什么 const_cast 也可以作为毫不相关的转换(重新解释的转换)。 const_cast 单独形成一类,也就是为了告诉我们这个地方很危险。还有一个方法可以解决这个问题:volatile(关键字),就是为了告诉编译器这个地方不要进行优化,直接去内存中取这个值。
总结
- 兼容C隐式类型转换和强制类型转换。
- 期望我们需要使用C语言的,期望我们用规范的C++显示的强制类型转换。
-
static_cast (隐式类型转换) 、reinterpret_cast、const_cast (强制类型转换) 。
dynamic_cast(dynamic动态)
是C语言没有的,是C++自己增加的,适用于向下转化。dynamic_cast用于将一个父类对象的指针 / 引用转换为子类对象的指针或引用(动态转换)。
- 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则) —— 天然支持
严格的来说:向上转型是不属于隐式类型转换,也不属于显示类型转换。它可以说是C++的一个特例,其不需要进行转换,它是赋值兼容的。
- 向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
- dynamic_cast只能用于父类含有虚函数的类 —— 否者编译直接报错。
-
dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0。
class A
{
public :
virtual void f(){}
};
class B : public A
{};
int main ()
{
B bb;
A aa = bb;
A& ra = bb;
return 0;
}
dynamic_cast是适用于向下转型,父转子(用dynamic_cast转型是安全的)。因为按道理父类对象是无论如何都不能转子类的,就是使用dynamic_cast也是不行的。
class A
{
public :
virtual void f(){}
};
class B : public A
{};
int main ()
{
A aa;
// 父类对象无论如何都是不允许转换成子类对象的
// B bb = dynamic_cast<B>(aa);
// B bb = (B)aa;
return 0;
}
对象是怎么样都不允许转的,但是指针与引用是要允许转的。因为涉及到一个指针,是有可能指向父类,也有可能指向子类。而如果是父类指针指向是父类的对象,那么就会导致出现越界问题。而如果是父类指针指向是子类的对象,那就没有问题。于是C++提供了一个dynamic_cast,让我们的行为是安全的,即:
- 如果父类指针指向的是子类的对象,那么可以转换,转换表达式返回正确的地址。
- 如果父类指针指向的是父类的对象,那么不能转换,转换表达式返回nullptr。
class A
{
public :
virtual void f(){}
};
class B : public A
{};
int main ()
{
// 如果pa是指向子类,那么可以转换,转换表达式返回正确的地址
// 如果pa是指向父类,那么不能转换,转换表达式返回nullptr
B bb;
A* pa = bb;
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;
}
}
总结:
其中,向上转型就是所说的切割/切片,是语法天然支持的,不需要进行转换,而向下转型是语法不支持的,需要进行强制类型转换。
#include <iostream>
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;
};
int main()
{
B bb;
A1* ptr1 = &bb;
A2* ptr2 = &bb;
std::cout << ptr1 << std::endl;
std::cout << ptr2 << std::endl << std::endl;
B* pb1 = (B*)ptr1;
B* pb2 = (B*)ptr2; // C语言的强制类型转换是回得到开头的
std::cout << pb1 << std::endl;
std::cout << pb2 << std::endl << std::endl;
B* pb3 = dynamic_cast<B*>(ptr1);
B* pb4 = dynamic_cast<B*>(ptr2);
std::cout << pb3 << std::endl;
std::cout << pb4 << std::endl << std::endl;
return 0;
}
Note:强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。强烈建议:避免使用强制类型转换。
RTTI
C++通过以下方式来支持RTTI:
- typeid运算符 —— 拿到变量的类型的字符串。
- dynamic_cast运算符 —— 父类的指针指向父类对象,还是子类对象,只能用于父类含有虚函数的类(多态)。
- decltype —— 推导一个对象的类型,这个类型可以用来定义另一个对象。
常见面试题
#问:C++中的4种类型转换分别是:____ 、____ 、____ 、____。
static_cast、reinterpret_cast、const_cast和dynamic_cast。
#问:说说4种类型转换的应用场景。
- static_cast用于相近类型的类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast。
- reinterpret_cast用于两个不相关类型之间的转换。
- const_cast用于删除变量的const属性,方便赋值。
- dynamic_cast用于安全的将父类的指针(或引用)转换成子类的指针(或引用)。