目录
继承与派生的概念
1.继承的层次结构
类的层次结构关系总结
2. 为什么要使用继承
基类与派生类
1.派生类的定义
C++派生类的定义格式:
C++派生类的继承方式:
2.派生类的生成过程
●吸收基类成员
●改造基类成员
●添加派生类新成员
3.继承中的构造函数和析构函数
4.组合与继承
访问权限控制
1.公有继承
2.私有继承
3.保护继承
4.三种继承方式的属性表
派生类的构造函数、析构函数
1. 派生类的构造函数
派生类构造函数的格式:
派生类构造函数的注意事项:
2.派生类的析构函数
派生类析构函数的调用顺序:
多继承及虚基类
1.多继承的定义
2.多继承的注意事项
3.多继承的构造函数
构造函数格式及注意事项
多继承方式下构造函数的执行顺序:
4.多继承的析构函数
5.虚基类
(1)虚基类的定义
(2) 虚基类的初始化及构造函数调用
(3)二义性的解决方法
赋值兼容原则
继承是自然界的一个普遍的重要特性,派生使新类再继承共性的同时,具有了更加丰富多彩的个性
●面向对象程序设计的继承与派生机制是源于自然界中的概念。
●一般采用层次分类方法来描述事物之间的关系。
如图所示:飞行器类的层次结构关系图
●类的层次结构图中,下层类是上层类的特殊类。
●下层类自动具有上层类的特性,同时也具有自身新的特性。
●越往层次结构图的下层,其特性越具体化
●这种从上到下的层次结构关系体现了继承与派生的过程。
● C++面向对象技术也采用了这种继承机制。
●由基类派生出新类的过程称为派生,派生类自身也可以作为基类派生出新的派生类。继承是指派生类自动拥有基类的属性和行为特征。
●派生类自动拥有基类的属性和行为,并表现出自身新的属性和行为特征。
●类的继承和派生机制使程序员无须修改已有的类,只需在既有类的基础上,根据问题的需要,通过增加部分代码或修改少量代码而得到新的类(派生类),从而很好的解决了程序代码的重用问题,避免了代码的重复。
基类与派生类的关系是相对的
多继承:一个派生类有多个直接基类
单继承:一个派生类只有一个直接基类
public:公有继承 private:私有继承 protected:保护继承
特别注意:未标注情况下表示为private继承
派生类的生成过程经历了三个步骤:
●在C++的继承机制中,派生类吸收基类中除构造函数和析构函数之外的全部成员。
●吸收基类成员是一个重用的过程,体现了代码的重用
●由于基类的部分成员在派生类中可能不需要却也被继承下来,对这些没有实际需要而被继承的成员,在派生类中需要对其进行改造。
●改造基类成员包括两个方面:
(1) 通过派生的三种继承方式来控制;
(2) 通过在派生类中定义同名成员(包括成员函数和数据成员)来屏蔽在派生类中不起作用的部分基类成员。(这里也可以称为覆盖)
●添加派生类的新成员是继承机制的核心内容。
●仅仅继承基类的成员是不够的,需要在派生类中添加新成员,以保证派生类自身特殊属性和行为的实现。
●派生类不继承基类的构造函数和析构函数,但是能调用基类的构造函数和析构函数;
●派生类的构造函数总是先调用基类的构造函数来初始化派生类中的基类成员,再进行派生类中成员的初始化;
class Base { public: Base(int i); ~Base(); void print(); private: int a; }; class Derive : public Base { public: Derive(int i, int j); ~Derive(); void print(); private: int b; }; Base::Base(int i) { a=i; cout<< "Base constructor" <<endl; } Base::~Base() { cout<< "Base destructor" <<endl; } void Base::print() { cout<< a << endl; } Derive::Derive(int i, int j) : Base(i) { b=j; cout<< "Derive constructor" <<endl; } Derive::~Derive() { cout<< "Derive destructor" <<endl; } void Derive::print() { Base::print(); cout<< b << endl; } int main() { Derive d(2,5); d.print(); }
运行结果如下图:
●组合:一个类的数据成员是另一个类的对象
●继承和组合都利用了已经定义的类,但是类之间关系上有差别
●基类中的成员(公有成员和保护成员)被继承后访问属性没有发生变化,但是派生类的成员无法访问基类的私有成员
class Location { public: void InitL(int xx,int yy) {X=xx; Y=yy;} void Move(int xOff,int yOff) {X+=xOff; Y+=yOff;} int GetX() {return X;} int GetY() {return Y;} private: int X,Y; }; class Rectangle : public Location //派生类 { public: void InitR(int x,int y,int w,int h); int GetH() {return H;} int GetW() {return W;} private: int W,H; }; inline void Rectangle::InitR(int x,int y,int w,int h) { InitL(x,y); //派生类直接访问基类的公有成员 W=w; H=h; } int main() { Rectangle rect; rect.InitR(2,3,20,10); rect.Move(3,2); //对象访问基类的公有成员 cout<<rect.GetX()<<',' //对象访问基类的公有成员 <<rect.GetY()<<',' <<rect.GetH()<<',' <<rect.GetW()<<endl; return 0; }
●基类中的公有成员和保护成员成为派生类的私有成员,在派生类中成员可以直接访问他们,但是派生类的成员无法访问基类的私有成员;
●在派生类的外部,派生类的成员和对象均无法访问基类的全部成员
●私有继承之后,全部基类成员在派生类中都成为了私有成员或不可访问的成员,无法进一步派生。
●私有继承方式一般很少使用。
class Rectangle : private Location { public: void InitR(int x,int y,int w,int h); void Move(int xOff,int yOff); int GetX(); int GetY(); int GetH() {return H;} int GetW() {return W;} private: int W,H; }; inline void Rectangle::InitR(int x,int y,int w,int h) { InitL(x,y); //派生类直接访问原公有成员 W=w; H=h; } void Rectangle::Move(int xOff,int yOff) { Location::Move(xOff,yOff); } int Rectangle::GetX() { return Location::GetX(); } int Rectangle::GetY() { return Location::GetY(); } int main() { Rectangle rect; rect.InitR(2,3,20,10); rect.Move(3,2); //对象访问派生类的函数 cout<<rect.GetX()<<‘,‘ //对象访问派生类的函数 <<rect.GetY()<<',' <<rect.GetH()<<',' <<rect.GetW()<<endl; return 0; }
●在派生类中,基类的公有成员和保护成员均作为派生类的保护成员,派生类的成员可以直接访问它们,而派生类的成员无法访问基类的私有成员。
●在派生类的外部,派生类的成员和派生类的对象均无法访问基类的全部成员。
●如果基类只进行了一次派生,则保护继承和私有继承的功能完全相同,但保护继承可以进一步派生,而私有继承则不可以,两者具有实质性差别。
●基类的构造函数的功能是创建基类对象并进行初始化,而析构函数的功能在基类对象生存期结束时对基类对象进行必要的清理工作。
●在派生类的生成过程中,派生类将产生新的成员,对新增数据成员的初始化需要由派生类自身的构造函数完成,而对派生类对象的清理工作需要由相应的析构函数完成。
派生类构造函数在对其对象初始化时需要对基类数据成员(此时需要调用基类的构造函数)、新增数据成员和内嵌对象成员进行初始化。
●当基类中没有显式定义构造函数时,派生类的构造函数定义可省略,系统采用默认的构造函数。
●当基类定义了具有形参的构造函数时,派生类也必须定义构造函数,提供将参数传递给基类构造函数的途径,使基类对象在进行初始化时可以获得相关数据。
●派生类构造函数的执行顺序:先调用基类构造函数,再调用内嵌成员对象的构造函数(如果有内嵌成员对象),最后才执行派生类构造函数。
派生类的析构函数的功能是在该类对象生存期结束前进行一些相关的清理工作。
●先调用派生类的析构函数
●再调用对象成员类的析构函数(如果有对象成员)
●最后调用基类的析构函数,其执行顺序与构造函数的执行顺序完全相反。
●定义具有两个或两个以上基类的派生类与定义单继承是类似的。
●在多个基类及继承方式之间用逗号分隔。
class child: public father,public mother { ............. };
●冒号之后为基类表,每一个基类名前都有继承方式。
●若缺省,系统默认为私有继承方式。
●实际上,派生类与每个基类之间的关系可以认为是一个单继承。
●多继承可以认为是单继承的自然拓展。
总参数表必须包含完成所有基类初始化需要的参数,各基类构造函数之间以逗号分隔。
●先执行所有基类的构造函数
●再执行对象成员的构造函数
●最后执行派生类的构造函数
内嵌对象成员的构造函数执行顺序与对象在派生类中声明的顺序一致,而处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的基类顺序,而与派生类构造函数中所定义的成员初始化列表顺序没有关系。
●虚拟基类:按它们被继承的声明顺序构造;
●非虚拟基类:按它们被继承的声明顺序构造;
●派生类的成员对象:按它们被声明的顺序构造;
●执行派生类的构造函数体;
●析构函数的调用次序与构造函数相反;
●析构函数名同样与类名相同,无返回值、无参数,而且其定义方式与基类中的析构函数的定义方式完全相同。
●功能是在派生类中对新增的有关成员进行必要的清理工作。
●析构函数的执行顺序与多继承方式下构造函数的执行顺序完全相反,首先对派生类新增的数据成员进行清理,再对派生类对象成员进行清理,最后才对基类继承来的成员进行清理。
虚基类的定义需要通过关键字virtual限定, 其定义格式如下:
在定义了虚基类以后,派生类对象中只存在一个虚基类成员的副本。
虚基类的初始化:
虚基类的初始化与一般多继承的初始化在语法上相同,如果在基类中定义了带参数的构造函数,且没有定义默认形式的构造函数,则整个继承结构中,虚基类所有的直接或间接派生类必须在派生类的初始化中列出对虚基类构造函数的调用。
●一般情况下,在派生类中对基类成员的访问应该是惟一的。
●但在多继承方式下,可能造成对基类中某个成员的访问出现不惟一的情况,这种现象称为对基类成员访问的二义性问题。具体而言,二义性指在多继承方式下,派生类的某些数据成员可能出现多个副本,或成员函数出现多个映射地址等现象。
虚基类构造函数的调用顺序
●先调用虚基类的构造函数,再调用非虚基类的构造函数。
●若同一层次中包含多个虚基类,其调用顺序按定义时的顺序进行。
●若虚基类由非虚基类派生而来,则按先调用基类构造函数,再调用派生类构造函数的顺序进行。
●创建对象时,如果该对象含有从虚基类继承来的成员,则虚基类成员是由最远派生类的构造函数的构造函数通过调用虚基类的构造函数进行初始化,该派生类的其他基类对虚基类构造函数的调用被编译系统自动忽略。
●使用作用域运算符“::”通过直接基类名进行限定,这种解决方法的特点是派生类中的同名成员依然具有多个副本,但通过基类名标识。
●通过定义虚基类来解决,在多继承方式下,派生类的同名数据成员在内存中出现多个副本、同名成员函数出现多个地址映射,如果将直接基类的共同基类设置为虚基类,那么从不同的路径继承过来的同名成员在内存中只拥有一个副本,从而解决了同名成员的二义性问题。
●赋值兼容规则指在程序中需要使用基类对象的任何地方,都可以用公有派生类的对象来替代。
●在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
若定义了以下类A和类B: class A {…} class B:public A {…} main() { A a1,*p; B b1,*pb; }
根据赋值兼容规则,该例可进行以下替代:
(一个公有继承的派生类对象可以隐式转化为一个基类对象)
●可以利用派生类对象赋值给基类对象,如:a1=b1;
●可以利用派生类对象初始化基类对象的引用,如: A &a1=b1;
●可以利用派生类对象的地址赋给指向基类的指针,如: p=&b1;
● 可以将指向派生类对象的指针的值赋值给指向基类对象的指针,如: p=pb;
基类对象不能代替派生类对象