class Base { public: int a; private: int b; }; class Derived : public Base { }; int main() { cout << sizeof(Base) << endl; //?? cout << sizeof(Derived) << endl;//?? return 0; }
class Base { public: Base(int a, int b) : _a(a) , _b(b) {} int _a; private: int _b; }; class Derived : public Base { public: Derived(int a = 0, int b = 0, int c = 0) : Base(a, b) , _c(c) {} private: int _c; }; int main() { Derived d(1, 2, 3); return 0; }
可以看见的是私有成员虽然在派生类中不可见, 但是本质上也是存在的, 仅仅只是语法上限制了访问...
切割 :
切片 : 切片, 指的是权限限制只能访问切片的基类部分的数据方法, 和切割还是不一样的, 不会重新拷贝一份出来, 而是直接的指向对应的基类, 然后限制只能访问数据基类的成员即可了, 这个就是切片引用的本质
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一 个static成员实例 。
static 静态成员是专属于一个类的, 无论派生出去多少个子类, 均只是一个static 成员实例对象, 不会一个派生类中产生一份, 而是全局独独一份:
class Base { public: Base(int a, int b) : _a(a) , _b(b) {} int _a; private: int _b; static int count; }; int Base::count = 0; class Derived1 : public Base { public: Derived1(int a = 0, int b = 0, int c = 0) : Base(a, b) , _c(c) {} private: int _c; }; class Derived2 : public Base { public: Derived2(int a = 0, int b = 0, int c = 0) : Base(a, b) , _c(c) {} private: int _c; }; int main() { Derived1 d1(1, 2, 3); Derived2 d2(1, 2, 3); return 0; }
Base 类的进程数据成员都不会存在在派生类对象中, 是属于整个基类的,是在进入主函数之前就定义好了的, 全局独一份的, 存储在静态数据段中的
class A { public: A(int a = 0) : _a(a) { } int _a; }; class B : public A { public: B(int a = 0, int b = 0) : A(a) , _b(b) { } int _b; }; class C : public A { public: C(int a = 0, int c = 0) : A(a) , _c(c) { } int _c; }; class D : public B, public C { public: D(int a = 0, int b = 0, int c = 0, int d = 0) : B(a, b) , C(a, c) , _d(d) { } int _d; };
直接访问上述的_a 成员出现了问题, 因为不知道应该访问哪一个_a, 先看一看内存窗口:
提出问题一: 存在两份_a数据成员, 造成两个问题, 1. 数据冗余,(两份_a) 2. 二义性问题(访问的之后究竟访问的是那一份_a)
解决方式1:仅仅可以解决的就是说二义性的问题, 可以通过指定访问的方式, 通过:: 访问
上述方式虽然是解决了数据访问的二义性问题, 但是还是没有解决的是数据冗余的问题, 数据冗余问题其实还是蛮大的, 现在A类的大小不大问题还显示不出来, 要是A类特别庞大, 就有一份重复的A类的各种数据成员被继承下来, 这个数据冗余带来的存储代价还是蛮大的
class A { public: A(int a = 0) : _a(a) { } int _a; }; class B : virtual public A { public: B(int a = 0, int b = 0) : A(a) , _b(b) { } int _b; }; class C : virtual public A { public: C(int a = 0, int c = 0) : A(a) , _c(c) { } int _c; }; class D : public B, public C{ public: D(int a = 0, int b = 0, int c = 0, int d = 0) : B(a, b) , C(a, c) , _d(d) { } int _d; };
首先咱先观察这个普通的监视窗口, 给我们的感觉好像就是存在两份的_a在其中? 是吗? 答案肯定不是呀, 那要这个虚继承有个屁用, 还不是没有解决数据冗余 (两份祖先数据成员的问题吗)
答案是因为监视器是为了让我们看着简单, 不想那么多, 而经过了处理的, 其中存在细节处理, 我们可以真正深入内存窗口去查看., 将列设置为4列, 单位就是一个int 了4字节的看,
存储的是地址差距的地址, 通过这个地址找到距离属于A的数据成员的位置进行访问
总结感慨: 多继承真他妈坑爹, 它搞出来的菱形继承的坑, 只能搞出来一个虚继承来填补: 数据冗余 + 数据访问的二义性, 但是来了菱形继承, 搞得这个继承下来的孙子派生类的内存模型复杂了简直不要太多, 内存分布模型再也不想切割和切片那样的明白简单了, 而是复杂了简直不要太多, 需要存储和A类数据成员的地址偏差, 为了保护这个偏差, 还没有直接存储, 而是通过存储这个差值的地址值来实现的, 解决了问题, but 令人的学习成本简直翻倍不要太多.
所以其实真正用的时候, 基本不是必须都是使用单继承, 而不是多继承
class Father { public: virtual void sleep() { cout << "我是大人睡觉不打铺盖" << endl; } }; class Son : public Father { public: virtual void sleep() { cout << "我是小孩睡觉打铺盖" << endl; } }; void Sleep(Father& who) { who.sleep(); } int main() { Father f; Son s; Sleep(s);//传入孩子是啥结果? Sleep(f);//传入Fa是啥结果? return 0; }
class Base { };//写一个空基类做返回 class Derived : public Base { };//写一个空派生类做返回 class Fa { public: virtual Base* GetBase() { cout << "传入基类对象, 掉基类的虚函数" << endl; cout << "基类虚函数返回基类指针引用. 派生类虚函数返回派生类指针引用, 称为协变" << endl; return new Base; } }; class Son : public Fa { public: virtual Derived* GetDerive() { cout << "传入派生类对象, 掉派生类重写的虚函数" << endl; cout << "基类虚函数返回基类指针引用. 派生类虚函数返回派生类指针引用, 称为协变" << endl; return new Derived; } }; void test(Fa& who) { delete who.GetBase(); } int main() { Fa f; Son s; test(f); test(s); return 0; }
特殊案例二: 其实也不算特殊, 因为底层编译器会进行处理:
尽量将析构函数定义为虚函数, 为啥?? 构造函数都不能定义为虚函数, 为啥析构函数可以? 函数名都不满足相同, 如何构成虚函数重写的要求??? (尽量让析构函数定义为虚析构函数构成多态??)
class Fa { public: Fa() :_a(new int(0)) { cout << "constructor Fa" << endl; } ~Fa() { delete _a; cout << "destructor Fa" << endl; } private: int* _a; }; class Son :public Fa { public: Son() { cout << "constructor Son" << endl; } ~Son() { cout << "destructor Son" << endl; } }; int main() { Fa* f1 = new Fa; Fa* f2 = new Son; delete f1; delete f2; return 0; }
class Base { public: virtual void Work() = 0; virtual void Sleep() = 0; }; class Derived : public Base { public: virtual void Work() { cout << "完成上述的接口函数" << endl; cout << "我正在工作" << endl; } virtual void Sleep() { cout << "完成上述的接口函数" << endl; cout << "我正在睡觉" << endl; } };
上述问题答案是4? 为啥. 就算是存在占位也应该是1字节, 去掉virtual 瞅瞅?
typedef void (*PFunc)(); //PFunc 是一个类型, 函数指针类型 class Base { public: virtual void func1() { cout << "Base virtual void func1()" << endl; } virtual void func2() { cout << "Base virtual void func2()" << endl; } virtual void func3() { cout << "Base virtual void func3()" << endl; } }; class Derived : public Base { public: virtual void func1() { cout << "Derived virtual void func1()" << endl; } virtual void func2() { cout << "Derived virtual void func2()" << endl; } virtual void func3() { cout << "Derived virtual void func3()" << endl; } }; //打印虚表函数 void PrintVTable(PFunc** vptr) { cout << "虚表函数调用如下: " << endl; PFunc* VFunArr = (PFunc*)vptr;//强制转换为 for (int i = 0; VFunArr[i] != NULL; ++i) { VFunArr[i](); //调用函数 } cout << endl; } int main() { Derived d; PrintVTable((PFunc**)*(int*)&d); return 0; }
再画一张图给大家加深一下印象:
- 本文从继承分析到多态: 很多小的细节没有介绍到, 主要是解释了一下其中我认为的重难点
- 继承: 主要是多继承引起的菱形继承, 处理是虚继承, 全局一份最初基类数据, 所以这个数据放置的位置放置在B 和 C中都不合适, 然后不放置在B和C中如何可以访问这个数据呢? 为了访问这个数据, 采取的措施是记录到这个全局一份数据的偏移量, 为了保护这个偏移量, 在对象内存模型中存储的是这个偏移量的地址
- final 关键字修饰类,该类不能继承
- 多态解释: 不同的对象调用同一个函数, 执行同样的行为会产生不同的结果
- 多态必要条件,父类指针指向子类对象或者引用子类对象, 在派生类中重写基类虚函数, 使用父类的指针或者引用调用这个虚函数
- 多态的特例: 协变 和 虚析构(底层处理 统一成destructor析构函数名)
- 多态的底层原理 为啥虚函数重写的本质是覆盖, 愿意是虚函数的地址存储在虚表中, 重写虚函数其实是对于虚表中对应的虚函数地址中的虚函数进行的一个覆盖操作..
- 如何找到虚表, 对象模型中会存储一个虚表指针, 帮我们找到这个虚函数