前面文章我们讲过dynamic_cast —> RTTI,static_cast;今天再讲讲reinterpret_cast ,const_cast,这些就是C++里面的全部类型转换,这些都熟练掌握后,写C++的类型转换会行云流水。
回顾一下之前: dynamic cast <-> RTTI(at runtime) 在一条继承链上,向上或向下做类型转换 子类 -> 父类类型,父类类型-> 子类(*)
static_cast 静态转换(在程序编译期就转换好了)、隐式转换
float_t flt = 1.1f; int32_t num = static_cast<int32_t>(flt); int32_t num2 = flt; // 不推荐省略强制类型转换可以把任何类型转换成我想要的样子,所以强制类型转换很危险。在强制转换之前你一定要知道你在干什么,你不知道就不要进行强制转换了。
// numPtr 内存的是num 这个数字所在的内存地址 int32_t numPtr = # // 0x000fffed (我随便写的一个地址) // 把一个指针强制转换成一个类型 int64_t numPtrAsInt = reinterpret_cast<int64_t>(numPtr);大家看看我这样写行不行
BaseClass* bC1 = reinterpret_cast<BaseClass*>(sC1); SubClass* sC2 = reinterpret_cast<SubClass*>(bC2); // 注意:代码可以编译通过,也可以执行。但是不可以这样写 // 会导致 RTTI error!程序的执行行为不可预测。我可以将子类强制转换为父类,或将父类强制转换为子类吗? 答案是不可以,强制类型转换不会像dynamic_cast一样做动态指针转换。所以在程序运行阶段会出现莫名错误,难以查找原因。
Best Practice 内存当中的地址一般情况下是不是唯一的,内存是连续的,里面的地址肯定是唯一的。很多时候呢我们会利用程序里面内存对象的首地址,把它cast成一个整型或一个数字作为它的UUID。UUID唯一标识符。怎么做,把一个指针转化成一个整型来做。尤其是开发分布式系统的时候,UUID是不是很重要呀。在某些场景下很重要,我这里建议也不要随意使用reinterpret_cast,很危险,除非你对这里面的东西很清楚,你也知道你要做什么事情。
不同电脑的地址不会存在重复吗? 会的,我们在UUID的基础上再增加别的东西,比如IP地址。公网上我们可以用IP地址,如果是内网,内网和外网有可能重复吗,其实一般也不会,网段不一样。如果IP地址还能重复的话,我们写硬件的Mac地址。
看这一段代码:
// numPtr是一个可以修改的指针地址 const int32_t* numPtrAsConst = numPtr; int32_t* numPtrAsNonConst = numPtrAsConst;将一个可以修改的numPtr变为不可修改的const 常量是可以的,const 常量转化为可以修改的类型numPtrAsNonConst这是绝对不可以做的。
那我知道这块内存原来是可以修改的,想把它变回来怎么办,用const_cast.
int32_t numPtrAsNonConst = const_cast<int32_t*>(numPtr);如果我一开始就定义一个常量,比如
const int a = 100; // 会存储到二进制文件的常量区这样,C++编译器会在编译期将数据存储到二进制数据的常量区。
int main { int32_t* a2 = const_cast<int32_t*>(&a); // Line1 *a2 = 200; // Line2 }而此时,如果我修改,第一行是可以编译通过的,但第二行会报错。程序在编译期存储在常量区,运行时是没有办法修改的。指向的内存是属于常量区。
前面的章节我们已经涉及到类和继承了,C++的继承比较复杂,因为灵活,C++是一门包罗万象的语言,用C++实现任何的功能理论上都是可以的。因为它灵活,导致了复杂性。继承里面有不少坑,今天带着大家去踩一踩。
#include <iostream> #include <cstdint> #include <cmath> class A { // 基类 public: A(): _num (0){} // 在初始化列表里把num初始化成0. virtual ~A() {} // 两个虚函数,一个修改num,一个返回num的值 virtual void SetNum(int32_t num) {_num = num;} virtual int32_t GetNum() const {return _num;} private: int32_t _num; // 这是数据 }; // public 就是C++当中的继承方式,跟C#和Java非常的相像 class B : public A { public: B(): _bNum(0) {} virtual ~B(){} // protected:继承链上不可以去访问 protected: int32_t _bNum; }; class C : public A { public: C(): _cNum(0) {} virtual ~C() {} protected: int32_t _cNum; };上面我们写了三个类,B和C分别继承于A。 下面注意调用方式的不同,一个箭头–>,一个是点语法。
int main(int argc, const char * argv[]) { // 在堆上 B* b = new B(); C* c = new C(); // 注意:在堆上的指针我们这样调用 // C++当中严格区分指针的调用,如果b是个指针类型 // 我调用它的函数我一定要用这个箭头。 b->SetNum(10); // 在栈上,如果我在栈上构造一个函数 B anotherB; // 普通的对象我们这样调用:如果是在栈上分配,我是这样去调用的 anotherB.SetNum(10); return 0; }注意:这种关系只能发生在多重继承。
而在这种情况下,以下的写法是错的
d->SetNum(10);这种写法是错的,因为D继承于B和C,而B与C继承于A,B、C同时拥有A的_num的一份拷贝,所以,当你访问SetNum的时候,不知道是要访问B的还是C的,程序是错误的。而我只想要一份,引起歧义了,所以,这就讲到了虚拟继承,设计C++的这帮人发现,从逻辑上讲,这个确实是有问题的。我D里应该只有一份_num,怎么会有两份呢,从逻辑上讲不通,那我们加virtual,虚拟继承。 class C : public virtual A,class B : public virtual A,这样就解决二义性的问题。 这个virtual和虚函数完全是两回事。这里只代表虚拟继承。
我们在B和C类的继承类A前面加virtual。代表虚拟继承,解决数据的二义性问题,此时d->SetNum(10);的写法就没问题了。来看一下完整代码。
#include <iostream> #include <cstdint> #include <cmath> class A { // 基类 public: A(): _num (0){} // 在初始化列表里把num初始化成0. virtual ~A() {} // 两个虚函数,一个修改num,一个返回num的值 virtual void SetNum(int32_t num) {_num = num;} virtual int32_t GetNum() const {return _num;} private: int32_t _num; // 这是数据 }; // public 就是C++当中的继承方式,跟C#和Java非常的相像 class B : public virtual A { public: B(): _bNum(0) {} virtual ~B(){} // protected:继承链上不可以去访问 protected: int32_t _bNum; }; // A 前面的virtual代表虚拟继承 class C : public virtual A { public: C(): _cNum(0) {} virtual ~C() {} protected: int32_t _cNum; }; // 多重继承 // 对于D来说,它使用了多重继承的技术,它拥有B的特性,也拥有C的特性 class D : public B, public C { public: D() {} virtual ~D() {} }; int main(int argc, const char * argv[]) { // multiple inheritance D* d = new D(); // D类既是B类也是C类,所以以下写法都是可以的 A* _a_ = dynamic_cast<A*>(d); // 加上需要继承之后此句正确,否则报error。 B* _b_ = dynamic_cast<B*>(d); C* _c_ = dynamic_cast<C*>(d); d->SetNum(10); return 0; }Best Practice 多重继承,尽可能只继承接口,基类不要用数据,不要像我们基类A里面还有数据_num,我们之所以现在用虚拟继承,就是因为基类里面有数据_num,产生了重复的数据,导致二义性。这也解释了为什么Java和C#当中 , 只支持多重实现接口。接口里面不可以有数据,Java和C#当中从语言层面去解决了这个问题,但是C++不一样,十分的自由,尽可能的让你做你想做的事情,但是这不是好的设计,所以我们只继承接口,不要在基类里面写数据。
