C++类型转换
1. const_cast
const_cast
可以将const转换成非const,也可以将非const转换成const。需要注意的是 const_cast
只能用于改变指针或者引用的底层const。
底层const和顶层const
首先要弄清楚 const
修饰的到底是谁,用顶层表示指针本身是个常量(指针常量),底层表示指针所指向的对象是个常量(常量指针)。
int* const p1 = &i1;
int const* p2 = &i2;
const int i3 = 3;
int const i4 = 4;
const int* p3 = &i5;
const int* const p4 = &i3;
const int const* p5 = &i6;
const int& r = i7;
const
默认是修饰它左边的符号的,如果左边没有,那么就修饰它右边的符号,比如例子中
- 第一个
const
左边是int*
,表示指针不能修改,所以是顶层const - 第二个修饰的
int
,表示指针指向的值不能修改,所以是底层const - 第三个
const
左边没有,修饰右边符号int
,这个非指针非引用,没有底层顶层的概念 - 第四个修饰左边
int
和第三个一样 - 第五个左边没有,修饰右边
int
,注意不是修饰int*
- 第六个第一个
const
左边没有,修饰右边int
是底层;第二个const
修饰左边int*
是顶层 - 第七个两个
const
都是修饰int
,都是底层,这个指针是可以修改指向对象的 - 第八个第一个
const
修饰int
,是底层const;引用默认了顶层const
,因为引用一旦初始化就不能再改为其他对象的引用。
关于顶层const和底层const可以看看这篇文章C++干货系列——顶层const和底层const
const_cast只可以对指针和引用使用
const_cast
针对指针和引用是语法上的要求,把一个常量变量变成非常量变量不应该是我们的目的。我们不应该为了修改一个const变量的值而去使用,例如我们有一个常量 const_a
,但是我们想去修改它的值于是决定用一个常量指针指向它,然后用 const_cast
让常量指针变成普通指针,修改这个变量
const int const_a = 66;
const int* const_p = &const_a;
int* p = const_cast<int*>(const_p);
*p = 88;
std::cout << *p << std::endl;//输出结果88
std::cout << const_a << std::endl;//输出结果66
理论上这个变量的内存的内容确实改变了,我们输出 *p
的结果也证实了这一点。但是当我们输出 const_a
的值会发现结果依然是66。难道是他们的内存地址不一样?
std::cout << &const_a << std::endl;//000000730AAFF5C4
std::cout << p << std::endl; //000000730AAFF5C4
事实上他们确实是同一块内存地址。真实的原因是编译器将const变量直接用66替换了。加const只是告诉编译器不能修改而不是真正地不可修改,如果程序员不注意而去修改了它会报错,现在我们利用const_cast去除了常量性,然后通过指针和引用对其进行了修改,所以通过指针打印或者引用传参的时候就能看出其内存确实变化了,但为了保护这个变量本来的const特性,所以每次我们使用const变量时,系统都将其替换成初始值66,确保了const变量还是“不可变”的。可以参考文章const_cast的小问题
这种情况是一种未定义行为,所谓未定义,是说这个语句在标准C++中没有明确的规定,由编译器来决定如何处理。很可能换一个编译器,处理结果就完全不同,我们要避免未定义行为的发生。
什么时候使用const_cast
const_cast
的目的,在于某些变量原本不是const的,但由于某种特殊原因,无意间被变成了const的。例如使用了一个const引用指向了一个本来不是const的对象。结果写了一些代码之后发现它实际上需要被修改。这在平时的工作中不会遇到因为你可以直接把const引用修改成非const的,但C++中可能的情况太多,尤其考虑到很多复用的时候,有时还是会出现本不该是const的对象被const引用了这种情况。
int k=10;
const int& ref=k;
int& x=const_cast<int&>(ref);
x=11;
还要一些情况,例如我们需要在类里面重载一个const函数和非const函数。这么做的目的很明确,因为不能在一个常量对象中调用非常成员函数,这是因为函数需要传入隐式的 this
指针,在默认情况下,this
的类型是指向类的非常量版本的常量指针(意思是 this
的值不能改变,永远指向那个对象,即“指针常量”,但是被 this
指向的对象本身是可以改变的,因为是非常量版本,这里 this
相当于是顶层const),而 this
尽管是隐式的,它仍然需要遵循初始化规则,普通成员函数的隐式参数之一是一个底层非const指针,在默认情况下我们无法把一个底层const的 this
指针转化为非const的 this
指针,因此我们不能在常量对象上调用普通的成员函数。可以参考文章常量对象只能调用常成员函数 原因分析
所以为了让常量对象能调用同样的函数,我们需要重载一个常量版本。
class A
{
public:
void fun()
{
std::cout << "非常量版本" << std::endl;
}
};
int main()
{
A a;
const A const_a;
a.fun();
const_a.fun();//错误,对象含有与成员函数不兼容的类型限定符
}
这样就会报错:对象含有与成员函数不兼容的类型限定符
我们需要加一个常量成员函数
class A
{
public:
void fun()
{
std::cout << "非常量版本" << std::endl;
}
void fun() const
{
std::cout << "非常量版本" << std::endl;
}
};
int main()
{
A a;
const A const_a;
a.fun();
const_a.fun();
}
如果这两个版本做的事情完全一样,那么我们并不需要写两份一样的代码,可以利用 const_cast
这样做
class A
{
public:
void fun()
{
std::cout << "hello" << std::endl;
}
void fun() const
{
const_cast<A*>(this)->fun();
}
};
int main()
{
A a;
const A const_a;
a.fun();
const_a.fun();
}
参考文章C++中const_cast的作用和缘由和【C++】const_cast基本用法(详细讲解)
2. dynamic_cast
dynamic_cast
:动态类型转换,用于将基类的指针或引用安全地转换成派生类的指针或引用(也可以向上转换),若指针转换失败返回NULL,若引用返回失败抛出bad_cast异常。
什么时候使用dynamic_cast
如果我们有一个 Eneity
类,Player
和 Enemy
继承了这个类。我们用基类指针e1和e2分别指向Player和Enemy。现在我有了基类指针,但我想把基类的指针分别转型到它们自己的类别指针上面。但可能我并不清楚e1和e2实际指向的到底是Player类还是Enemy类。如果我做了错误的转型,我把实际指向Enemy类的e2转型成了Player类,并且调用了函数 func
,最后输出的是“I am Player”。这不是我们想要的,如果我们尝试做一些Enemy没有,而Player所独有的事情,比如访问特定的成员变量,只有player有,而enemy没有,因为潜在的类型实际上是Enemy,我们的程序很可能崩溃,或者做出些我们不想做的奇怪的事情。
class Entity
{
};
class Player : public Entity
{
public:
void func()
{
std::cout << "I am Player";
}
};
class Enemy : public Entity
{
public:
void func()
{
std::cout << "I am Enemy";
}
};
int main()
{
Entity* e1 = new Player();
Entity* e2 = new Enemy();
Player* p = (Player*)e2;
p->func(); //输出“I am Player”
}
为了避免一些安全问题,我们可以使用 dynamic_cast
。dynamic_cast
是在运行时进行安全性检查;使用 dynamic_cast
父类一定要有虚函数,否则编译不通过;
我们给基类加一个虚析构函数就行了。最终 dynamic_cast
返回一个空指针,转型失败
class Entity
{
public:
virtual ~Entity() {};
};
class Player : public Entity
{
public:
void func()
{
std::cout << "I am Player";
}
};
class Enemy : public Entity
{
public:
void func()
{
std::cout << "I am Enemy";
}
};
int main()
{
Entity* e1 = new Player();
Entity* e2 = new Enemy();
Player* p = dynamic_cast<Player*>(e2);
if (p)
p->func();
else
std::cout << "转型失败" << std::endl;
}
dynamic_cast的原理
dynamic_cast
是怎么做到能知道Player是Player,Enemy是Enemy的呢?这是因为他用到了运行时类型信息RTTI(Runtime Type Information)。RTTI的实现原理是通过在虚表中放一个额外的指针,每个新类只产生一个typeinfo实例,额外指针指向typeinfo, typeid返回对它的一个引用 。类的内存布局可以看这篇文章C++类内存布局与虚继承。
我们来看上面示例的布局
第一个指针就是指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。只要对比type_info就能知道是否能进行转型。虽然这么做会消耗资源,但也是不得已而为之。
RTTI不仅仅有 dynamic_cast
,要更详细了解可以看视频明解 C++ Primer:19.2运行时类型识别RTTI,讲的是C++Primer730页的内容。
3. static_cast
static_cast
: 隐式类型转换,可以实现C++中内置基本数据类型之间的相互转换,enum、struct、 int、char、float等,能进行类层次间的向上类型转换和向下类型转换(向下不安全,因为没有进行动态类型检查)。它不能进行无关类型(如非基类和子类)指针之间的转换,也不能作用包含底层const的对象;
4. reinterpret_cast
reinterpret_cast
:reinterpret是重新解释的意思,此标识符的意思即为将数据的二进制形式重新解释,但是不改变其值,有着和C风格的强制转换同样的能力。它可以转化任何内置的数据类型为其他任何的数据类型,也可以转化任何指针类型为其他的类型。它甚至可以转化内置的数据类型为指针,无须考虑类型安全或者常量的情形。不到万不得已绝对不用(比较不安全)