1.继承
有一个关于类成员初始化列表的小知识点,即采用成员初始化列表的方法初始化数据成员的时候之直接使用对应的复制构造函数进行初始化,而常规初始化则往往需要先对成员使用默认构造函数,再通过赋值运算符将参数赋值给成员。
标准string类型,有一个将const char *作为参数的构造函数,这使得允许其接受c风格字符串(“hahah”)的情况。虽然通常是const string &类型的构造函数。
派生类格式:
Class ratedplayer : public tabletennisplayer
{
…
};
冒号指出ratedplayer的基类是tabletennisplayer。Public声明头表明tabletennisplayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象,使用公有派生,基类的公有成员将称为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
继承后具有:
派生类对象存储了基类的数据成员(派生类继承了基类的实现)
派生类对象可以使用基类的方法(派生类继承了基类的接口)
需要在派生类中添加:
派生类需要自己的构造函数(此时构造函数的参数可以接受,注意暂时只是接受!基类所有的成员以及自己新添加的成员,也可以接受基类对象的引用作为参数)
派生类可以根据需要添加额外的数据成员和成员函数
构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类的方法进行访问。例如ratedplayer构造函数不能直接设置继承的成员(firstname,lastname,hastable),而必须使用公有的方法来访问私有的基类成员。具体的说派生类构造函数必须使用基类构造函数。
如:
Ratedplaye::ratedplayer(unsigned int r,const string & fn,const string &ln,bool ht):tabletennisplayer(fn,ln,ht)
{
Rating=r;
}
其中:tabletennisplayer(fn,ln,ht)是成员初始化列表,它是可执行的代码,调用tabletennisplayer的构造函数。
假设程序包含如下声明ratedplayer rplayer(1140,”mallory”,”duck”,true);
则relatedplayer构造函数将把实参“mallory”,“duck”,true赋值给形参fn,ln,ht,然后将这些参数作为实参传递给tabletennisplayer构造函数,后者将创建一个嵌套tabletennisplayer对象,并将数据“mallory”,“duck”,true存储在该对象中,然后程序进入ratedplayer构造函数体,完成ratedplayer对象的创建,并将参数r的值赋给rating成员。除非要使用默认基类构造函数,其他情况都应该显式指定正确的基类构造函数。
第二种构造函数
Ratedplayer::ratedplayer(unsigned int r,const tabletennisplayer &tp):tabletenniplayer(tp)
{
Rating=r;
}这时可以看到后面调用的是基类的复制构造函数。如果基类未定义那么将生成一个默认的浅复制构造函数。
派生类构造函数的要点:
首先创建基类对象
派生类对象构造函数应该通过成员初始化列表将基类信息传递给基类构造函数。
派生类构造函数应初始化派生类新增的数据成员
派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
注意:基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可在不进行显示类型转换的情况下引用派生类对象。
Ratedplayer rplayer(1120,”mallory”,”duck”,true);
Tabletennisplayer &rt=rplayer;
Tabletennisplayer *pt=&rplayer;
Rt.name();
Pt->name();
但是基类指针和引用只能用于调用基类方法,不能来调用派生类方法。虽然c++要求引用和指针类型与赋给的类型匹配,但这一规则对于继承来说是例外的,然而这种例外只是单向的,不可以将基类对象和地址赋值给派生类引用和指针。
这种兼容属性使得派生类对象可以调用基类的方法(继承后),同时也使得基类对象可以初始化为派生类对象。尽管不那么直接
Ratedplayer olaf1(1840,”plaf”,”loaf”,true);
Tabletennisplayer olaf2(olaf1);
不论是将基类对象初始化为派生类对象还是将派生对象赋值给基类对象,其实都是使用派生类对象中嵌套的基类对象初始化/赋值给基类对象。
继承is-a关系
公有继承最长建立的一种继承关系是,is-a关系。这种关系描述为:派生类对象也是一个基类对象,可以对基类对象执行的任何操作都可以对派生类对象进行操作。即继承可以在基类的基础上添加属性,但不能删除基类的属性。尽量使用is-a关系进行继承。
2.多态公有继承
上述例子派生类对象使用基类对象的方法,不做修改。有时希望同一个方法在派生类和基类中的行为是不同的,即方法的行为取决于调用该方法的对象。这就是多态。有两种重要的机制可用于实现多态公有继承。
在派生类中重新定义基类的方法。
使用虚方法。
关于虚方法:
需要指出同一个方法在基类和派生类中行为不同的办法是使用virtual声明,两个virtual void Viewacct()将有两个独立的定义,注意此时需要在基类和派生类中都用virtual 关键字将该方法声明一次。程序将使用对象类型来确定使用哪个版本。
使用virtual 的时候注意:如果方法是通过引用或者指针而不是对象调用的时候,程序将根据引用或者指针指向的对象的类型来选择方法。如果没有使用virtual那么程序将根据引用类型或者指针类型来选择方法。
因此经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动生成虚方法。不过在派生类中使用virtual来指出哪些函数是虚函数也不失为一个好办法。此时为基类声明一个虚析构函数也是一个惯例。另外需要注意,virtual只用于类声明中的方法原型中,而不用于对应的定义cpp中。
注意:派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方法有所不同,构造函数使用一种方法,其他成员函数使用一种技术。派生类构造函数在初始化基类私有数据时采用的是成员初始化列表。非构造函数不能使用成员初始化列表,但派生类可以调用共有的基类方法。此时可在需要操作基类私有成员之时调用带作用域符号的基类方法,针对自己的私有数据可以使用自定义的方式。
派生类没有重新定义的方法就不必使用作用域解析符。
基类中也需要一个虚析构函数的作用在于,使用指针或者引用指向对象时,如果析构函数是不虚的,那么将只调用对应于指针或者引用类型,而不管指向对象类型的析构函数,如果析构函数是虚的那么将调用对应对象类型的析构函数。
3.静态联编和动态联编
将源代码中的函数调用解释成为执行特定函数代码块被称为函数名联编。C语言中很简单,每个函数名对应一个不同的函数。但在c++中由于函数重载,就变得复杂,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而c++编译器可在编译过程中完成这种联编。在编译过程中进行联编被称为静态联编,又称为早期联编。然而虚函数仅仅是限定作用域不同,其他的都一样这就变得更加复杂,使用哪一个函数不是能在编译时确定的,因为编译器不知道用户选择哪种类型的对象,而使用虚函数的版本需要知道使用的是基类对象还是继承类对象。所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称为晚期联编。
指针和引用类型的兼容性:
由前面已知,指向基类的引用和指针可以引用派生类对象,将派生类引用或者指针转换为基类引用或指针被称为向上强制转换。向上强制转换是可传递的。也就是说brass类派生出brassplus类,再由brassplus类派生出brassplusplus类,那么brass引用或者指针可以指向brass对象,brassplus对象,brassplusplus对象。即允许向上隐式类型转换。
有一个情况需要注意,如果基类的某个方法接受的不是基类引用和指针,而是仅仅的基类对象,即传值方式,那么如果该方法还是虚方法时,将继承类对象传递给该方法时还将使用基类的虚方法,因为传值时,继承类对象仅仅将基类对象容纳的参数作为副本传值,即相当于使用基类对象调用。
如:
假设每个函数都将调用虚方法viewacct();
Void fr(brass &rb);// use rb.viewacct();
Void fp(brass *pb);// use pb->viewacct();
Void fv(brass b);//b.viewacct();
Int main()
{
Brass b(“billy bee”,123432,10000);
Brassplus bp(“betty beep”,23123,12345.0);
Fr(b); //brass::viewacct()
Fr(bp); //brassplus::viewacct()
Fp(b); //brass::viewacct();
Fp(bp); //brassplus::viewacct();
Fv(b); //brass::viewacct();
Fv(bp); //brass::viewacct()
}
因为在编译时就能知道需要使用哪个版本方法,编译器对非虚方法使用静态联编。而对虚方法使用动态联编。
将基类指针和引用转换为派生类指针和引用被成为向下强制转换,如果不适用显式类型转换,那么这种转换是不被允许的。即必须向下显示类型转换。
虚函数和动态联编:
动态联编允许重新定义类方法,但保留静态联编则是为了效率和和概念模型。在效率方面为使程序能够在运行阶段进行决策,必须采取一些方法跟踪基类指针或者引用指向的对象类型,这增加了额外的处理开销。如果类不用作基类,那么不需要动态联编,如果派生类不重新定义基类的任何方法也不需要使用动态联编。使用静态联编更加合理,效率也更高。因此被设置为c++的默认选择。在概念模型方面,在设计类时可能包含一些不在派生类重新定义的成员函数。
即如果要在派生类中重新定义基类的方法则将它设置为虚方法。否则设置为非虚方法。
虚函数的工作原理:
虚函数内在机理:编译器给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。如基类对象包含一个指针,该指针指向基类中所有虚函数的地址表,派生类对象中将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。如果派生类没有重新定义虚函数,该vtbl(虚函数表)将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也被添加到vtbl中。无论类中包含的虚函数是1个还是10个,都只需要在对象中添加一个地址成员,只是表的大小不同而已。
调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。因为不同对象的vtbl地址表中不一样,所以找到不同的表头,然后再在该表中寻找对应的函数版本。
总之在使用虚函数的时候,在内存和执行速度方面有一定的成本。包括:
每个对象都将增大,增大量为存储地址的空间。
对于每个类,编译器都创建一个虚函数地址表(数组)
对于每个函数调用,都需哟执行一项额外的操作,即到表中查找地址。
虽然非虚函数的效率比虚函数稍高,但不具备动态联编的功能。
虚函数的注意事项:
在基类方法的声明中使用关键字virtual可使得该方法在基类以及所有派生类(包括从派生类中派生出来的类)中是虚的。
如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不是用为引用或者指针类型定义的方法。这称为动态联编或晚期联编。
如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
注意:构造函数不能是虚函数。当类作基类时,析构函数应该是虚函数。这样可以使得使用基类指针为派生类对象申请内存时,当delete 基类指针时,先调用派生类的析构函数释放派神类组件指向的内存,然后调用基类析构函数释放由基类组件指向的内存。这意味着即使基类不需要显示析构函数提供服务,也应该提供虚析构函数,即使它不执行任何操作。
Virtual ~baseclass();
友元不能是虚函数,因为友元不是类成员,而只有成员才能使虚函数。
如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。
注意:重新定义继承的方法并不是重载,如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。所以必须注意以下两种特性:
注意:如果重新定义继承的方法应确保与原来的原型完全相同,但如果返回类型是基类引用或者指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。
如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则另外两个版本将被隐藏。派生类对象将无法使用它们,如果不需要修改则新定义可只调用基类版本。
2.protected保护
其与private类似,在类外只能用共有类成员来访问protected类成员,private和protected 之间的区别只有在基类派生类中才会表现,派生类成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此对于外部世界来说,保护成员的行为与私有成员类似,但对于派生类来说,保护成员的行为与公有成员相似。
抽象基类:
当基类与继承类有共性但又在多方面存有不同之时,可以从基类和派生类中抽象出它们的共性,将这些特性放在ABC中,然后从该ABC中派生出原来的基类和派生类。这样就可以使用基类指针同时管理两类对象,C++通过纯虚函数提供未实现的函数,纯虚函数声明的结尾处=0. virtual double area() const=0;
注意:当类声明中包含纯虚函数时,则不能创建该类的对象,这里的理念是包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含一个纯虚函数。原型中的=0使虚函数称为纯虚函数。C++允许纯虚函数有定义。
总之在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。抽象基类不能创建自己的对象。但可以用抽象基类的指针和引用操纵派生类的对象,被派生出来的类有时被称为具体类,即可以创建这些类型的对象。
总之,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这个接口。
继承和动态内存分配:
如果基类使用动态内存分配,并重新定义赋值和复制构造函数,将怎么影响派生类的实现呢,这个问题的答案取决于派生类的属性。如果派生类也使用动态分配。
当基类使用动态内存分配
第一种:派生类不使用new的时候,就不需要定义显示析构函数,复制构造函数和重载赋值运算符。
第二种:派生类使用了new。在这种情况下必须定义显示析构函数,赋值构造函数,和赋值运算符。
总而言之:当基类和派生类都采用动态内存分配之时,派生类的析构函数、赋值构造函数、赋值运算符都必须采用相应的基类方法来处理基类元素。这里有三种不同的方式:析构函数是自动完成的。构造函数是通过在初始化列表中调用基类的复制构造函数,如果不这样做,将自动调用基类的默认构造函数,对于赋值运算符,这是通过使用作用域解析运算符显示地调用基类的赋值运算符来完成的。
关于派生类如何使用基类的友元:
假设hasdma是basedma的派生类,那么作为hasdma类的友元,该函数能够访问hasdma新增的style成员,但是它如何访问从basedma继承而来的lable,rating成员呢,它并不是basedma的友元函数,答案是使用basedma类的友元函数operator<<().另一个问题是因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。解决办法就是使用强制类型转换,以便匹配原型时能够选择正确的函数。如下:
Std::ostream &operator<<(std::ostream &os,const hasdma &hs)
{
Os<<(const basedma &)hs;//使用基类友元函数操作继承下来的成员,使用强制类型转换得到对应的类型。
Os<<hs.style<<endl;
Return os;
}
注意赋值和初始化的区别:
如果语句创建新的对象则使用初始化,如果语句修改已有对象的值则是赋值。
将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。要将类对象转换为其他类型,应该定义转换函数,如star::star double() {…};
按引用传递对象的一个原因是提高效率,另一个原因是被定义为接受基类引用参数的函数可以接受派生类。
在普通情况下const int *ps;是指向int常量的指针,此时指向的值不能修改,但指针可以指向别的值,当int *ps const;是指向常量的int型指针,此时指针的值不能修改,但指向的值可以修改。当在类中,star::star(const char *s){…};//此时指向的值不能修改,当void star::show() const;{…};此时不能改变调用它的对象。
构造函数和析构函数,以及赋值运算符,友元函数是不能继承的,然而在释放对象时程序将首先调用派生类的析构函数,然后调用基类的析构函数,