文章目录
派生与继承
派生类说明
class <类名>:<基类说明符>{
...
};
class manager:public employee{...};
//不写派生方式默认为private
派生方式决定了从基类继承过来的成员在派生类中的封装属性。
派生方式 | 基类存取权限 | 派生类存取权限 |
---|---|---|
public | public | public |
public | protected | protected |
public | private | 不可访问 |
protected | public | protected |
protected | protected | protected |
protected | provate | 不可访问 |
private | public | private |
private | protected | private |
private | private | 不可访问 |
- 一个类可以派生出多个派生类
- 一个类可以有一个或多个基类,称为单一继承或多重继承
- 派生类可以派生出类,称为多级继承
- 继承关系不可循环 `A->B->C->A
- 基类的友元关系和基类的构造函数和析构函数都不能被派生类所继承
派生类的构造函数和析构函数
虽然类B的派生类A不可以访问基类B的私有成员,但是派生类A的对象的创建却必须包含着基类B的对象的创建。
构造函数和析构函数是用来创建和释放该类的对象的,当这个类是派生类时,其对象的创建和释放应与其基类对象的创建和释放相联系。
派生类的构造函数:
<派生类名> (<参数总表>):<初始化符表>{
<构造函数体>
}
/*初始化符表列出各基类及各按如下格式构成:
<基类名1>(<基类参数表1>),...,<基类名n>(<基类参数表n>),<对象成员名1>(<对象成员参数表1>),...,<对象成员名n>(<对象成员参数表n>)
*/
创建派生类对象时系统按下列步骤工作:
- 调用各基类的构造函数,调用顺序按照它们被继承时的声明顺序(从左到右)
- 再调用基类各对象成员的构造函数,调用顺序按照它们在派生类中的声明顺序(从左到右)。注意:派生类中声明对象顺序可以与派生类构造函数处所列对象成员的顺序不相同,它们之间无必然联系。
- 最后调用派生类自己的构造函数。
释放派生类对象时系统的工作步骤则相反
- 先调用派生类自己的析构函数
- 再调用对象成员的析构函数,调用顺序按照它们在派生类中声明的相反顺序,从右向左。
- 最后调用各基类的析构函数,调用顺序按照它们被继承时声明的相反顺序,从右向左。
其他特征的继承关系
友元关系以及静态成员的继承
友元关系
基类的友元不继承。即如果基类有友元类或友元函数,则其派生类不因继承关系也有此友元类或友元函数。
另一方面,如果基类是某类的友元,这种友元关系是被继承的。即被派生类继承过来的成员如果原来是某类的友元,那么它作为派生类的成员仍然是某类的友元。
静态成员的继承
如果基类中被派生类继承的成员是静态成员,则其静态属性也随静态成员被继承过来。
无论这些成员有多少个对象被创建,都只有一个拷贝,被基类和派生类的所有对象共享。<类名>::<成员名>
基类对象和派生类对象间的赋值
派生类对象间的赋值操作依据下面的原则:
- 如果派生类有自己的赋值运算符的重载定义,即按该重载函数处理
- 派生类未定义自己的复制操作,基类定义了赋值操作,则系统自动定义派生类赋值操作,其基类成员的赋值按基类的赋值操作进行
- 二者都未定义,系统自动定义默认赋值操作(按位进行复制)
另外,基类对象和派生类对象之间允许有下述的赋值关系(允许将派生类对象“当作”基类对象来使用)
- <基类对象>=<派生类对象>
- <指向基类的指针>=<派生类对象的地址>允许这种形式的赋值是函数超载及虚函数用法的基础。
- <基类的引用>=<派生类对象>。允许派生类对象初始化基类的引用。
通过引用只可以访问基类成员部分,但不可访问派生类对象的非基类成员部分,因为不可将基类的引用强制转换成其派生类类型。
派生关系中的二义性问题
基类与派生类间重名成员的处理
不加类名限定时默认为是处理派生类成员,而要访问基类重名成员时,则要通过类名限定。
函数成员的重名是指二者的原型相同,若函数名相同,但参数的个数和类型不尽相同,也无须进行二义性处理。
多重继承情况下两基类间重名成员的处理
类名限定,默认是处理派生类成员。
多级混合继承(非虚拟继承)包含两个基类实例情况的处理
A->B
A->C
B、C->D
D中会包含两个类A的实例,可能会产生二义性。
两种解决方法:
- 仍用类名限定
- 通过虚基类,这是一个较好的处理方式
为了解决由混合(多重多级)继承造成的二义性问题。
当类A作为类B、C的基类,当把类A定义为派生类B、C的虚基类时,各派生类的对象共享其基类A的一个复制。这种继承称为共享继承。(把基类声明成虚基类,增加关键词virtual)
class A{...};
class B: virtual public A{...};
class C: virtual public A{...};
class D: public B, public C{...};
多态性与虚函数
OOP三大特征:封装性、继承性、多态性
对于同一消息——指对于类的成员函数的调用命令,当不同类型的对象接收时可以导致完全不同的行为,谓之多态性—>动态联编、虚函数实现。
函数重载与静态联编
静态联编:在编译时就可以确定该函数与程序中的哪一段代码相联系,即在编译时就已确定函数调用语句对应的函数体代码,故称为静态联编处理方式。
函数超载、虚函数及动态联编
不同类中具有相同函数原型的函数,称为函数的超载。它的实现与类的继承与派生相联系。
虚函数概念是实现的关键。
由于超载函数允许不同函数具有相同的函数原型,故在编译阶段无法判断此次调用应执行哪一段函数代码。只有到了运行过程中执行到此处时,才能临时判断应执行哪一函数代码。这种方式叫做动态联编。
函数超载
- 在基类与其多个派生类的范围内实现
- 实现的关键是虚函数和动态联编机制
- 通常这些函数语义相近。
虚函数
在类的说明中,将其某一非静态成员函数的属性说明为virtual,则称该函数为虚函数。
virtual <返回类型><函数名>(<参数表>) {...};
若基类中某函数被说明成了虚函数,则其派生类中也要列出与该函数的原型相同但函数体不同的成员函数。
class graphelem{
protected:
int color;
public:
graphelem(int col){
color = col;
}
virtual void draw(){...};//基类中含有一个虚函数
};
class line:public graphelem{
public:
virtual void draw(){...};
...
};
class circle:public graphelem{
public:
virtual void draw(){...};
...
};
class triangle:public graphelem{
public:
virtual void draw(){...};
...
};
关键词virtual
在派生类说明中可以缺省。它们自动地定义为虚函数。在基类中不可省略。
虚函数的调用机制是通过动态联编实现。
虚函数的特点:
- 虚函数不仅同名,而且同原型
- 虚函数仅用于基类和派生类中
- 虚函数需要在程序运行时通过动态联编以确定具体函数代码
- 虚函数一般是一组语义相近的函数
构造函数不能说明为虚函数!
构造函数的调用一般出现在对象创建的同时或之前,这时无法用指向其对象的指针引用它。
析构函数可以说明为虚函数
当在析构函数中采用基类指针释放对象时,故意把析构函数说明为虚函数,以确定释放的对象。
动态联编
graphelem *pObj;
line lin1;
circle cir1;
triangle tri1;
pObj=&lin1; pObj->draw();
pObj=&cir1; pObj->draw();
pObj=&tri1; pObj->draw();
纯虚函数与抽象基类
如果不准备在基类的虚函数中做任何事情,则可使用如下的格式将该虚函数说明为纯虚函数:
virtual <函数原型>=0;
(不能被直接调用)
- 纯虚函数只为其派生类的各虚函数规定的一个一致的“原型规格”,该虚函数的实现将在它的派生类中给出。
- 含有纯虚函数的基类称为抽象基类。注意,不可使用抽象基类来创建它自己的对象,只有在创建其派生类对象时才有抽象基类自身的实例伴随而生。
- 抽象基类的引入体现了上文中虚函数使基类作为这一组类的抽象对外接口的思想。通过抽象基类,再“加上”各派生类的特有成员以及对基类中的纯虚函数的具体实现,方可构成一个具体的实用类型。
- 注意如果一个抽象基类的派生类中没有定义自己的纯虚函数,而只是继承了基类的纯虚函数的话,那这个派生类还是一个抽象基类。