C/C++是强类型语言,不同类型之间的相互转换是比较麻烦的.但是在编程实践中,不可避免的要用到类型转换.
类型转换:
- 隐式类型转换
- 强制类型转换
- 隐式类型转换
- 提升精度,此种是编译器自动完成的,安全的.所以编译的时候不会有任何错误或者警告信息提示.
示例:
int ival = 3;
double dval = 3.14159;
// ival 被提升为 double 类型: 3.0
ival + dval;
- 降低精度,也是有编译器自动完成,会造成精度丢失,所以编译时得到一个
警告信息
提示.
示例:
double dval = 3.14159;
// dval的值被截取为 int 值3
int ival = dval;
- 显式类型转换
- C风格的强制转换(包括旧式C++风格的强制转换)
格式:
类型(表达式); // 旧的C++风格
或者
(类型)表达式 // C风格
示例: int(dval) 或者 (int)dval
此种强制转换是比较粗暴直接的,有可能导致精度丢失
(如从 double 转换为 int)或者一些莫名其妙的错误(如把 int 转换为 函数指针),一旦使用了强制转换,编译器将不提示任何警告
.这也往往成为错误的源泉.而且这种错误非常难找.我想这也是C++11要使用新的强制转换操作符的原因
之一吧.
- C++强制转换操作符
C++增加了4个关键字用于强制类型转换:
static_cast, reinterpret_cast, const_cast 和 dynamic_cast.
-
const_cast 用来移除 const
- 常量指针被转化成非常量的指针,并且仍然指向原来的对象;
- 常量引用被转换成非常量的引用,并且仍然指向原来的对象;
- const_cast一般用于修改底指针。如const char *p形式。
- 使用const_cast去掉const属性,其实并不是真的改变原类类型(或基本类型)的const属性,它只是又提供了一个接口(指针或引用)
const int g = 20; int *h = const_cast<int*>(&g);//去掉const常量const属性 const int g = 20; int &h = const_cast<int &>(g);//去掉const引用const属性 const char *g = "hello"; char *h = const_cast<char *>(g);//去掉const指针const属性 const A *pca1 = new A; A *pa2 = const_cast<A*>(pca1); //常量对象转换为非常量对象 pa2->m_iNum = 200; //常量对象被转换成非常量对象时出错 const A ca; A a = const_cast<A>(ca); //不允许 const int i = 100; int j = const_cast<int>(i); //不允许
-
dynamic_cast 需要 RTTI 支持, 主要用于把
基类指针转换为派生类指针
.这里的基类指针其实是指向一个派生类实例
,只是类型为基类
.
示例:
#include <iostream>
#include <string>
using namespace std;
class Base
{ //有虚函数,因此是多态基类
public:
virtual ~Base() {}
};
class Derived : public Base { };
int main()
{
Base b;
Derived d;
Derived* pd;
pd = reinterpret_cast <Derived*> (&b);
if (pd == NULL)
//此处pd不会为 NULL。reinterpret_cast不检查安全性,总是进行转换
cout << "unsafe reinterpret_cast" << endl; //不会执行
pd = dynamic_cast <Derived*> (&b);
if (pd == NULL) //结果会是NULL,因为 &b 不指向派生类对象,此转换不安全
cout << "unsafe dynamic_cast1" << endl; //会执行
pd = dynamic_cast <Derived*> (&d); //安全的转换
if (pd == NULL) //此处 pd 不会为 NULL
cout << "unsafe dynamic_cast2" << endl; //不会执行
return 0;
}
// 前提假设: class B 由 class A 派生
A *ptrA = new class B;
B *ptrB = dynamic_cast<B*>(ptrA);
-
static_cast 运算符完成
相关类型
之间的转换所谓"相关类型"指的是从逻辑上来说,多多少少还有那么一点联系的类型,比如从 double 到 int,我们知道它们之间还是有联系的,只是精度差异而已,使用 static_cast 就是告诉编译器:我知道会引起精度损失,但是我不在乎. 又如从 void* 到 具体类型指针像 char*,从语义上我们知道 void* 可以是任意类型的指针,当然也有可能是 char* 型的指针,这就是所谓的"多多少少还有那么一点联系"的意思. 又如从派生类层次中的上行转换(即从派生类指针到基类指针,因为是安全的,所以可以用隐式类型转换)或者下行转换(不安全,应该用 dynamic_cast 代替).
对于static_cast操作符,如果需要截断,补齐或者指针偏移编译器都会自动完成.注意这一点,是和 reinterpret_cast 的一个根本区别.
class A {
public:
int m_a;
};
class B {
public:
int m_b;
};
class C : public A, public B {};
那么对于以下代码:
C c;
printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));
这个代码中,C 同时继承自 A 和 B。
打印结果分析:
1. 第一个%p打印出对象c的地址,表示整个C对象的起始地址。
2. 第二个%p使用reinterpret_cast进行强制转换,将C*转换为B*,实际上进行了字节复制,没有调整偏移量。
3. 第三个%p使用static_cast进行转换,考虑了多继承的偏移量,转换结果正确。
所以打印结果可能是:
0x100, 0x100, 0x114
reinterpret_cast直接转换的地址和C对象起始地址一样。
而static_cast转换的地址考虑了继承带来的偏移量,比C对象起始地址要大。
前两个的输出值是相同的,最后一个则会在原基础上偏移4个字节,这是因为static_cast计算了父子类指针转换的偏移量,并将之转换到正确的地址(c里面有m_a,m_b,转换为B*指针后指到m_b处),而reinterpret_cast却不会做这一层转换。
因此, 你需要谨慎使用 reinterpret_cast.
-
reinterpret_cast 处理
互不相关的类型
之间的转换."互不相关的类型"指的是两种完全不同的类型,如从整型到指针类型,或者从一个指针到另一个毫不相干的指针.
*
示例:int ival = 1; double *dptr = reinterpret_cast<double*>(ival); 或者 int *iptr = NULL; double *dptr = reinterpret_cast<double*>(iptr);
reinterpret_cast 操作执行的是比特位拷贝,就好像用 memcpy() 一样.
int *iptr = reinterpret_cast<int*>(1); double *dptr = reinterpret_cast<double*>(2); memcpy(&dptr, &iptr, sizeof(double*)); // 等效于 dptr = reinterpret_cast<double*>(iptr); 结果 dptr 的值为1;
上面这个示例也说明了 reinterpret_cast 的意思:编译器不会做任何检查,截断,补齐的操作,只是把
比特位拷贝
过去.
所以 reinterpret_cast 常常被用作不同类型指针间的相互转换,因为所有类型的指针的长度都是一致的(32位系统上都是4字节),按比特位拷贝后不会损失数据.
编程实践中几种典型的应用场景
-
数值精度提示或者降低,包括把无符号型转换为带符号型(也是精度损失的一种),
用 static_cast
可以消除编译器的警告信息,前面提到好几次了.int a = 7; int b = 3; double result = static_cast<double>(a) / static_cast<double>(b);
-
任意类型指针到 void*,
隐式类型转换,
自动完成. 看看 memcpy 的原型void *memcpy( void *dest, const void *src, size_t count );
参数定义为 void* 是有道理的,不管我们传入什么类型的指针都符合语义,并且不会有编译器警告.
-
void* 到任意类型指针,
用
static_cast 和 reinterpret_cast
都可以,这是由 void* 是通用指针这个语义决定的.我个人倾向用 reinterpret_cast,表达要"重新解释"指针的语义.double a = 7; void* p = &a; double* dp = static_cast<double*>(p); double* dp = reinterpret_cast<double*>(p);
-
不同类型指针间的相互转换用
reinterpret_cast
. -
int 型和指针类型间的相互转换
用 reinterpret_cast.
比如我写代码的时候经常这样做: new 一个 struct,然后把指针返回给外部函数作为一个"句柄",我不希望外部函数知道这是一个指针,只需要外部函数在调用相关函数时把这个"句柄"重新传回来.这时,就可以把指针转换为一个 int 型返回. 这是 reinterpret_cast 存在的绝佳理由.
struct car { int doors; int height; int length; float weight; }; int create_car() { car *c = new car; return reinterpret_cast<int>(c); } int get_car_doors(int car_id) { car *c = reinterpret_cast<car*>(car_id); return c->doors; } void destroy_car(int car_id) { car *c = reinterpret_cast<car*>(car_id); delete c; }
如上,外部函数不需要知道 struct car 的具体定义,只需要调用 create_car() 得到一个 car id,然后用此 car_id 调用其他相关函数即可,至于 car_id 是什么,根本没必要关心.
-
派生类指针和基类指针间的相互转换.
派生类指针到基类指针
用隐式类型转换
(直接赋值)或者用static_cast
. 显然不应该也没必要用 reinterpret_cast.在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的 -
基类指针到派生类指针用
dynamic_cast
(运行期检查)或者static_cast
(运行期不检查,由程序员保证正确性). dynamic_cast具有类型检查的功能,比static_cast更安全。考虑到C++对象模型的内存分布可能引起的指针偏移问题,绝对不能用 reinterpret_cast. -
求offset
如果父类和子类都没有虚函数,那么在单继承情况下,子类对象中确实可以完全包含父类对象,起始地址没有偏移量。
主要原因如下:
1. 没有虚函数,所以不需要额外的虚函数表指针。
2. 构造析构函数可以直接继承复用。
3. 没有多继承,字段顺序一致。
4. 内存对齐要求相同。
所以如果父子类都没有虚函数,单继承下,子类对象可以完整包含父类对象,它们的起始地址可以相同,不会有偏移量。
实际编译结果也可以验证这一点。
如果父类没有虚函数,子类有一个虚函数的单继承场景,那么子类对象与父类对象之间确实存在一个虚函数表指针大小的偏移量。
分析如下:
- 父类没有虚函数,不需要虚函数表。
- 子类有虚函数,需要引入虚函数表指针。
- 该虚函数表指针会在子类对象的开头。
因此,子类对象相对于父类对象就多了一个虚函数表指针的偏移量。
这个偏移量正好就是一个指针的大小(通常是4字节或8字节)。
#define OFFSET(base, inherit) (reinterpret_cast<char*>(0x400) \
- reinterpret_cast<char*>(static_cast<base*>(reinterpret_cast<inherit*>(0x400))))
#define DOWNCAST(pBase, base, inherit) reinterpret_cast<inherit*>(reinterpret_cast<char*>(pBase) + OFFSET(base, inherit))
reinterpret_cast<char*>(1) - reinterpret_cast<char*>(static_cast<base*>(reinterpret_cast<inherit*>(1))))
中的static_cast<base*>(reinterpret_cast<inherit*>(1))部分:
- reinterpret_cast<inherit*>(1)先获取inherit类对象的地址
- 然后static_cast将inherit转换为base
- 这个转换计算出了inherit相对于base的偏移量
- 并按照这个偏移量将inherit地址转换为base地址
static_cast计算了父子类指针之间的偏移量,并基于这个偏移量将inherit地址转换为base地址。
它实现了根据继承关系,从子类指针得到基类指针的转换。
继承内存布局:
- 继承会把父类的Layout直接放入派生类中
- 然后在此基础上,子类再新增自己的成员
- 父类子对象的大小和布局不会发生变化
- 所以父类部分位于内存较低的位置
- 子类新增内容则位于父类内容之后,因此是较高的地址
但是,如果父类中含有虚函数,还需要考虑虚函数表指针的影响,它通常会位于最前端。