
继承的概念继承机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展增加功能。这样产生新的类称派生类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。继承本质是类层次的复用。继承的语法class A { public: int a; }; //以public的方式继承A //派生类 继承方式 父类 class B : public A { public: int _B;//此时B里面其实也包含了A };继承方式的不同让派生类继承的成员的访问方式有所变化知识点1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它2. 基类private成员在派生类中是不能被访问如果基类成员不想在类外直接被访问但需要在派生类中能访问就定义为protected。可以看出保护成员限定符是因继承才出现的3. 使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public不过最好显示的写出继承方式4. 在实际运用中一般使用都是public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强。赋值兼容转换派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去但是父类不能赋值给子类赋值兼容转换包括三种形式1. 派生类对象赋值给基类对象发生对象切片2. 派生类地址赋值给基类指针向上转型3. 派生类对象绑定到基类引用向上转型class A { public: A(int input):_A(input) { } int _A; }; class B :public A { public: B(int input):A(3) ,_B(input) { } private: int _B; }; int main() { B b(2); A a(2); a b; cout a._A;//输出3说明_A被b中的_A赋值 return 0; }继承中的名字隐藏机制当派生类中声明了与基类同名的成员时会触发C的名字隐藏规则名字查找优先编译器从当前类作用域开始查找一旦找到匹配的名字就停止声明即隐藏只要派生类中有同名成员的声明无论定义在哪里基类的所有同名成员都会被隐藏参数无关隐藏不考虑参数匹配只要是同名就会全部隐藏作用域限定访问被隐藏的基类成员仍然可以通过Base::显式访问关键点隐藏是编译期的名字查找行为不是运行时的函数选择。只要派生类作用域中有这个名字的声明编译器就不会继续向上查找基类的同名成员。派生类的默认成员函数构造函数派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用有的话编译器会自动调用基类的默认构造函数此时如果没有默认构造函数会报错不能在构造函数的内部对基类成员进行初始化需要使用初始化列表进行初始化原因可以查到这里简单理解为C的规定。顺带一提初始化列表初始化的顺序是按照声明的顺序来的而基类肯定在子类之前声明所以不管放在初始化列表的第几行都是第一个被初始化的拷贝构造函数派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化假如显式调用基类的构造函数也是可以的因为编译器只是确保基类成员得到初始化但是这样不符合拷贝构造本身定义class A{ public: int a }; class B:public A{ //... B(B temp) :A(temp){//至少显式调用A的构造函数 //如果没有默认的又不调用就会报错 _B temp._B; } }赋值运算符重载函数派生类的operator必须要调用基类的operator完成基类的复制调用方式值得注意如下显示调用基类的赋值运算符重载函数class A{ public: int a }; class B:public A{ //... B operator(B temp){ A::operator(temp)//发生切片操作注意不要写成temp.a之类的a不是A类型 //等效于this-A::operator(temp) 又因为A::operator()是对A内的成员操作的且A内的成员已经通过继承转移到B所以该语句生效 }析构函数由于基类的成员不可能涉及子类因为是用子类去继承基类的而子类中的操作可能涉及基类那我故意调用子类中被释放的成员不也一样吗可能还没学到把所以一般先释放子类然后编译器自动调用基类的构造函数不需要显式调用。友元关系不能被继承很好理解基类的友元函数到了子类那边就用不了除非在子类也使用friend关键字修饰静态成员的继承基类的静态成员就算被多个派生类继承了从始至终都只有一个成员class A { public: A() { a; } static int a; }; int A::a 0; class B :public A { //...; public: B():A() { } }; class C :public A { //...; public: C() :A() { } }; int main() { B b; C c; cout A::a;//输出2因为调用了两次A的构造函数且始终是一个a在进行操作 return 0; }多继承一个类可以继承多个类需要分别指定每个基类的继承方式class A { //... }; class B :public A { //... }; class C :public A,private B { //... };菱形继承的问题这是C语法设计的一个缺陷之一由多继承导致假如A被B,C继承了那B,C分别有A的成员然后D继承了B和C那D就会继承到两次A的成员此时出现了二义性问题这种继承的关系看起来就像菱形一样解决方式在D中单独开一块空间来存放B和C中重复的A的成员即可实现的机制其实挺复杂的采用的是虚继承和虚基表来解决该问题虚继承class A{ //... public: int a; }; // 虚继承关键字 通常在菱形的腰部使用 class B:virtual public A{ //... }; class C:virtual public A{ //... }; class D:public B,public C{ //... //此时就不会出现“两个 a”了 };虚基表虚基表是当一个类选择虚继承另一个类的时在内存中产生的一个“表”是用来存储偏移量的比如变量a相对于变量b偏移了多少。当D继承B,C的时候前面说过会在D中单独搞一块空间来存放A中的成员假如在D中的BC需要对A的成员进行操作它们是不知道这些成员放在哪的。D自己是知道的D中的在B,C之外可以直接对A的成员进行操作当B,C,D分别继承到A的时候都会产生一个特定的指针来指向这个虚基表从而根据虚基表的偏移量来确定A中成员的位置其实单独的B,C继承A是没必要搞虚继承的只是为了防止菱形继承的情况虚基表的个数通常只有一个虚基表中偏移量的个数是根据继承的基类的个数来确定的除了虚基表以外在B,C中直接为A的每个变量指针也可以解决这个问题早期的C设计那后来为什么不用指针显然假如A有很多个变量且B,C继承多个基类 指针的数量会很庞大同时访问的效率也会大打折扣每次都要解引用所以搞出了虚基表总结有了多继承就存在菱形继承有了菱 形继承就有菱形虚拟继承底层实现就很复杂。所以一般不建议设计出多继承一定不要设计出菱形继承。否则在复杂度及性能上都有问题。继承和组合class A{ //... }; class AA:public A{ //...//基类更改导致派生类也需要更改依赖性高 }; class AAA:virtual public A{ //... //如果是虚继承还有虚基表的额外开销 }; class A2{ //... }; class B{ A a;//想用谁的就用谁的有可能发生内联优化效率更高 A2 a2; AA aa; };继承对基类的依赖性高而组合更加灵活所以一般优先采用组合。当然继承并非没有价值后面学习的多态则依赖于继承实现。继承和组合的根本区别不是能不能用成员而是以什么身份、什么代价、什么灵活性去用继承和组合总结语义不同继承是我是组合是我用耦合度不同继承是白盒知道内部组合是黑盒只知接口灵活性不同继承关系编译时固定组合可以运行时改变演化能力不同继承链难改组合部件易换测试难度不同继承难测组合易测可mock