author: hjjdebug
date: 2025年 05月 18日 星期日 20:28:52 CST
descrip: c/c++数据类型转换.
文章目录
本来数据是什么类型就是什么类型,一般是不需要转换的,
但某些特殊情况下会发生转换.
1. 为什么需要类型转换?
因为类型不匹配,所以要进行类型转换.
1.1 发生的时机:
- 当进行赋值和运算时有可能发生数据转换.
- 进行函数调用时,有可能会发生数据类型转换.
有可能是说类型不匹配时,会按照一定规则进行变换,如果不是自定义的规则,
那就是编译器隐含的规则.
1.2 常见的发生转换的类型:
- 整形变成浮点型,浮点型变成整形.
- 子类指针退化为父类(上变换)和父类指针变换为子类(下变换)
- 自定义的类型变换规则.
2. c语言的类型转换: (Type) value
举例:
$ cat main.cpp
#include <stdio.h>
int main()
{
int i = 1;
double d = i; // 隐式类型转换
printf("i:%d, d:%.2f\n",i,d);
int* ptr = &i;
long address = (long)ptr; // 显示的强制类型转换
printf("ptr:%p, address:%ld\n",ptr,address);
return 0;
}
执行:
$ ./Test
i:1, d:1.00
ptr:0x7ffca9a5b0bc, addr:140723154694332,addr:0x7ffca9a5b0bc
2.1 c语言的类型变换是如何实现的? 规则是什么?
4 int i = 1;
0x00005555555546c1 <+23>: movl $0x1,-0x24(%rbp) //把1赋值给变量i
5 double d = i; // 隐式类型转换
0x00005555555546c8 <+30>: mov -0x24(%rbp),%eax //取到数值i
0x00005555555546cb <+33>: cvtsi2sd %eax,%xmm0 //变成浮点数
0x00005555555546cf <+37>: movsd %xmm0,-0x20(%rbp) //保存数值到d变量
7 int* ptr = &i;
0x00005555555546f7 <+77>: lea -0x24(%rbp),%rax //取到i的地址
0x00005555555546fb <+81>: mov %rax,-0x18(%rbp) //把地址保存到ptr变量
8 long address = (long)ptr; // 显示的强制类型转换
0x00005555555546ff <+85>: mov -0x18(%rbp),%rax //从ptr地址中取到地址(解引用的意思)
0x0000555555554703 <+89>: mov %rax,-0x10(%rbp) //把地址保存到address 变量
可见地址类型就天然等于long 类型, 它们数值时完全相等的.
c语言会按照它默认规则进行转换,例如把long 转为 int 它也给转,当正确性你自己保证
c++认为c的强制类型转换(type)value 方式太过强暴, 将之分解为4重转换类型,
static_cast, 静态转换
dynymic_cast, 动态转换
reinterpreted_cast, 再解释转换
const_cast, 常变换.
3. c++ 的static_cast
测试代码:
#include <iostream>
using namespace std;
int main()
{
double d = 1.23;
int a = static_cast<int>(d);
cout << a << endl;
int* p = &a;
// int address = static_cast <int>(p); //error: invalid static_cast from type ‘int*’ to type ‘int’
// long address = static_cast <long>(p); //error: invalid static_cast from type ‘int*’ to type ‘long int’
long address = reinterpret_cast <long>(p); //error: invalid static_cast from type ‘int*’ to type ‘long int’
cout << address << endl;
return 0;
}
我们看到static_cast 把浮点数到int的变换与c下的转换时完全一致的.
汇编代码如下:
5 double d = 1.23;
=> 0x0000555555554892 <+8>: movsd 0x136(%rip),%xmm0 # 0x5555555549d0
0x000055555555489a <+16>: movsd %xmm0,-0x8(%rbp)
6 int a = static_cast(d);
0x000055555555489f <+21>: movsd -0x8(%rbp),%xmm0
0x00005555555548a4 <+26>: cvttsd2si %xmm0,%eax
0x00005555555548a8 <+30>: mov %eax,-0xc(%rbp)
但它拒绝将整形地址向整形变量转换,也拒绝向long 整形变量转换. 编译会直接给出错误.
可见对于static_cast, 它的转换实现与c下的强制转换是一样的.
但对于高风险的转换,它不干了.
如果你确实需要把指针变成整数怎么办,那你可以用另一个关键字reinterpret_cast, 它的要求
会放宽,允许你转换,不过后果自负喽!, 你自己负责使用时的正确性.
4. c++的reinterpret_cast
从上面的例子可以看出,static_cast 对风险高的转换它不干了,让reinterpret_cast来承担风险. reinterpret_cast 的转换方式跟c下的强制转换是一样的.
5. dynymic_cast 的使用.
其主要是为了完成指针类型的上变换(辈份变大了和下变换辈分变小了)
也可能辈分没有变,但从一个基类指针变到了另一个基类指针.
c++的类和继承关系, 一个形象的理解是一个类就对应一个椭圆,子类继承了父类,那就是一个大椭圆套住了一个小椭圆,
继承了2个基类,那就是套住了2个小椭圆.
static_cast 可以处理这种有继承关系的对象指针的转换.
dynymic_cast 也可以处理这种有继承关系的对象指针的转换. 但它是动态的,支持运行时监测.
完成这种转换是需要类型信息的, 下面举例说明转换是怎样进行的,
5.1: 测试代码
$ cat main.cpp
#include <iostream>
using namespace std;
// 假设有2个基类 Base1和Base2 和一个派生类 Derived,各类型指针转换会是什么?
class Base1
{
public:
// error: cannot dynamic_cast ‘p’ (of type ‘class Base*’) to type ‘class Derived*’ (source type is not polymorphic)
// 必需要有一个虚函数,否则编译错误说类型不具有多态性.
virtual void foo() {cout<<"from Base1::foo\n";}
};
class Base2
{
public:virtual void bar() {cout<<"from Base2::bar\n";}
};
class Derived : public Base1, public Base2
{
public:
void compose() { cout<< "from Derived:compose\n";}
};
int main()
{
Derived * pd = new Derived();
// 用基类指针,指向一个派生类对象
Base1* p1 = pd;
Base2* p2 = pd;
//当你看到p1,p2的不同,不要奇怪,这里有隐含指针变换
cout<<"p1:"<<p1<<",p2:"<<p2<<",pd:"<<pd<<endl;
// 使用 dynamic_cast 将 p1 转换为派生类指针 q, 下变换
// 能变换吗? 能,因为p1 本来就是派生类对象的指针.
// 如果p1 是用 new Base() 创建出来的, 那就转不成继承类指针了!
// 转不成了变换的值是nullptr.
// dynamic_cast<Derived *> 可看成是一个编译器生成的内置函数
// 变化比较复杂. 有对指针的调整. 需要RTTI 运行时类型信息支持.
Derived* q1 = dynamic_cast<Derived*>(p1);
cout<<"p1:"<<p1<<",q1:"<<q1<<endl; //p1 和q1 是相等的.
Derived* q2 = dynamic_cast<Derived*>(p2);
//当你看到转出的q1,q2竟然相等,也不用奇怪,因为它们就是一个对象
cout<<"q1:"<<q1<<",q2:"<<q2<<",p1:"<<p1<<",p2"<<p2<<endl;
// 甚至从Base1 也能导出Base2指针,都是一个对象,它们是可以互相推导计算的.
// 推导计算的法则是根据RTTI信息,计算就是加一个偏移,减一个偏移的事情.
// 如果转换成功,q1 不为空,可以调用派生类的成员函数 compose()
if (q1)
{
q1->compose();
}
else
{
cout << "down convert failed\n";
}
return 0;
}
5.2 statci_cast 处理父子类对象地址
继承类指针向基类指针的隐含转换.
24 // 用基类指针,指向一个派生类对象
25 Base1* p1 = pd;
0x00000000004008f8 <+49>: mov -0x38(%rbp),%rax
0x00000000004008fc <+53>: mov %rax,-0x30(%rbp) //直接赋值
26 Base2* p2 = pd;
0x0000000000400900 <+57>: cmpq $0x0,-0x38(%rbp) //pd==0? 付给0
0x0000000000400905 <+62>: je 0x400911 <main()+74>
0x0000000000400907 <+64>: mov -0x38(%rbp),%rax
0x000000000040090b <+68>: add $0x8,%rax //把值加上8,付给p2
0x000000000040090f <+72>: jmp 0x400916 <main()+79>
0x0000000000400911 <+74>: mov $0x0,%eax
0x0000000000400916 <+79>: mov %rax,-0x28(%rbp)
36 Derived* q1 = static_cast<Derived*>(p1); //静态变换,也叫编译期变换
0x0000000000400999 <+210>: mov -0x30(%rbp),%rax
0x000000000040099d <+214>: mov %rax,-0x20(%rbp) // p1直接付给了q1
// p2付q2, 它就改一改算法, 你说gcc多聪明啊! 它了解意图.
38 Derived* q2 = static_cast<Derived*>(p2);
0x00000000004009ff <+312>: cmpq $0x0,-0x28(%rbp) // p2==0? 付给0
0x0000000000400a04 <+317>: je 0x400a10 <main()+329>
0x0000000000400a06 <+319>: mov -0x28(%rbp),%rax
0x0000000000400a0a <+323>: sub $0x8,%rax // p2减去8,付给q2
0x0000000000400a0e <+327>: jmp 0x400a15 <main()+334>
0x0000000000400a10 <+329>: mov $0x0,%eax
0x0000000000400a15 <+334>: mov %rax,-0x18(%rbp)
5.3 动态变换: dynamic_cast 处理父子类对象地址
36 Derived* q1 = dynamic_cast<Derived*>(p1);
0x00000000004009e9 <+210>: mov -0x30(%rbp),%rax
0x00000000004009ed <+214>: test %rax,%rax
0x00000000004009f0 <+217>: je 0x400a0f <main()+248> //p1==0? 付给0
0x00000000004009f2 <+219>: mov $0x0,%ecx //第4参数,0,偏移数据
0x00000000004009f7 <+224>: mov $0x601da0,%rdx //第3参数,类型信息
0x00000000004009fe <+231>: mov $0x601de8,%rsi //第2参数,类型信息
0x0000000000400a05 <+238>: mov %rax,%rdi // 第一参数p1
0x0000000000400a08 <+241>: callq 0x400810 __dynamic_cast@plt//调用函数
0x0000000000400a0d <+246>: jmp 0x400a14 <main()+253>
0x0000000000400a0f <+248>: mov $0x0,%eax
0x0000000000400a14 <+253>: mov %rax,-0x20(%rbp) //返回值给q1
38 Derived* q2 = dynamic_cast<Derived*>(p2);
0x0000000000400a76 <+351>: mov -0x28(%rbp),%rax
0x0000000000400a7a <+355>: test %rax,%rax
0x0000000000400a7d <+358>: je 0x400a9c <main()+389>
0x0000000000400a7f <+360>: mov $0x8,%ecx //第4参数,8,偏移数据
0x0000000000400a84 <+365>: mov $0x601da0,%rdx //第3参数,类型信息
0x0000000000400a8b <+372>: mov $0x601dd8,%rsi //第2参数,类型信息
0x0000000000400a92 <+379>: mov %rax,%rdi /// 第一参数p2
0x0000000000400a95 <+382>: callq 0x400810 __dynamic_cast@plt
0x0000000000400a9a <+387>: jmp 0x400aa1 <main()+394>
0x0000000000400a9c <+389>: mov $0x0,%eax
0x0000000000400aa1 <+394>: mov %rax,-0x18(%rbp)//返回值给q2
注意: -0x18(%rbp)与-0x20(%rbp)是相差8个byte 而不是2个byte, 一走神有点犯晕.
刚好存储长整形地址.
5.4 为什么指针类型老是变来变去的,是什么类型就是什么类型,不变不好吗?
不变确实挺好,能不变就不要变.
但是c++的2大要点是继承和多态. 另一大要点是封装这就不说了.
就是例子中一个类继承了2个类,本来你用导出类指针是什么都能访问到的.
但是,如果有一个函数,它要求你传入基类的地址而不是导出类地址,这就需要转换了.
把地址调一调,才能访问到正确的数据.
用基类地址还能访问到继承类的函数(虚函数),这就是多态.
运行结果:
$ ./Test
p1:0x16b6e70,p2:0x16b6e78,pd:0x16b6e70
p1:0x16b6e70,q1:0x16b6e70
q1:0x16b6e70,q2:0x16b6e70,p1:0x16b6e70,p20x16b6e78
from Derived:compose
对运行结果的解释
$ ./Test
//p1和p2是不等的(从一个导出类对象地址转换成的两个基类地址是不等的),
//p1等于pd(其中一个基类地址跟导出类地址相等)
p1:0x16b6e70,p2:0x16b6e78,pd:0x16b6e70
//这是逆变换, 转换出的导出类与基类地址相同,但另一个导出类和基类地址不同
p1:0x16b6e70,q1:0x16b6e70
//2个基类转换出了相同的导出类地址q1,q2, 这说明它们是一个对象
q1:0x16b6e70,q2:0x16b6e70,p1:0x16b6e70,p20x16b6e78
from Derived:compose
5.4 为什么要用 dynamic_cast ?
从上边例子中我们看出, static_cast 直接计算了偏移,很简洁, 而dynamic_cast
还要去调用函数__dynamic_cast@plt 才能得到地址,显然代价更高.
那为什么还要用dynamic_cast, 全部改为static_cast 不好吗?
上边的例子是用不着dynamic_cast, 但是, 这里有但是…
我写了一个函数, 传来的是基类指针, 我不知道它真实身份是那种子类,就需要动态指针变换,由此判定它是什么类型. 这就是运行期判断,
测试代码:
#include <iostream>
using namespace std;
class Base {
virtual void foo(){}
};
class Derived1 : public Base
{
};
class Derived2 : public Base
{
};
void deal_it(Base *b)
{
auto d1 = dynamic_cast<Derived1 *>(b);
// auto d1 = static_cast<Derived1 *>(b);
if(d1)
{
// 处理Derived1类型
cout<<"is Derived1 type\n";
return;
}
auto d2 = dynamic_cast<Derived2 *>(b);
// auto d2 = static_cast<Derived2 *>(b);
if(d2)
{
// 处理Derived2类型
cout<<"is Derived2 type\n";
return;
}
}
int main()
{
auto b = new Derived2();
deal_it(b);
return 0;
}
测试结果:
$ ./Test
is Derived2 type
如果你将dynamic_cast改称static_cast, 那就得不到正确结果了, 因为static_cast 不管三七二十一,把指针加个偏移就返回了.
这样转换的指针不为0,就永远只走第一条了.
至于为什么dynamic_cast 能正确工作, 就不再这里说明了.
自定义的类型变换就不在本博举例了,可参考其它博客.