面向对象程序设计

数据抽象:将接口与实现分离。

继承:定义相似的关系并建模

动态绑定:忽略相似类型的区别,统一进行使用,可以使用基类指针或引用指向派生类对象

基类与派生类

基类一般要定义一个虚折构函数。

派生类的折构函数会先调用自己的函数体,再调用基类的折构

当我们希望进行动态绑定时,就应该将函数声明为虚函数,否则在编译时就会被解析。

派生类重写的时候可以通过override来显式表示重写(写在函数的最后,const后)。

派生类可以访问public和protected(只有自己和派生类能访问)的基类成员,而不能访问private的基类成员,但是可以通过基类的可访问成员函数类访问私有成员。

通过使用派生类列表来继承。格式:

class classname : 访问说明符 derivated_classname

访问说明符就是public,protected和private。

对于所有需要重写的成员都需要进行声明。

如果某个派生类对于基类的虚函数并没有进行重写,则会直接使用基类的函数。

虽然派生类继承了基类中的一部分成员,但是每个类自身控制它对应的初始化过程。在构造函数中原本是使用构造函数初始值列表来初始化,但其中属于基类的成员可以直接使用基类的构造函数来初始化。方式与其他构造函数初始值列表一致,如classname(param)。

而其调用的基类构造函数由编译器决定。当初始化时,基类部分先被初始化,然后再是派生类的部分。

在作用域方面,在使用派生类作用域的时候其外层就已经套了一层隐式的基类作用域,因此可以直接使用基类成员。

需要注意的是基类的成员虽然可以被直接调用并赋值,但是使用的时候最好都调用基类的函数类对基类的成员进行控制与赋值,而不要直接在派生类中对基类进行控制。

基类的静态成员即便被继承了也是通用的,但仍然遵循访问说明符的继承规则。

派生类在声明时不需要声明继承。

通过在类名后加final关键字可以防止继承的发生,也可以用在成员函数上。

派生类可以作为基类继续继承,派生类的基类是派生类的派生类的间接基类,而基类是这个派生类的直接基类。

类型转换

派生类对象是由多个对象组成的,派生类独有的成为组成一个对象,其继承的每一个基类对象的成员各自组成一个基类对象,这些存储是对象内的成员存储连续,而对象之间则不一定。

正是由于派生类中存在基类对象,因此可以将其作为基类来使用,而且,其成员一定是大于等于基类对象成员的,这很显然。也就是派生类可以向基类进行类型转换(会隐式发生),只能转换四种:指针到指针,引用到引用,对象到引用,对象到指针。

智能指针也存在这一转换。

而当我们这么做的时候,我们并不在乎对象是派生类还是基类。

而此时,这一类型是动态类型,有且仅有基类指针和引用可能是动态类型,而其他都是静态类型。

但不存在从基类向派生类的转换,或者说不存在隐式转换,可能存在显式的,因为可能访问不存在的成员,需要注意的是,即便一个基类指针指向了派生类,也不能将其赋值给一个派生类指针,因为编译器并不知道这件事,除非使用static_cast。

但上述的都是基类的指针与引用,而对象之间实际本身是不存在转换的。除非像普通的转换一样定义构造函数或赋值运算符,而其原始的合成版本会像普通的类型转换一样一一赋值并会把派生类多余的部分切掉。

在类型转换中可能存在访问权限的问题。

虚函数

由于虚函数是动态绑定的,因此并不知道会不会使用基类的虚函数本身,即便我们知道其并不会被使用,但仍然要对它进行定义。

当且仅当对有派生类的指针或引用调用虚函数的时候会进行动态绑定,而且也只有在调用的时候会发生。

虚函数在派生类中也将被隐式声明为虚函数。并且其形参表,返回类型都必须与原虚函数一致。除了当虚函数返回的是类本身的指针或引用,假设基类返回自己的指针,而派生类可以返回派生类自己的指针,不过这只有在派生类到基类的类型转换是可行的时候可用。

由于当形参表不同时会认为不是继承,因此可以通过override说明符来显式的表示这是重写来让编译器来检查这个函数是否重写了某个函数,而不发生前面的问题。

当虚函数中存在默认实参时,其调用时默认实参的值会依据静态类型(也就是基类),而非派生类的默认实参值。

这一动态绑定可以被指定为静态,在调用运算符后,函数之前加上基类的作用域或其他作用域来指定调用的虚函数版本。常常用于派生类的成员函数中。若在派生类版本的虚函数中需要调用基类版本的虚函数,那么若是不指定,则会调用自身版本,造成无限循环。

派生类是否能重写虚函数与虚函数访问权限无关。

函数模板不能被声明为 virtual

抽象基类

有时我们完全不希望用户使用基类或基类中的某个成员函数,这个基类是抽象的,用来方便派生类的实现的,是抽象基类。

此时我们可以将这个成员函数定义为纯虚函数。定义的方式是在它最后增加=0。

虽然我们可以对纯虚函数提供定义,但必须在类外。

只要一个基类含有纯虚函数,那么这个基类是抽象基类。所有的抽象基类都是不能实例对象化的。

一般用于规范接口。

一般用于实现接口类,当一个接口类的所有函数都是纯虚函数,那么这个类是纯接口类。

重构:重新设计类的体系以便将操作和数据从一个类移动到另一个类中。

访问控制与继承

protected:基类的protected成员对于派生类与友元时可访问的。但是只能通过派生类对象来访问,也就是访问派生类对象继承的protected成员,而其他普通的与其没有任何关系的基类对象的protected成员是不可访问的。

对类声明的访问说明符并不影响派生类对于基类的访问权限,这些由基类自身的访问说明符决定,其决定的是继承的部分在派生类中的访问权限。

对于对类定义的访问说明符,会使得基类中大于等于它的权限缩小为其权限用于本类中。

派生类向基类转换的可访问性

对用户来说:当且仅当派生类是公有继承的时候可行

对派生类的派生类的成员函数与友元来说:当且仅当是公有继承或者受保护继承的时候是可行的(因为如果是私有的,那么派生类与基类对于派生类的派生类就是私有的不可访问的了?)

对派生类的成员函数与友元来说:是一直可行的

友元是不能被继承的,但是,能够访问派生类的基类部分(如果基类声明了友元)。而友元的基类或派生类并不会因为这种关系而具有对其他类的访问权限。

在派生类由类名后的访问说明符来确定访问权限后,可以通过using bassclassname::成员名 来调整其对外的访问权限。而这一访问权限由该语句处于哪一个访问运算符下来决定。但只能改变其能访问的成员的对外权限。

class的默认继承是private继承,struct的默认继承则是public继承。

作用域

由于派生类的作用域包在基类的作用域之内,因此,名字查找的时候是由内而外的。派生类的同名会隐藏基类的同名(即便类型和形参可能是不同的,若都相同,就不是隐藏而是虚函数了),但派生类除了虚函数重写外不要使用基类中使用过的名字。

在查找对应对象(成员)的过程中,先寻找名称再进行类型检查。从内到外寻找相同名称的对象,找到后进行类型检查,若成功则根据是不是虚函数来选择调用的函数。

虚函数与普通函数的最根本的区别就是当使用指针指向一个可能非指针类型的对象的时候,虚函数会根据指向的对象而选择函数(实际上是在就相当于先找到名字相同的普通函数,然后只不过多了一步虚函数的重新选择函数),而普通函数只会根据指针的静态类型而选择函数。

?可以通过using来一次性覆盖一个函数的所有版本,因为using是通过名称来匹配的,而不管类型与参数。(p551)

虚构折构函数

由于基类的指针可能指向派生类对象,而delete指针会调用其指向对象的折构函数,我们希望去调用派生类的折构函数,因此将基类的折构函数定义为虚函数。

虚折构函数会阻止合成移动操作。

另外,当且仅当基类的折构函数被声明为虚的了,那么派生类的折构函数才会去隐式调用基类的虚构函数。

合成的拷贝控制

派生类的合成的拷贝控制函数会先调用基类的拷贝控制函数来处理其自身中的基类部分,后再调用自己的函数体。合成的拷贝控制函数能成立只需要其基类的对应的可以被访问。

而合成的析构函数会先删除派生类自身的成员,再删除其直接基类的成员,而这会调用直接基类的折构函数。

若基类中的折构函数无法访问或者对应的拷贝控制函数无法访问,则合成的对应的拷贝控制函数会被删除(折构对应全部)。

基类没有合成移动操作会使得派生类也不合成移动操作,因此基类中最好声明移动操作,以使得派生类中会自动合成移动操作(因为派生类中的折构函数不是虚的)。

派生类的合成拷贝

折构函数会隐式销毁包括基类的所有成员,因此折构函数只用处理自己派生类独有的部分即可。

而拷贝控制函数需要显式的去拷贝控制基类部分。派生类的移动或拷贝构造函数在初始化列表中调用基类对应的函数来初始化其基类部分,因为在传入参数的过程中会自动将派生类独有的部分被切掉了。

派生类的移动或拷贝赋值运算符中,调用基类对应的函数来赋值其基类部分。必须通过作用域符显式指定是基类的函数base::operator=(variable)base::operator=(variable),实际上可以看做是this.base::operator=,接着再自行处理自己的部分。

没懂。

继承构造函数

using声明语句只是令某个名字在当前作用域可见。被using的函数往下的继承也会因为被声明时所在的访问声明符的不同而产生不同的往下继承权限。只能using能访问的函数来调整它的权限(到底能不能using private的基类函数)。就好像这个函数是在这里声明的一样。

构造函数只会继承普通构造函数(非默认,拷贝与赋值)。继承的构造函数形如:

derived(params) : base(args) {}derived(params)\ :\ base(args)\ \{\}

这种继承方式只对构造函数有用。

相当于在初始化列表中只调用了基类的构造函数,其他部分被默认初始化。

但构造函数的using 并不会改变访问级别。using也不能改变函数的属性。

当构造函数中含有默认实参的时候,派生类会自动生成实参数量+1个构造函数,每一个构造函数比上一个构造函数少一个默认实参。

派生类中定义的构造函数会覆盖继承的同名构造函数。

通过using基类的构造函数,其名字就会自动在派生类中变为派生类的名字,相当于直接使用基类的构造函数作为自己的构造函数。

存储

当我们想用容器来存储多种类型(属于同一基类的底下时(可以使用间接基类的指针指向派生类的对象)),最好使用基类的指针来间接存储。

在使用基础类的时候要加上std::。


面向对象程序设计
https://lhish.github.io/project/hide/面向对象程序设计/
作者
lhy
发布于
2024年6月30日
许可协议