一.继承1.什么是继承class A { public: int ma; protected: int mb; private: int mc; }; class B :public A { public: int md; protected: int me; private: int mf; };如图假如我们定义了A,B两个类而类B同时也拥有包含了类A的mambmc三个成员变量直接一点我们可以在类B中再定义ma,mbmc三个成员变量但这样不仅会导致代码的大量重复而且这样也并不能表示B中包含的是A中的三个成员变量。为了解决这种问题面向对象的编程设计了继承操作如图class B:public A表示B类继承了A类A称为基类或父类B称为子类或派生类。2.继承的本质(好处)继承的本质其中之一是代码的复用这里B类继承了A类实际在B类中对A类进行了复用所以虽然B类中只有mdmemf但是B类中实际包含了A类的ma,mb,mc实际的内存大小是24字节但同时如果我们再在B类中定义一个ma此时编译器并不会报错不会发生命名冲突因为这里的两个变量的作用域实际上不同。既然这里的B类包含了A类的成员变量那么这里就涉及一个问题在类B中类A的三个成员变量的访问权限究竟是怎么样的注意这里派生类的访问限定指的是基类成员在派生类中的访问限定不是派生类的成员在外部的访问限定。如图就是各种情况下继承过程中的访问权限从中我们可以总结出几点1.基类成员的访问限定在派生类中是不可能超过继承方式的即派生类的访问限定不能大于继承方式。2.外部只能访问对象的public成员。3.在继承中派生类从基类继承的private成员派生类无法访问即对于private成员只有定义其的类本身或友元才能访问派生类无法访问4.protected和private的区别?在基类中定义的成员,想被派生类访问,但是不想被外部访问,那么在基类中,把相关成员定protected保护的;如果派生类和外部都不打算访问,那么在基类中,就把相关成员定义成private私有的。默认继承方式对于继承中的限定符我们也可以像类的访问限定符一样不写即class B : A同类的访问限定符如果类使用class定义默认为private使用struct定义默认为public继承的本质好处其二在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了3.派生类的构造过程class Father { public: Father(int data) :ma(data) { } ~Father() { } protected: int ma; }; class Son :public Father { public: Son(int data) :mb(data) //,ma(data)(错误) ,Father(data) { } ~Son() { } private: int mb; };如图我们在基类中定义了一个protected的成员变量派生类通过public继承此时派生类可以访问成员变量ma但是如果我们在派生类的构造函数中直接访问ma进行初始化就会报错这点表明尽管派生类可以访问ma但是不能用自己的构造函数初始化它。实际上派生类继承基类除了构造与析构函数外的所以成员包括成员变量与成员方法而基类负责初始化基类的成员变量派生类负责初始化派生类的成员变量并且永远是基类先调用其构造函数派生类先调用其析构函数先构造的后析构。总结一下派生类的构造过程就是1.派生类调用基类的构造函数,初始化从基类继承来的成员2.调用派生类自己的构造函数,初始化派生类自己特有的成员派生类对象的作用域到期了3.调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存,文件)4.调用基类的析构函数,释放派生类内存中,从基类继承来的成员可能占用的外部资源(堆内存,文件)4.继承中的重载与隐藏class Father { public: void show() { cout Father::show endl; } void show(int) { cout Father::show(int) endl; } }; class Son :public Father { public: void show() { cout Son::show endl; } }; int main() { Son s; s.show(); s.show(10);//错误 }如图我们定义了基类与派生类派生类继承了基类的show函数且在外部main可以访问我们再在外部调用有参的show与无参的show结果有参的show报错无参的show调用的是派生类的表示我们这里无法调用基类中继承来的show而如果我们这里没有派生类中的show则可以正常调用。由此我们可以得出结论1.如果基类和派生类中都有某个同名函数会调用派生类中的不会调用基类中的如果派生类中没有才会在基类中找即派生类中的同名函数会将基类中的隐藏作用域隐藏如果这里想要调用基类中被隐藏的函数需要加上作用域即s.Father::show(10)。2.派生类中的同名函数与基类中的并不是重载关系重载的前提是位于同一个作用域这里的基类与派生类并不在同一个作用域。5.基类对象与派生类对象/指针我们通常也称继承是从上基类到下派生类的结构。(1)基类对象与派生类对象的转化class Father { public: Father(int data) :ma(data) { } ~Father() { } protected: int ma; }; class Son :public Father { public: Son(int data) :mb(data) ,Father(data) { } ~Son() { } private: int mb; }; int main() { Father f(10); Son s(10); f s; s f;//错误基类无法转化为派生类 Father* pfs; Son* psf;//错误基类类型的指针无法转换为派生类 }如图我们可以将一个派生类对象赋值给一个基类实现类型从下派生类向上基类的转换但我们不能将一个基类对象赋值给一个派生类即不能实现从上到下的转换。(2)基类对象指针与派生类对象指针如图基类指针可以指向派生类的对象不过由于指针的限制只能访问派生类继承的基类的部分由下向上的转换但派生类对象的指针不能指向基类由上向下不支持总一下就是在继承结构中进行上下的类型转换,默认只支持从下到上的类型的转换当然我们可以通过强转调用但是不安全会非法访问6.多重继承我们知道使用继承的一个优点是提高代码的复用性多重继承的一个重要优点也在此。学习多重继承首先我们需要知道虚基类与虚继承1虚继承与虚基类class A { private: int ma; }; class B : virtual public A { private: int mb; };如图在继承方式前或者后面加上virtual此时就称作虚继承发生了虚继承的类称为虚基类那么虚继承与普通继承有什么区别呢如图我们打印A类与B类大小时会发现B的大小变成了12字节而如果是普通继承的话这里应该是8字节所以毫无疑问这里除了mamb外还存储了其他的东西。实际上这里除了mamb还存储了vbptr虚基类指针指向vbtable虚基类表同虚函数表一样这里的vbtable也是在编译阶段创建运行阶段加载到.rodata段如图我们知道对于普通继承来说这里的ma应该是存放在mb前面的但是对于虚继承来说虚基类的数据都存放在末尾在开头的存放的是vbptr指向vbtable虚基类表存储的是两个偏移量第一个是向上的偏移量与虚函数表相似指的是vbptr的地址相对于起始地址的差值一般是0第二个是向下的偏移量指的是原本应该存放在起始位置的ma相较于起始位置的差值便于查找。class A { private: int ma; public: void func(){coutA::funcendl;} }; class B : virtual public A { private: int mb; public: void func(){coutB::funcendl;} }; int main() { A* pnew B(); p-func(); delete p; return 0; }接下来我们来看虚函数与虚继承的结合其实单从调用上来说确实能够成功调用但是问题在于这里的delete我们知道基类指针指向派生类对象这里的基类指针指向的是派生类对象中的基类部分的起始地址对于普通继承来说继承来的ma本就是在mb的上面但是我们刚才说过虚继承来的基类的数据都存放在mb下面但这里的p仍然指向的是ma所以导致了这里的delete只释放了ma没有释放原来B类的内存。2菱形继承多重继承能够提高代码的复用性但同样存在一些问题如图是菱形继承我们发现菱形继承后对于对象D继承了两次ma属性这里就会发生重复所以许多C开源代码都尽量避开使用多重继承我们应当尽量不使用多重继承。而为了解决这种问题就需要用到我们前面提到的虚继承了这里我们将B和C对A的基础都改成虚继承此时A就成了虚基类这里再继承时ma属性就只会添加一份但是这样之后需要我们D类对A类中的ma进行初始化。二.多态1.静态绑定class Father { public: Father(int data10) :ma(data) { } void show() { cout Father::show endl; } void show(int) { cout Father::show(int) endl; } private: int ma; }; class Son :public Father { public: Son(int data10) :mb(data) { } void show() { cout Son::show endl; } private: int mb; }; int main() { Son s; Father* ps s; ps-show(); ps-show(10); cout sizeof(Father) endl; cout sizeof(Son) endl; cout typeid(ps).name() endl; cout typeid(*ps).name() endl; }如图我们定义了一个基类的指针指向派生类这里打印类型大小的结果基类是4字节派生类由于继承了基类的ma总共是8字节。这里打印指针ps的类型为class Father* 类型一旦确定无法再更改一般类类型类型名前面都会加上class打印*ps类型的结果是class Father。由于ps的类型是class Father*所以通过指针访问的show成员函数都是基类的show函数如图我们可以看到这里的show函数的调用在编译阶段就已经确定了我们称这种函数的调用是静态绑定。2.虚函数class Father { public: Father(int data10) :ma(data) { } virtual void show() { cout Father::show endl; } virtual void show(int) { cout Father::show(int) endl; } private: int ma; };如图我们将基类中的两个成员函数show都加上了virtual关键字将它们写成了虚函数1.如果一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区只读数据段如图为Father的虚函数表其中RTTI是run-time type information(运行时类型信息)指向的是类型字符串的地址这里就是Father字符串这里的偏移量指的是vfptr虚函数指针相较于起始位置的偏移量由于vfptr的优先级很高所以这里一般都是0。2.一个类里面定义了函数数,那么这个类定义的对象其运行时,内存中开始部分,多存储一个vfptr虚函数指针即原来4字节的基类现在为8字节,指向相应类型的虚函数表vftable。一个类型定义的n个对象,它们的vfptr指向的都是同一张虚函数表3.一个类里面虚函数的个数,不影响对象内存大小(vfptr),影响的是虚函数表的大小4.如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数即这里Son类的show方法同时这里由于派生类存在与基类相同的show方法在虚函数表中会发生覆盖派生类的show方法的地址会覆盖掉基类show方法的地址那些函数不能实现为虚函数实现虚函数依赖于虚函数必须能够产生地址存储在vftable中虚函数必须依赖于对象。所以根据这两点构造函数不能成为虚函数构造函数运行完对象才创建构造函数中调用的任何函数也都不可能发生动态绑定即就算构造函数中有virtual函数也不能实现动态绑定。static 静态成员方法不能定义为虚函数静态成员方法不依赖于对象。3.动态绑定当我们通过上述步骤将基类中的两个函数实现为虚函数后如图此时编译器在发现show函数为虚函数后就不会进行静态绑定而是动态绑定这里我们看到call指令后面不在是明确的函数而是存储函数地址的一个寄存器此时只有在运行时才能确定究竟调用的是哪一个函数这种函数调用方式我们称为动态绑定。我们再看这里的sizeof(Father)sizeof(Son)由于多出来了一个虚函数指针vfptr所以比原先多出来了4字节 typeid(ps).name()类型不变函数class Father*但是对于这里的*ps的类型要先看ps的类型为什么这里为class Father*再看这个类中是否存在虚函数如果不存在*ps就是编译时的类型class Father否则是运行时的类型class Son同时这里调用的第一个show不再是基类的show而是派生类的show那么有上述内容就产生了一个问题是不是所有的虚函数的调用就一定是动态绑定答案当然是否定的我们上面就提到过一个反例构造函数中就算有虚函数也不会进行动态绑定。如图Father f; Son s; f.show(); s.show(); Father* pf1f; pf1-show(); Father* pf2s pf2-show();假如我们使用对象本身来调用虚函数我们来看看其汇编代码我们会发现前两个调用都是直接call 它们调用的虚函数说明这是静态绑定。而后两个使用指针来调用的这里我们可以清楚的看到无论是使用基类的指针指向派生类的对象函数使用基类的指针指向基类的对象都是动态绑定。总结一下就是如果不是通过指针或者引用变量来调用虚函数,那就是静态绑定4.虚析构函数class Father { public: Father(int data10) :ma(data) { cout Father()endl; } ~Father() { cout ~Father() endl; } void show() { cout Father::show endl; } void show(int) { cout Father::show(int) endl; } private: int ma; }; class Son :public Father { public: Son(int data10,int* p new int(10)) :mb(data) ,mptr(p) { cout Son() endl; } ~Son() { cout ~Son() endl; delete mptr; mptr nullptr; } void show() { cout Son::show endl; } private: int mb; int* mptr; }; int main() { Father* ps new Son; ps-show(); delete ps; }我们来看这段代码我们在堆区开辟了Son的空间ps指向这块空间Son中存在int* mptr指向堆内存当ps释放时应该调用Son的析构函数释放mptr但是运行结果如图我们看到这里并未调用Son的析构函数导致了内存泄漏。原因在于这里的ps是静态绑定ps的类型是Father**ps的类型是Father导致了数据运行时程序根本就没有调用到Son函数中的析构函数所以我们需要将动态绑定改成静态绑定。为了解决这种问题我们可以将基类的析构函数实现为虚函数virtual ~Father() { cout ~Father() endl; }当我们将基类的析构函数定义为虚析构函数时编译器会将派生类的析构函数自动识别为虚析构函数此时进行的就是动态绑定*ps的类型就是Son,就能够顺利的调用~Son函数了。什么时候把基类的析构函数必须实现成虚函数?基类的指针(引用)指向堆上new出来的派生类对象的时候,delete ps(基类的指针)它调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用5.多态1静态编译时期多态函数重载函数重载就是一种典型的静态多态同一个函数名多种重载形式并且在编译时期编译器就已经确定需要调用的函数版本。模板包括函数模板和类模板模板同样也是一种典型的静态多态举个例子tmepletetypename T compare(T a,T b) { return ab; } compare(10,10); compare(10.5,20.5);编译器会根据我们传入的值来推导T的类型然后使用T的类型这里是int 和double来分别实例化一个int_int和double_double的 模板函数。2动态运行时期多态在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数基类指针指向哪个派生类对象,就会调用哪个派生类对象的覆盖方法,称为多态多态底层是通过动态绑定来实现的class Animals { public: Animals(string name) :name(name) { } virtual void bark(){} private: string name; }; class Cat :public Animals { public: Cat(string name) :Animals(name) { } void bark() { cout miao miao! endl; } }; class Dog :public Animals { public: Dog(string name) :Animals(name) { } void bark() { cout Wang Wang! endl; } }; class Pig :public Animals { public: Pig(string name) :Animals(name) { } void bark() { cout Heng Heng! endl; } }; void bark(Cat cat) { cat.bark(); } void bark(Dog dog) { dog.bark(); } void bark(Pig pig) { pig.bark(); } int main() { Cat cat(C); Dog dog(d); Pig pig(p); bark(cat); bark(dog); bark(pig); }如图我们定义了一个Ainmals类和CatDogPig三个子类我们现在向使用全局的方法bark来调用类内的bark成员方法比较直接的方法就是直接定义三个全局方法接收三个不同的参数但这样显然不好后续如果需要添加其他Animal时需要对全局方法进行修改所有我们考虑使用一个bark将所有的Animal全部包含这里将bark中传递的参数类型改成基类就行了在调用时会发生动态绑定调用传入派生类的bark成员方法。6.抽象类我们借助上面的Animals类来具体分析首先我们知道普通类的目的是抽象一个具体事物成为一个对象这里的CatDogPig都是具体的但是这里的Animals类并不代表某一个具体的动物没有具体的name和bark实现这里定义Animal的初衷,并不是让Animal抽象某个实体的类型1.string name;让所有的动物实体类通过继承Animal直接复用该属性2.给所有的派生类保留统一的覆盖/重写接口因为这里的bark并不实现具体功能所以我们可以这样写virtual void bark() 0;这样的bark函数我们称为纯虚函数拥有纯虚函数的类我们称为抽象类抽象类不能再实例化对象但是可以定义指针和引用变量。总结一下就是我们通常将基类设计为抽象类。三.经典问题问题1.问下面程序的输出结果是什么class Animals { public: Animals(string name) :name(name) { } virtual void bark() 0; private: string name; }; class Cat :public Animals { public: Cat(string name) :Animals(name) { } void bark() { cout miao miao! endl; } }; class Dog :public Animals { public: Dog(string name) :Animals(name) { } void bark() { cout Wang Wang! endl; } }; void bark(Animals animal) { animal.bark(); } int main() { Animals* p1 new Cat(加菲猫); Animals* p2 new Dog(二哈); int* p11 (int*)p1; int* p22 (int*)p2; int tmp p11[0]; p11[0] p22[0]; p22[0] tmp; p1-bark(); p2-bark(); delete p1; delete p2; }这段代码的重点就在中间的这里int* p11 (int*)p1;int* p22 (int*)p2;int tmp p11[0];p11[0] p22[0];p22[0] tmp;这里我们将p1p2强转为int*类型所以p11[0]的作用就是访问Cat的前4字节即Cat的vfptr同理p22这里fw的就是Dog的vfptr即将Cat的vfptr与Dog相交换那么打印的结果也就是两者的bark相交换了。问题2.问下面程序的输出结果class Base { public: virtual void show(int i 10) { cout call Base :: show i: i endl; } }; class Derive : public Base { public: void show(int i 20) { cout call Derive :: show i: i endl; } }; int main() { Base* p new Derive(); p-show(); delete p; return 0; }这里我们使用基类的指针指向派生类的对象发生动态绑定毫无疑问这里调用的是派生类的show方法那么这里就有一个坑了既然调用的是派生类的show方法那么自然也是使用的派生类show方法的默认参数对吗其实这种想法是错误的我们知道对于一个函数的调用编译器需要在编译阶段将形参压栈由于这里发生的是动态绑定在运行阶段*p才会绑定到派生类对象上而在编译阶段*p还是基类对象所以在编译阶段压栈压压入的参数是基类中的10。如图我们也能看到这里压栈压入的是10问题3问这里的输出结果是什么class Base { public: virtual void show() { cout call Base :: show endl; } }; class Derive : public Base { private: void show() { cout call Derive :: show endl; } }; int main() { Base* p new Derive(); p-show(); delete p; return 0; }这段代码的问题就在于这里的派生类的show方法定义为私有的了但是这里通过动态绑定p调用的又是派生类的show方法由此产生了冲突。实际上这里与问题2十分相似这里是可以正常的调用的原因在于C 对函数访问权限public、private、protected的检查是在编译阶段进行的也即是说在动态绑定之前*p还是基类对象此时进行了访问权限检查发现基类的show方法是public的没有问题然后在运行时进行动态绑定p成功调用了派生类的show方法(如果我们这里将基类改成private而将派生类改成public这里就无法编译成功了)。问题4这里1和2分别的运行是否有误class Base { public: Base() { cout call Base() endl; clear(); } void clear() { memset(this, 0, sizeof(*this)); } virtual void show() { cout call Base::show() endl; } }; class Derive :public Base { public: Derive() { cout call Derive() endl; } void show() { cout call Derive: :show() endl; } }; int main() { Base* pb1 new Base();//1 pb1-show(); delete pb1; Base* pb2 new Derive();//2 pb2-show(); delete pb2; return 0; }毫无疑问这里的问题在于clear这个函数基类的构造函数中调用了它而它做了将Base基类强制清0的操作。这里的代码1将基类对象清0了vfptr就指向0地址了访问不到它的虚函数了就会出现问题。这里的代码2由于构造派生类需要先调用基类的构造函数所以这里的clear将这里的派生类清0了导致这里的pb2无访问对吗其实这个解释并不正确原因在于忽略了编译器的隐藏操作实际上这里清0的是派生类的内存没错但是在调用基类的构造函数时编译器会悄悄地将vfptr修改指向基类的虚函数被清0后调用派生类的构造函数编译器同样悄悄的将vfptr指向了派生类的虚函数地址由此pb2的调用并不会出现问题。