第十三章 类继承
1、类继承:扩展和修改类。
2、公有继承格式:冒号指出B类的基类是A,B是派生类。
class B :public A
{
。。。
};
3、派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。派生类对象存储了基类的数据成员(派生类继承了基类的实现);派生类对象可以使用基类的方法(派生类继承了基类的接口)。
4、需要在继承特性添加什么呢?首先,派生类需要自己的构造函数。然后,派生类可以根据需要添加额外的数据成员和成员函数。
5、派生类的构造函数必须给新成员(有的话)和继承的成员提供数据。但是,派生类不能直接访问基类的私有成员,而必须通过基类的方法进行访问。具体地说,派生类构造函数必须使用基类构造函数。
6、创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表句法完成这种工作。例如,下面是派生类B的一个构造函数: 其中,A(fn,ln,ht) 是成员初始化列表。
B::B(int r,const char *fn,const char *ln,bool ht):A(fn,ln,ht) // (fn,ln,ht)是基类私有数据成员
{
rating =r; // rating是派生类成员
}
如果省略成员初始化列表,会如何?
B::B(int r,const char *fn,const char *ln,bool ht) // (fn,ln,ht)是基类私有数据成员
{
rating =r; // rating是派生类成员
}
基类对象必须首先被创建,如果不调用基类构造函数,程序将使用默认的基类构造函数,因此上面额代码与下面的等效:
B::B(int r,const char *fn,const char *ln,bool ht):A() // (fn,ln,ht)是基类私有数据成员
{
rating =r; // rating是派生类成员
}
注:除非要使用默认构造函数,否则应显式调用正确的基类构造函数。
7、下面看第二个派生类B的构造函数: 因为tp的类型为const A &,因此将调用基类的复制构造函数。如果需要使用复制构造函数,而又没有定义,编译器将自动生成一个。
B::B(int r,const A & tp):A(tp)
{
rating =r; // rating是派生类成员
}
8、如果愿意,也可以对派生类成员使用成员初始化列表句法。
9、记住:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类的构造函数。可以使用成员初始化列表指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
10、派生类和基类之间的特殊关系:一、派生类对象可以使用基类的方法(方式和基类对象调用方法一样),条件是方法不是私有的。二、基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。不过,基类指针或引用只能用于调用基类方法。注:不可以将基类对象和地址赋给派生类引用和指针。
11、对于一个函数,如果其形参是一个基类引用,则可以使用基类对象或派生类对象作为实参。
对于一个函数,如果其形参是一个指向基类的指针,则可以使用基类对象的地址或派生类对象的地址作为实参。
12、引用兼容性属性也能够将基类对象初始化为派生类对象,尽管不那么直接。如下:
B b=(1840,"abc","def",true);// 派生类对象
A a(b); // 用派生类对象初始化基类对象
要初始化a,匹配的构造函数原型为:A (const B &);类中没有这样的构造函数,但存在隐式复制构造函数:A (const A &);形参是基类引用,因此它可以引用派生类。隐式构造函数将a初始化为嵌套在B对象b中的A对象。
13、同样,也可以将派生类对象赋给基类对象:
B b=(1840,"abc","def",true);// 派生类对象
A a;
a=b;// 用派生类对象赋给基类对象
在这种情况下,程序将使用隐式重载赋值操作符:
A & operator=(const A &)const;基类引用指向的也是派生类对象,因此b的基类部分被复制给a。
14、C++有3种继承方式:公有继承、保护继承和私有继承。公有继承最常用,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
15、类方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态——具有多种形态,就是指同一个方法的行为将随上下文而异。有两种重要的机制可以用于实现多态公有继承:①在派生类中重新定义基类的方法。②使用虚方法。
16、对于在两个类中行为相同的方法,则只在基类中声明。
17、基类方法如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
18、经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚拟的后,它在派生类中将自动成为虚方法。
19、在派生类方法中,标准的技术是使用作用域解析操作符来调用基类方法。
20、可以创建指向基类的指针数组,那么数组里的指针既可以指向基类对象,也可以指向派生类对象,因此可以使用一个数组来表示多种类型的对象,这就是多态!!
21、为何需要虚拟析构函数?如果析构函数不是虚拟的,则将只调用对应于指针类型的析构函数。如果析构函数是虚拟的,将调用相应对象类型的析构函数。因此,使用虚拟析构函数可以确保正确的析构函数序列被调用。
22、静态联编(早期联编):编译器负责回答程序调用函数时,将使用哪个可执行代码块。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C++中,由于函数重载的缘故,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编。动态联编(晚期联编):使用哪个函数不能在编译时确定,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编。
23、将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。如下:
B b=(1840,"abc","def",true);// 派生类对象
A *a=&b; // A &a=b;
将基类指针或引用转换为派生类指针或引用,称为向下强制转换,如果不使用显式类型转换,则向下强制转换是不允许的。
A a;// 基类
B b;// 派生类
A *a=&b;
B *b=(B *)&a;
24、记住:派生类对象都是基类对象,因为它继承了基类对象所有的数据成员和成员函数。所以,可以对基类对象执行的操作,都适用于派生类对象。
25、编译器对非虚方法使用静态联编;对虚方法使用动态联编。
26、如果类不会用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率更高。因此,静态联编被设置为C++的默认选择。
27、虚函数的工作原理:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(vtbl)。虚表中存储了为类对象进行声明的虚函数的地址。基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数将保存新函数的地址。如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的定义。如果派生类重新定义了新的虚函数,该函数的地址也将被添加到vtbl中。注意:无论类中包含的虚函数是一个还是10个,都只需要在对象中添加一个地址成员,只是表的大小不同而已。
28、使用虚函数的内存和执行速度的成本:①每个对象都将增大,增大量为存储地址的空间。②对每个类,编译器都创建一个虚函数地址表(数组)。③每个函数调用都需要执行以不额外的操作,即到表中查找地址。
29、构造函数不能是虚函数;析构函数应当是虚函数,除非类不用作基类;友元不能是虚函数,因为友元不是类成员,只有类成员才能是虚函数。
30、如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外情况是基类版本是隐藏的。
31、重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管特征标如何。
32、两条经验规则:①如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。②如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义了一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本。
33、private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员类似。
34、C++通过使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处为=0.当类中包含纯虚函数时,则不能创建该类的对象,包含纯虚函数的类只用作基类!!要成为纯虚函数,必须至少包含一个纯虚函数。纯虚函数可以定义也可以不定义!!
35、抽象基类(ABC)的派生类被称为具体类。ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类根据派生类的具体特征,使用常规函数来实现这种接口。ABC包含派生类共有的所有方法和数据成员,而那些在派生类中的行为不同的方法应被声明为虚函数。
36、继承和动态内存分配:①派生类不使用new:不需要为派生类定义显式析构函数、复制构造函数和赋值操作符。②派生类使用new:必须为派生类定义显式析构函数、复制构造函数和赋值操作符。注:派生类复制构造函数只能访问派生类的数据,因此它必须调用基类复制构造函数来处理共享的基类数据。
37、总结:当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数和赋值操作符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果别这样做,将自动调用基类的默认构造函数。对于赋值构造函数,这是通过使用作用域解析操作符显式的调用基类的赋值操作符来完成的。
38、关于友元函数:如果希望派生类的友元函数能够使用基类的友元函数,可以通过强制类型转换将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。
39、如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。
40、什么不能被继承?构造函数、析构函数、赋值操作符、友元函数。
41、可以将基类对象赋给派生类对象么?可以!!法①派生类有一个转换构造函数:B(const A &),转换构造函数可以接受一个类型为基类的参数和其他参数,条件是其他参数有默认值:B(const A & a,double c=1.0,double r=0.1);如果有转换函数,程序将通过它根据基类对象来创建一个派生类对象,然后将它用做赋值操作符的参数。法②定义一个用于将基类赋给派生类的赋值操作符:B & B::operator=(const A &){...}