0%

C++复习(继承与多态)

1、继承

基类的构造函数和析构函数不可以被继承。

派生类继承至基类(父类继承至子类),派生类对于基类成员的继承是没有选择的,不能选择接收或者舍弃基类中的成员。

2、类的层次结构

通过继承可以形成类的层次结构,比如:

1
2
3
4
5
class A {......};

class B:public A {......};

class C:public B{.......};

即A为顶层类,不存在不可访问成员,C作为底层类。

3、基类成员按操作分类

public成员:公有成员,类内、类外、派生类都可以使用。

protect成员:受保护成员,类内、派生类可以使用。

private成员:私有成员,类内可以使用。

不可访问成员:不可访问。

4、继承权限

继承类型有三种,公有继承(public)私有继承(private)保护继承(protected)

public继承:公有继承,所有类型成员权限都不变

protect继承:保护继承,public和protect成员都变成protect权限成员,可以依此多次保护继承,而数据仍然为保护权限。

private继承:私有继承,public和protect成员都变成privatet权限成员,可以依此多次私有继承,而终止基类功能在子类中的派生。

公有继承:

公有继承的特点是基类成员在派生类中都保持原来的状态

  • 基类中的public仍为public,
  • 基类中的protected仍为protected,
  • 基类中的private仍为private;

私有继承:

私有继承的特点是基类中所有成员在派生类中都变为私有成员

  • 基类中的public,protected变为private,
  • 基类中的private仍为private;

保护继承:

  • 基类中的public变为protected,
  • 基类中的protected仍为protected,
  • 基类中的private仍为private;

private在派生类中依然存在,但不论以哪种方法继承基类,派生类都不能直接访问基类的私有成员。

继承方式 基类的public成员 基类的protected成员 基类的private成员
public继承 public成员 protected成员 private成员
protected继承 protected成员 protected成员 private成员
private继承 private成员 private成员 private成员

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Base {	//基类 
public :
int pub;
private:
int pri;
protected :
int pro;
};

class A : public Base{ //public继承
public :
int a;
void init() {
a = pub; //可以,依然为public成员
a = pro; //可以,依然为protected成员
a = pri; //错误,基类的私有成员在派生类中是不可见的
}
};

class B : protected Base{ //protected继承
public :
int b;
void init() {
b = pub; //可以,变为protected成员
b = pro; //可以,依然为protected成员
b = pri; //错误,基类的私有成员在派生类中是不可见的
}
};

class C : private Base{ //private继承
public :
int c;
void init() {
c = pub; //可以,变为private成员
c = pro; //可以,变为private成员
c = pri; //错误,基类的私有成员在派生类中是不可见的
}
};
int x;
A a;
x = a.pub; //可以,public继承的public成员是public的,对对象可见
x = a.pro; //错误,public继承的protected成员是protected的,对对象不可见
x = a.pri; //错误,public继承的private成员是private的,对对象不可见

B b;
x = b.pub; //错误,protected继承的public成员是protected的,对对象不可见
x = b.pro; //错误,protected继承的protected成员是protected的,对对象不可见
x = b.pri; //错误,protected继承的private成员是private的,对对象不可见

C c;
x = c.pub; //错误,protected继承的public成员是private的,对对象不可见
x = c.pro; //错误,protected继承的protected成员是private的,对对象不可见
x = c.pri; //错误,protected继承的private成员是private的,对对象不可见
return 0;
  • public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
  • protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
  • class的默认继承是private的,struct的默认继承是public的。

5、类型兼容

不同类型数据之间在一定条件下可以进行类型的转换,比如int n =‘a’,由于字符和整型兼容,可以对整型变量n赋值为字符’a’。基类和派生类对象之间也有赋值兼容的关系,可以进行类型建的转换。

一般继承情况下,派生类对象会首先存放基类的数据成员,在存储派生类新增的数据成员。并且所占字节数就是类型字节数。

重点理解派生类对象在基类对象数据成员的基础上新增了自己的数据成员,因此两者兼容关系是不可逆的。

派生类对象可以像基类对象赋值。

派生类型关系是单向的,不可逆。

派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。

如果函数的参数是基类对象或基类对象的引用,函数调用时的实参可以时派生类对象。

派生类对象的地址可以赋值给基类指针变量。

即派生类对象对于基类对象兼容。

6、派生类中的构造函数和析构函数

如果基类中没有构造函数或者仅存在无参构造函数,则派生类中的构造函数的定义中可以省略掉对基类构造函数的调用。而当基类中的构造函数使用一个或多个参数时,派生类必须定义构造函数,提供将参数传递给基类构造函数的方法,从而实现对基类数据成员的初始化。

即先调用基类构造函数再调用派生类构造函数)

析构函数则相反,先调用派生类析构函数再调用基类析构函数)

7、隐藏基类函数

派生类中重新定义基类同名函数的方法,称为对基类函数的覆盖或改写,覆盖后基类同名函数再派生类中被隐藏,定义派生类对象调用该函数时,调用的是自身的函数,基类同名函数被隐藏,通过这个可以方便软件更新功能的实现。

8、多重继承

8.1、多重继承的数据排列规则

通过多继承,派生类会从多个基类中继承成员。在普通多继承方式下,若定义派生类对象,其中的数据成员排列规则是:首先按照派生类定义形式中基类的排列顺序,将基类成员依次排列,接下来再存放派生类中新增的数据成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base1
{
public:
void func_base1();
private:
int n_base1;
i nt m_base1;
}
class Base2
{
public:
void func_base2();
private:
int n_base2;
int m_base2;
}
class Derive:public Base1,public Base2//派生类定义形式
{
public:
void func_derive();
private:
int n_drive;

}

其中派生类对象中的数据成员排列如下:

1
2
3
4
5
Base1::n_base1;
Base1::m_base1;
Base2::n_base2;
Base2::m_base2;
Drive::n_drive;

8.2、多重继承派生类中的构造函数

如果派生类中新增的成员有类对象的话,那么派生类需要初始化的数据有三部分:继承的数据成员,新增类对象的数据成员和新增的普通数据成员。

那么多重继承中的构造函数调用顺序规则为:

首先调用基类构造函数,按它们再派生类定义中的先后顺序,依次调用各基类的构造函数。

其次调用对象成员的构造函数,按它们再派生类中定义的先后顺序依次调用。

最后调用派生类的构造函数

多重继承派生类、复杂派生类的析构函数只需要编写对于新增普通函数的善后处理,而对类对象和基类的善后处理工作是由类对象和基类的析构函数完成的,并且析构函数的调用顺序与构造函数完全相反。

8.3、多重继承引起的二义性

8.3.1调用不同基类中的同名成员函数时产生二义性

消除方法:

使用作用域限定符

派生类中定义与基类同名函数,将基类函数覆盖

8.3.2派生类中访问共有成员时产生二义性

多重继承中派生类由多个基类,多个基类又可能由同一个基类派生,则在派生类中访问公共基类时会出现二义性。

可能会对于一个基类成员出现多次拷贝。

消除方法:

将基类说明为虚基类。

1
2
3
4
5
6
7
class 派生类名:virtual 继承方式 基类名

{

......

};

对于普通基类来说,派生类构造函数值负责调用其直接基类的构造函数,若直接基类还要它更上层德基类,则依次调用各层基类的构造函数;而对于虚基类的派生类来说,其构造函数不仅调用其直接基类的构造函数,还需要调用其虚基类的构造函数。

即对于C++编译器来说,对于虚基类的构造函数,由最后定义的派生类,即类的层次结构中最低层的派生类在定义对象时完成虚基类构造函数的调用,该派生类其它基类对基类的构造函数的调用被忽略。

9、多态

多态是指不同的对象接收到相同的操作指令时,产生不同的动作。即每个对象以自己的方式是响应相同的消息。

多态分为静态多态和动态多态。

9.1、静态多态(静态联编)

不同对象直接调用同名函数,在编译时确定调用。通过重载机制实现。

9.2、动态多态(动态联编)

基类指针者引用指向派生类对象调用同名函数,在运行时确定调用。通过继承和虚函数实现。

9.3、虚函数

虚函数是运行时多态,若某个基类函数声明为虚函数,则其共有派生类将定义与其基类虚函数原型相同的函数,这时,当使用基类指针或基类引用操作派生类对象时,系统会自动用派生类中同名函数代替基类函数。(派生类函数覆盖掉基类同名函数)

虚函数只能时类内的函数,但不可以时静态成员函数。

派生类对基类虚函数重新定义时,必须与基类中虚函数的原型完全一致,包括返回值类型、函数名、参数个数、参数类型以及参数顺序。而派生类中同名函数无论是否添加virtual关键字,都被视为虚函数。(但是一般保留virtual 关键字,以提高程序的可读性)

9.3.1、一般虚函数成员

虚函数在编译器执行过程中会为包含虚函数的类建立一张虚函数表vtable,编译器按照虚函数的声明顺序依次保持虚函数地址,同时在每个带有虚函数的类中都放一个vptr指针,用来指向虚函数表,通常在定义对象时为vptr分配空间,该指针被分配旨在对象的起始位置,从而通过对象的构造函数将vptr初始化为本类的虚函数表地址。(虚函数按照其声明顺序列于表中并且基类的虚函数在派生类虚函数之前)

即带有虚函数时,C++编译器会按以下操作进行:

·为每个类建立虚函数表,若无虚函数则不操作;

·暂不连接虚函数,只是将各个虚函数地址放入虚函数表;

·连接各静态函数

9.3.2虚析构函数

C++中不能声明虚构造函数,因为在构造函数执行时,对象还没有构造好,无法按虚函数方式进行调用。

虚析构函数是为了解决基类的指针指向派生类对象,并用基类指针销毁派生类对象的应用产生的。

由于继承基类时不会继承基类的构造函数和析构函数,则会出现派生类申请的资源得不到回收的问题。

例如:

如果是虚析构函数, 调用时,会根据对象类型动态决定调用的函数。
如果,Base()是虚函数, a.Base()时,对象已确定,可以调用相应的析构函数。
对于构造函数,对象类型还没有决定,无法确定要调用的函数。
所以,虚构造函数是没有意义的。
比如,Base a = new Base();
如果Base()是虚函数,它无法确定要调那个函数。

使用基类指针指向一个new生成的派生类对象,通过delete销毁基类指针指向的派生类对象时,会出现以下两种情况:

1、如果基类析构函数不是虚析构函数,则只会调用基类的析构函数,派生类的析构函数不会被调用,导致派生类种申请的资源不被回收。

2、如果基类析构函数为虚析构函数,则释放基类指针指向的对象时会调用基类及派生类析构函数,派生类对象所有资源被回收。

9.3.3、纯虚函数

在定义一个表示抽象概念的基类时,有时无法或者不需要给出某些成员函数的具体实现,函数的实现在派生类种完成,基类中这样的函数声明为纯虚函数。(纯虚函数作为一个接口,具体实现在各派生类中完成)

1
virtual 函数返回类型 函数名(参数表) = 0

纯虚函数只有函数名而不具备函数功能,不可以被调用,声明格式后面的“=0”并不是表示函数的返回值为0,而是以这种形式说明此函数为纯虚函数吗,“=0”后以分号结尾,表示声明结束。

若在一个类中声明了纯虚函数,但是在其派生类中没有实现该函数,则该函数在派生类中仍然为纯虚函数。

10、抽象类与内部类

抽象类

如果一个类中至少包含一个纯虚函数,则该类称为抽象类。

抽象类的主要作用是通过它为一个类群建立一个公共的接口,使它们能够更加有效地发挥多态特性。抽象类负责声明公共接口(纯虚函数),而接口的完整实现,即纯虚函数的函数体,由派生类自己定义。

注意:

1、抽象类只能作为基类来派生新类,不能声明抽象类对象,但可以声明抽象类指针或引用,通过指针或引用操作派生类对象。

2、抽象类中可以由多个纯虚函数,在派生类中应该实现这些纯虚函数,使得派生类不再是抽象类。派生类中如果没有实现所以纯虚函数,则未重新定义的函数仍然未纯虚函数,派生类也是一个抽象类。

内部类

在一个类的内部定义的类称为内部类,内部类所在的类称为外部类。

内部类可以作为外部类的基础,外部类在内部类基础上扩充新的功能并且不会相互影响。

1
2
3
4
5
6
7
8
9
class 外部类名
{
外部类成员;
访问限定符:
class 内部类名
{
内部类成员;
};
};

一个内部类对象可以访问创建它的外部类对象的内容,包括私有成员。要实现这个功能,需要在内部类对象那定义一个指向外部类对象的引用。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Outer
{
public
class Inner
{
private
int inner_n;
public:
void set_outer_n(Outer & outer){outer.outer_n =10;}
};
//Inner inner_obj;
private:
int outer_n;
}

定义内部类对象

1、类外定义(通过作用域限定符)

由于内部类是外部类公有的,所有可以采用类外定义的方式。

Outer::Inner inner_obj;

2、类内定义

如上图的注释所示。

1
2
Outer outer_obj;
outer_obj.inner_obj.set_outer_n(outer_obj);