1.基本概述:
首先说下总体的规则,相当于重点:派生类可以经过继承后,拥有基类的所有数据成员和部分函数(不包括构造函数,赋值运算符和友元函数),但是拥有了,却不一定能使用,这里并不区分私有继承还是公共继承,都是只能访问基类中public和protected的成员。也就是说可不可以访问,要看基类的中成员变量是否可以被访问(public和protected),而不是看继承之后属性是否可以被访问。
经过公有继承继承后,原来的暴露属性还是原来的属性。私有继承则是将继承过来的所有public和protectd都变为私有的属性,也就是在第三代继承后,第三代成员就不能在访问基类中的共有方法了。
在继承时不管是什么类型的继承,都会将基类的所有数据成员和函数复制到子类的里边,是所有的,包括基类的私有成员和函数。随意子类的成员变量就是子类的成员变量加上父类的成员变量。那么在子类的构造函数的时候,就需要将所有的成员变量初始化。一般有两种方法:
class RatedPlayer : public TableTennisPlayer
//方法1,将父类和子类的成员都列出来初始化
Ratedplayer::RatedPlayer(unsigned int r,const string & frr, const string & ln,bool ht):TableTennisplayer(fn , In , ht)
{
firstname = fn; //基类中的私有,不能显示访问,所以只能通过调用基类的构造函数
lastname = In; //基类中私有,不能这样用
hasTable = ht; //基类中私有,不能这样用,当然可以把初始化表放在下面
//TableTennisplayer(fn,in,ht);
rating = r;
}
//同样是方法1,如果省略掉父类的几个属性,那么相当于调用父类的默认构造函数。
Ratedplayer::RatedPlayer(unsigned int r,const string & frr, const string & ln,bool ht)
{
rating = r;
}
//方法2,传入父类的引用,
RatedPlayer::RatadPlByer(unsigned int r, const tableTennisPlayer & tp):trableTennisPlayer(tp),rating(r){}
在本书中,将继承视为先创建基类的对象,然后在创建子类的对象,所以在初始化的时候,也要先初始化基类中的属性。因为常常基类中属性都是私有的,所以只能通过基类中共有的方法,如构造函数等。所以继承的时候一定注意不仅仅要初始化自己的成员变量。在析构函数调用是,则是先调用子类的析构函数,然后在调用父类的析构函数。当然这个过程类似于构造函数的过程, 如果程序员不予以明确的声明调用析构函数,那么编译器将自动调用默认析构函数,也就是唯一定义的那个。
2.派生类和基类之间的关系:
a.派生类可以访问基类中的共有方法
b.基类指针可以指向派生类
c.基类引用可以引用派生类。
所以在作为函数参数传递时,将派生类的指针作为基类的参数传递过去,如下面的函数
void Show(const TableTennisPlayer & rt);
TableTennisPlayer playerl("Tara","Boopca", false);
RatedPlayer rplayerl (1140, "Mallory", "Duck", true);
Show (playerl );
Show (rplayerl);
在使用指向派生类的基类指针的时候常常会遇到搞不清楚到底指针调用的是基类的方法还是派生类的方法。所以这里说下,当基类中的方法并没非是虚函数的时候,基类指针调用的都是基类自己的方法,但是遇到用虚函数的时候,就要引用派生类新的类型。
3.为什么需要虚析构函数:
在派生类中的周期结束后,会调用析构函数,如果不是虚析构函数,在不使用指向基类的指针的时候并没有什么区别,都是先调用派生类的析构函数,在派生类的析构函数中会默认调用基类的析构函数。但是如果使用的是指向派生类的基类指针,在指针被销毁的时候,理论上应该先调用派生类的指针,但是如果没有虚析构函数,那么就会造成直接调用基类的析构函数。造成构造函数和析构函数的不对应。
4.关于静态关联和动态关联:
一般的使用定义过程都是在静态关联,在定义一个变量的时候,编译器就能够将其翻译为目标文件的语言,但是在使用虚函数派生的时候,编译器将不知道具体使用的是哪个派生类对象,所以使用的是动态关联。
静态关联的效率更高,因为使用动态关联实在运行的时候才知道具体的使用方式,所以必须使用一些方式来跟踪基类指针的指向。所以在设计类的时候也不能为了省事,而将所有方法都设计为虚函数,可以根据要不要重写这种方法来决定,可以增加效率。当然这有一定难度,因为需求不定,不能知道具体的实现方式。
5.虚函数的原理--虚函数表
虚函数是如何实现动态关联的?虚函数底层的工作原理是什么?虚函数使用的是虚函数表,具体就是没一个类中都有一个隐藏的指针指向一个数组表,这个表中保存了所有虚函数的地址。在派生类中同样存在着一个类似的虚函数表,但是此表与基类的表略有不同,如果派生类中没有重写虚函数,那么此表对应位置的虚函数地址还是父类的地址,如果重写了虚函数,那么对应此函数的位置的地址值就是新的重写的函数的地址。
类似于基类A,有两个虚函数virtual funcA_a(),virtual funcA_b(),那么此表的值为table={0x45,0x78}假设的值;
派生类B,重写funcA_b(),并新增funcB_c()。那么此表的值为table={0x45,0x96,0x99},瞎编的数值,只是表示相对地址改变。
在每次函数的调用的时候,都会先访问次对象中隐藏的虚函数表中此函数对应位置处的地址,然后确定其函数到底是哪个,然后在调用。在这个过程中就会更消耗资源。
6.继承中需注意:
a.构造函数不能是虚函数,如果是虚函数,在创建的时候就会调用调用继承类的构造函数,就会出问题。
b.析构函数一般设为虚函数。
c.重新定义将隐藏方法:重新定义是指名字与基类中的函数相同,但是参数却不同,不管是虚函数还是普通函数,原来的函数都会被覆盖。所以在定义函数的时候就需要严格依据基类中的名字做定义;同时如果一个相同名字的函数被重载了,那么需要重载所有同名的函数。
7.派生类使用new
在派生类的构造函数中使用new来初始化部分属性,同时在基类中也有类似操作,就会出现问题。但是在使用基类的时候,我们往往无法看到基类的内容,所以这里建议不管基类中是否应用new初始化属性,都必须显式的在派生类中声明复制构造函数,赋值运算符,而析构函数因为会自动调用默认析构函数,所以并不需要特殊处理,只需销毁自己的属性就可以了。
其实显示声明复制构造函数,就是显式的在开始调用一下基类的复制构造函数,而复制构造函数则是显式的调用基类的赋值运算符。如下代码:
baseDMA::operators(hs);
8.关于私有继承
私有继承是将基类的成员继承过来变为派生类的私有成员,但是派生类只允许使用基类的共有和保护成员,私有成员只能通过显式调用构造函数初始化。
这里说一下比较少用的一个地方,基类的公有成员经过私有继承之后,变为派生类的私有成员,外部不可访问,但是可以使用using关键词声明,公有的成员经过继承后,仍然是公有的,外部可以访问。如以下代码:只包含函数名就可以了,不需要额外的参数和返回值等。
using std::baseClass::min;
如果在继承的时候不明确声明继承方式,则默认是隐式声明。
9.多重继承:
多重继承是指,一个类继承多个基类,有时候我们常常会遇到这样的实际问题,但是在使用的过程中,常常会遇到一个问题,就是菱形继承问题,也就是有基类A,然后子类B,C都是继承A的,基类A中有个work函数,那么B,C都重写了这个work,然后超级子类D又多重继承了B,C,那么盲目使用就会出现问题,如A *pa = new D();这样使用就出现问题了,实际上D中有两个work函数,分别是继承B和继承C的,如果自己在重写,那么就相当于有三个work函数,基类实在无法确定这个是哪个work函数,那么这样使用就产生了二义性。
为了解决这个问题,C++提出了虚基类这个概念,也就是在继承的时候增加virtual关键字,如:virtual public A,这样就只含有一个基类的work,而并非多个。
如果我们在上面不是用多态的性质,而且使用虚基类,而是直接创建D类对象,那么在调用work函数的时候,依然会出现二义性,此时的解决方案是在函数前加一个类解析符,如B.work();上面说的两个问题看似都是二义性的问题,但是一个是使用多态的时候面临的二义性,而另一个是在不用多态性,而直接调用函数的二义性。