多继承、虚基类、虚函数、多态
//如果没有指定继承方式,对与结构体来说就是公有继承,对于类就是私有继承
//公有成员函数称为接口,公有继承,基类的接口成为派生类的接口
//继承与重定义。重定义隐藏了基类的成员。
//可以重定义基类数据和基类成员函数。
//重写基类成员函数有两种情况:1、与基类完全相同2、与基类成员函数名相同,参数不同
重载:要在同一个类中,发生再作用域相同的范围
覆盖(override):要求虚函数才可以
继承与组合:
无论是继承还是组合,本质是都是把子对象放在新类型中,两者都使用构造函数的初始化列表去构造这些子对象。
组合通常是在希望新类内部具有已存在的类的功能时使用,而并不是希望已存在类作为它的接口。组合通过嵌入一个对象以实现新类的功能,而新类用户看到的是新定义的接口,而不是来自老类的接口.(has-a)
如果希望新类与已存在的类有相同的接口(在这基础上可以增加自己的成员)。这时候需要继承.(is-a)
不能自动继承的成员函数:
构造函数、析构函数、=运算符
基类的构造函数不被继承,派生类中需要声明自己的构造函数。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化调用基类构造函数完成。
派生类的构造函数需要给基类的构造函数传递参数。
只能在构造函数初始化列表中初始化的情况:
const成员 const int a=10;
引用成员 int n2=100; int& rn=n2;
基类没有默认构造函数的时候,基类的构造函数要在派生类构造函数的初始化列表中调用。
派生类对象的构造次序:
先调用基类的对象成员的构造函数、然后调用基类构造函数、再然后是派生类对象成员的构造函数、最后是派生类自身的构造函数。析构次序,正好与之相反。
友元关系不能被继承:A是B的友元类 C是A的派生类 那么C并不是B的友元类。
友元关系是单向的,A是B的友元类,B并非A的友元类
友元关系是不能被传递的。
静态成员与继承:
静态成员无所谓继承,被所有对象共享,只有一份。
转换与继承:
派生类到基类的转换
1、 派生类以public方式继承基类时,编译器可以自动执行的转化
派生类对象指针自动转化为基类对象指针
派生类对象引用自动转化为基类对象引用
派生类对象自动转化为基类对象(特有成员消失)
2、 当派生类以private/protected方式继承基类时
派生类对象指针(引用)转化为基类对象指针(引用)需用强制类型转化,但不能用static_cast,要用reinterpret_cast.
static_cast用于编译期认可的静态转换,比如说从char到int,从double到int。或者具有转换构造函数。或者重载了类型转换运算符。
reinterpret_cast用于编译器不认可的静态转换。比如从int* 转换为int.
const_cast去除常量性
不能把派生类对象强制转化为基类对象。
转换构造函数(带一个参数构造函数):将其他类型转换为类类型
类型转换运算符重载:将当前类类型转换为其他类型
3、 多重继承
多重继承,一个派生类可以有多个基类
派生类同时继承多个基类的成员,更好的软件重用。
可能会有大量的二义性,多个基类可能包含同名变量或函数
多继承中解决歧义的方法:基类名::数据成员名
4、 虚继承与虚基类
当派生类从多个基类派生,而这些基类又从一个基类派生,则在访问此共同基类的成员时将产生二义性——采用虚基类来解决。
虚基类的引入:用于有共同基类的场合
声明:以virtual修饰说明基类 class B1: virtual public BB
作用:主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题。为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝。
5、 虚基类及其派生类构造函数。
虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数。每个继承类都必须对虚基类的数据成员进行初始化构造
1 #include<iostream> 2 using namespace std; 3 class Furniture 4 { 5 public: 6 Furniture(int weight):weight_(weight){ cout << "Furniture..." << endl; } 7 ~Furniture() { cout << "~Furniture..." << endl; } 8 int weight_; 9 }; 10 class Bed:virtual public Furniture 11 { 12 public: 13 Bed(int weight) :Furniture(weight) { cout << "Bed..." << endl; } 14 ~Bed() { cout <<"~Bed..." << endl; } 15 void Sleep() { cout << "sleep..." << endl; } 16 17 }; 18 class Sofa:virtual public Furniture 19 { 20 public: 21 Sofa(int weight) :Furniture(weight) { cout << "Sofa..." << endl; } 22 ~Sofa() { cout << "~Sofa..." << endl; } 23 void Watch() { cout << "watch..." << endl; } 24 25 }; 26 class SofaBed :public Bed, public Sofa 27 { 28 public: 29 //需要提供对虚基类成员的构造 30 //每个继承类都需要在构造函数中给出对虚基类成员的构造 31 SofaBed(int weight):Bed(weight),Sofa(weight), Furniture(weight) 32 { 33 cout << "SofaBed..." << endl; 34 FoldIn(); 35 } 36 ~SofaBed() { cout << "~SofaBed..." << endl; } 37 void FoldOut() { cout << "FoldOut..."<< endl; } 38 void FoldIn() { cout << "FoldIn..." << endl; } 39 }; 40 int main(void) 41 { 42 SofaBed sofabed(5); 43 sofabed.weight_ = 10; 44 //SofaBed sb; 45 sofabed.Watch(); 46 sofabed.FoldOut(); 47 return 0; 48 } 49 /* 50 Furniture... 51 Bed... 52 Sofa... 53 SofaBed... 54 FoldIn... 55 watch... 56 FoldOut... 57 ~SofaBed... 58 ~Sofa... 59 ~Bed... 60 ~Furniture... 61 */
在建立对象时,只有最底层派生类的构造函数调用了虚基类构造函数,其他类调用虚基类构造函数被忽略。
类/对象大小的计算:
类大小计算遵循前面学过的结构体对其原则。
类大小与数据成员有关与成员函数无关
类大小与静态数据成员无关
虚继承对类大小的影响
虚函数对类大小的影响
#include<iostream> using namespace std; class BB { public: int bb_; }; class B1:virtual public BB { public: int b1_; }; class B2 :virtual public BB { public: int b2_; }; class DD :public B1, public B2 { public: int dd_; }; int main(void) { cout << sizeof(BB) << endl;//4 cout << sizeof(B1) << endl;//12 cout << sizeof(DD) << endl;//24 B1 b1; cout << &b1 << endl;//00CFFADC 存放的是虚基类表指针 cout << &b1.bb_ << endl;//00CFFAE4 bb_ cout << &b1.b1_ << endl;//00CFFAE0 b1_ long **p; p = (long **)&b1; cout << p[0][0] << endl;//表内容 0 本类地址与虚基类表地址的差 cout << p[0][1] <<endl;//表内容 8 虚基类地址与虚基类表地址的差 DD dd; cout << &dd << endl; cout << &dd.b1_ << endl; cout << &dd.b2_ << endl; cout << &dd.bb_ << endl; cout << &dd.dd_ << endl; p =(long**) ⅆ cout << p[0][0] << endl;//0 cout << p[0][1] << endl;//20 cout << p[2][0] << endl;//0 cout << p[2][1] << endl;//12 return 0; } BB* pp; pp = ⅆ//pp指向的地址实际上并不是dd首地址,实际上pp会根据偏移指向bb_ pp->bb_;//间接访问。需要运行时支持
虚函数与多态:
多态性是面向对象程序设计的重要特征之一。
多态性是指发出同样的消息被不同的类型的对象接受时有可能导致完全不同的行为。调用同名的函数导致不同的行为。
多态的实现:
函数重载
运算符重载
模板
虚函数(动态绑定,动态多态)
静态绑定与动态绑定:
静态绑定,绑定过程出现在编译阶段,在编译期就已经确定要调用的函数了。
动态绑定,绑定过程工作在程序运行时执行,在程序运行时才确定将要调用的函数。
虚函数的概念:在基类中冠以关键字virtual的成员函数。
虚函数的定义:virtual 函数类型 函数名称(参数列表)
如果一个函数在基类中被声明为虚函数,则他所在派生类中都是虚函数。只有通过基类指针或者引用调用虚函数才能引发动态绑定。虚函数不能声明为静态。
1 #include<iostream> 2 using namespace std; 3 class Base 4 { 5 public: 6 virtual void Fun1() 7 { 8 cout << "Base::Fun1..." << endl; 9 } 10 virtual void Fun2() 11 { 12 cout << "Base::Fun2..." << endl; 13 } 14 void Fun3() 15 { 16 cout << "Base::Fun3..." << endl; 17 } 18 }; 19 class Derived:public Base 20 { 21 public: 22 virtual void Fun1() 23 { 24 cout << "Derived::Fun1..." << endl; 25 } 26 virtual void Fun2() 27 { 28 cout << "Derived::Fun2..." << endl; 29 } 30 void Fun3() 31 { 32 cout << "Derived::Fun3..." << endl; 33 } 34 }; 35 int main(void) 36 { 37 Base* p; 38 Derived d; 39 p = &d; 40 /* 41 Derived::Fun1... 42 Derived::Fun2... 43 Base::Fun3... 44 */ 45 p->Fun1(); 46 p->Fun2();//虚函数,基类指针指向派生类对象,调用派生类成员函数 47 p->Fun3();//不是虚函数,根据指针类型来确定 48 return 0; 49 }
虚析构函数:
析构函数可以是虚函数。如果没有虚析构函数,派生类指针调用基类析构函数,不会调用派生类析构函数。派生类对象的析构应该调用派生类析构,所以可以将析构函数声明为虚函数。
1 #include<iostream> 2 using namespace std; 3 class Base 4 { 5 public: 6 virtual void Fun1() 7 { 8 cout << "Base::Fun1..." << endl; 9 } 10 virtual void Fun2() 11 { 12 cout << "Base::Fun2..." << endl; 13 } 14 void Fun3() 15 { 16 cout << "Base::Fun3..." << endl; 17 } 18 Base() 19 { 20 cout << "Base..." << endl; 21 }
//如果一个类要作为多态基类,要将析构函数定义成虚函数。否则可能存在内存泄漏 22 virtual ~Base() 23 { 24 cout << "~Base..." << endl; 25 } 26 }; 27 class Derived:public Base 28 { 29 public: 30 virtual void Fun1() 31 { 32 cout << "Derived::Fun1..." << endl; 33 } 34 virtual void Fun2() 35 { 36 cout << "Derived::Fun2..." << endl; 37 } 38 void Fun3() 39 { 40 cout << "Derived::Fun3..." << endl; 41 } 42 Derived() 43 { 44 cout << "Derived..." << endl; 45 } 46 virtual ~Derived() 47 { 48 cout << "~Derived..." << endl; 49 } 50 }; 51 int main(void) 52 { 53 Base* p; 54 Derived d; 55 p = &d; 56 /* 57 Derived::Fun1... 58 Derived::Fun2... 59 Base::Fun3... 60 */ 61 p->Fun1(); 62 p->Fun2();//虚函数,基类指针指向派生类对象,调用派生类成员函数。 63 p->Fun3();//不是虚函数,根据指针类型来确定 64 65 Base* q; 66 q = new Derived; 67 q->Fun1(); 68 delete q;//如果析构函数不是虚的,则delete析构函数就会调用指针所对应类的析构函数,不会调用派生类析构函数 69 return 0; 70 } 71 /* 72 Base... 73 Derived... 74 Derived::Fun1... 75 Derived::Fun2... 76 Base::Fun3... 77 Base... 78 Derived... 79 Derived::Fun1... 80 ~Derived... 81 ~Base... 82 ~Derived... 83 ~Base... 84 请按任意键继续. . . 85 */
何时需要虚析构函数?
1、当你可能通过基类指针删除派生类对象时
2、如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做很正常),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数作为虚函数。
下面的例子说明了虚函数的内存模型:
1 #include<iostream> 2 using namespace std; 3 class Base 4 { 5 public: 6 virtual void Fun1() 7 { 8 cout << "Base::Fun1..." << endl; 9 } 10 virtual void Fun2() 11 { 12 cout << "Base::Fun2..." << endl; 13 } 14 15 /*Base() 16 { 17 cout << "Base..." << endl; 18 } 19 virtual ~Base() 20 { 21 cout << "~Base..." << endl; 22 }*/ 23 int data1_; 24 }; 25 class Derived:public Base 26 { 27 public: 28 //覆盖了基类的fun2,但是并没有覆盖基类的fun1 29 virtual void Fun2() 30 { 31 cout << "Derived::Fun2..." << endl; 32 } 33 virtual void Fun3() 34 { 35 cout << "Derived::Fun3..." << endl; 36 } 37 /*Derived() 38 { 39 cout << "Derived..." << endl; 40 } 41 virtual ~Derived() 42 { 43 cout << "~Derived..." << endl; 44 }*/ 45 int data_2; 46 }; 47 typedef void(*Func)(); 48 int main(void) 49 { 50 Base b;//base头四个字节存放的是虚表指针 51 Derived d; 52 cout << sizeof(b) << endl;//8 53 cout << sizeof(d) << endl;//12 54 long **p = (long **)&b; 55 Func fun = (Func)p[0][0];//指向基类的虚函数 56 fun();//Base::Fun1...调用到了基类的虚函数 57 fun = (Func)p[0][1]; 58 fun(); 59 /* 60 Base::Fun1... 61 Derived::Fun2... 62 Derived::Fun3... 63 */ 64 p = (long**)&d; 65 fun = (Func)p[0][0];//虚表内容,并未覆盖基类fun1 66 fun(); 67 fun = (Func)p[0][1];//覆盖了基类fun2 68 fun(); 69 fun = (Func)p[0][2]; 70 fun(); 71 //实施动态绑定 72 Base* pp = &d; 73 pp->Fun2();//应该调用的是派生类成员函数。 74 return 0; 75 }
overload、overwrite、override
成员函数重载的特征:
1、 相同的作用域范围(在同一个类中)
2、 函数名相同
3、 参数不同
4、 Virtual关键字可有可无
覆盖是指派生类函数覆盖基类函数,特征是:
1、 不同的范围(分别位于派生类与基类)
2、 函数名字相同
3、 参数相同
4、 基类函数必须有virtual
重定义(派生类与基类)
1、 不同的范围(分别位于派生类与基类)‘
2、 函数名与参数都相同,无virtual关键字
3、 函数名相同,参数不同,virtual关键字可有可无
虚函数:基类指针指向派生类对象,调用的是派生类的虚函数,这就使得我们可以以一致的观点来看待不同的派生类对象。动态绑定的。
虚函数是实现多态的前提:需要在基类中定义共同的接口、接口要定义为虚函数。
如果基类不知道如何实现接口,需要将这些接口定义为纯虚函数,拥有纯虚函数的接口类称为抽象类。
在基类中不能给出有意义的虚函数定义,这时可以把它说明成纯虚函数,把他的定义留给派生类来做。
定义纯虚函数:
Virtual 返回值类型 函数名(参数表)=0;
抽象类:
作用:抽象类为抽象和设计的目的而声明,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意:抽象类只能作为基类来使用、不能声明抽象类的对象、构造函数不能是虚函数,析构函数可以是虚函数。多态基类的虚函数就该声明为虚析构函数。
虚函数是动态绑定的,先要构造出对象,对象中的虚表指针才可到虚表中的查找虚函数。
虚析构函数:
析构函数可以声明为虚函数:
delete 基类指针;
程序会根据基类指针指向的对象的类型确定要调用的析构函数
基类的析构函数为虚函数,所有派生类的析构函数都是虚函数
构造函数不得是虚函数。
如果要操作具有继承关系的类的动态对象,最好使用虚析构函数。特别是在析构函数需要完成一些有意义的操作,比如释放内存时。
析构函数还可以是纯虚的。
//如果一个类没有任何成员函数,如果没有任何接口,又想把它声明为抽象类
class Base
{
public:
//可以将析构函数声明为纯虚的
virtual ~Base() = 0 {}//实现了纯虚析构函数
};
注意:一般纯虚函数不需要实现,但是对于上述纯虚析构函数,需要实现,因为派生类析构函数会调用基类析构函数。