基类与派生类、多态公有继承、虚函数的重载与重写、protected、抽象基类(ABC)
1.派生类与基类:
派生类对象存储了基类的数据成员和方法(即继承了基类的实现和接口),派生类需要自己添加新的构造和额外的数据成员即成员函数。
派生类的构造函数必须给新成员和继承的成员提供数据。
2.派生类构造函数要点:
基类对象首先被创建,派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数,派生类构造函数应初始化派生类新增的数据成员。
例如:派生类为RatedPlayer,基类为TableTennisPlayer,其中派生类私有成员为rating(unsigned int),基类成员为firstname(char),lastname(char),hasTable(bool)。
则派生类构造函数可以是:
RatedPlayer::RatedPlayer(unsigned int r,const char*fn,const char *ln,bool ht):TableTennisPlayer(fn,ln,ht)
{
rating =r;
}
也可以是:
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer &tp):TableTennisPlayer(tp),rating(r)
{
} //也可以在初始化列表中只给基类传tp,在函数中给rating赋值
如果没有在派生类的初始化列表中指明要使用的基类构造函数,派生类将调用基类的默认构造函数
3.派生类与基类的关系
派生类不能访问基类的私有成员(包括数据成员和方法),但可以使用基类的非私有方法。
基类指针可以直接指向派生类对象(不用显示转换就可以),基类引用可以直接指向派生类对象。但是不可以将派生类引用或指针指向基类对象。
但基类指针或引用只能调用基类方法
例如Rated rplayer(113,"a","b",true);
TableTennisPlay *xx=&rplayer; //这种可以。因为基类中的方法派生类都含有,xx和yy作为基类指针可以访问派生类中
TableTennisPlay &yy=rplayer; //的基类方法和派生类中的新方法。
但如 TableTennisPlay player("bsd","dd",true); //这种不可以,派生类的指针或引用可以指向派生类中的新方法,但是父类对象
RatedPlayer &rr=player; //没有派生类中特有的新方法,因此会出现问题。
RatedPlayer *pr=player;
因此,对于函数传递参数时,可以传递基类指针或引用作为参数,这样在调用时既可以传递基类亦可以传递派生类作为参数。
4.公有继承——一种is-a关系(is-a-kind-of是一种),即派生类对象也是一个基类对象,可以对基类对象执行任何操作,继承类可以在基类的基础上添加属性,但不能删除基类属性。<如香蕉派生类水果基类>,但is-a通常不可逆。
其他关系可以为has-a(派生类包含基类,但本质与基类并不相同的关系)<如午餐派生类水果基类>
is-like-a不采用明喻,本质不同但有相同部分<如律师和鲨鱼,律师可以像鲨鱼但不是鲨鱼或它的一部分>
is-implemented-as-a作为...来实现<如数组可以实现堆栈,但堆栈亦可以不用数组实现,解决方法是可以将数
组作为堆栈的对象成员>
uses-a使用关系<如电脑可以使用打印机,但打印机反过来对计算机没有意义,解决方法是可以使用友元函数>
公用继承可以实现其他关系,但不易编程,因此还是最好使用公有继承表示is-a关系
5.多态——使得同一个方法在基类和派生类中具有不同行为(即方法的行为取决于调用对象)
实现多态公有继承的方法:
在派生类中重新定义基类方法或使用虚方法
如果没有关键字virtual,程序将根据引用类型会指针类型选择方法,如果使用了virtual,程序将根据引用或指针指向的对象类型来选择方法。如:
Brass dom("Dominic Banker",12118,418); //如果ViewAcct()不是虚函数
BrasesPlus dot(“Dorothy Banker”,123,344);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use Brass::ViewAcct()
Brass dom("Dominic Banker",12118,418); //如果ViewAcct是虚函数
BrasesPlus dot(“Dorothy Banker”,123,344);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use BrassPlus::ViewAcct()
经常在基类中将派生类会重新定义的方法声明为虚方法。为基类声明一个虚拟析构函数也是一种惯例。
6.非构造函数不能使用成员初始化列表句法,但是派生类方法可以调用共有的基类方法。
7.使用虚析构的意义是,如果析构函数不是虚拟的,则将只调用对应于指针类型的析构函数,即加入申请一个基类的指针数组,并通过该数组自由操作该基类和它的派生类,但如果不使用虚析构,将只对基类的内存进行释放,而不释放它的派生类分配的内存,因为指针是基类型。但使用虚析构可以使得尽管是基类指针,但具体的调用对象又声明时它指向的对象类型决定,即使是基类型指针,如果指向派生类,则在释放时会自动调用它的派生类的析构函数。
8.联编是指函数调用解释为函数代码块的过程,静态联编是指这个过程在编译时实现,动态联编是在运行时实现。虚函数是动态联编。将派生类转化为基类的过程是向上强制转换,允许这种操作,且向上强制转换可传递。但将基类指针或引用转换为派生类,这个过程是向下强制转换,如果不适用显示类型转换,这种情况不被允许。
动态联编使得基类指针或引用可以指向派生类对象。实现的方法是在其中通过虚函数表实现。
9.哪些函数可以是虚函数总结:
(1)构造函数一定不能使虚函数
(2)析构函数一定是虚函数,除非类不做基类
(3)友元函数不能使虚函数。因为友元不是类成员。只用成员才能使虚函数
(4)没有重新定义的函数,如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本
(5)重新定义隐藏方法:在派生类中,如果重新定义了继承方法(参数与基类相同、返回值与基类相同)这种方式并不是重载,而是重写。重写后的函数可以通过基类指针或引用调用。而如果声明同名但参数种类、个数或返回值不同这种时是重载,重载的函数将覆盖掉派生类中继承的基类中的该函数,同时重载后的这个函数不能通过基类指针访问,因为基类中并没有这个函数,它是个派生类中的新函数(由于基类中该函数是虚函数,因此通过虚函数表去寻找,所以会造成重载的虚函数找不到。如果派生类中的普通新函数,基类中没有该函数,则通过在静态内存加载的方式寻找,因此基类指针将可以找到该函数)。重载后的同名函数使用父类指针访问会报错,解决的方法是在派生类中先重写这个函数,再重载。
10.派生类中的成员可以直接访问基类的保护(protected)成员,但不能直接访问基类的私有成员。protected可以用于成员函数中,可以让派生类能够访问公众不能使用的内部函数。
11.纯虚函数:C++通过纯虚函数提供未实现的函数,声明方式为在结尾处加=0。要成为ABC(抽象基类),必须至少含有一个纯虚函数。同时类声明中包含纯虚函数时,则不能创建该类的对象。