C++//面向对象程序设计

1. 定义基类和派生类

在这里插入图片描述

OOP(object-oriented programming)面向对象变成。
在这里插入图片描述
基类将其成员函数分为两类:一类是基类希望派生类进行覆盖的函数(虚函数),一类是基类希望派生类直接继承而不要改变的函数。

使用指针或引用调用虚函数时,该调用会被动态绑定,根据引用或指针所绑定的对象类型不同,该调用可能执行基类或者派生类的版本。任何非构造和非static的函数都可以是虚函数。

成员函数若未被声明为虚函数,其解析发生在编译时而不是运行时。

protected权限是指允许派生类访问而不允许其他用户访问。

1.1 定义派生类

派生类必须使用类派生列表指出他是从哪个基类继承而来的。每个基类前面可以有访问说明符。

派生类可以覆盖所继承的虚函数,也可以不覆盖直接继承。可在虚函数形参列表后,或者const关键字后,加上一个override显式注明覆盖了虚函数。

在这里插入图片描述
可以把基类的指针或引用绑定到派生类的基类部分,所以存在派生类到基类的隐式类型转换。
在这里插入图片描述
在这里插入图片描述
派生类需要用自己的构造函数初始化数据成员,初始化时,首先使用基类的构造函数初始化它的基类部分,然后按照声明顺序初始化派生类的成员
在这里插入图片描述
关于初始化,值得注意的一点是,每个类负责定义各自的接口,初始化自己的成员,因此派生类对象不能直接初始化基类成员,尽管从语法上是没问题的。

派生类可以访问基类的public和protected成员。

如果基类定义了静态成员,那么无论基类派生出多少派生类,静态成员都只有唯一的实例。对静态成员的访问和普通成员一样,都需要遵循访问控制规则。

派生类声明不需要派生列表。
在这里插入图片描述
如果试图使用某个类作为基类,该类需要被定义而不仅仅是被声明。

基类和派生类仅仅是一种派生关系,一个类可以是基类也可以同时是派生类:
在这里插入图片描述
Base是D1的基类(直接基类),D1是D2的基类。Base也是D1的基类(间接基类)。

如果某个类不希望被继承,可使用final关键字防止继承。
在这里插入图片描述

1.2 类型转换与继承

1.2.1 静态类型和动态类型

静态类型在编译时已知,变量声明的类型或者表达式生成的类型都是静态类型
动态类型是变量或表达式指向的内存的对象的类型,直到运行时才可知

类Bulk_quote继承自类Quote,net_price是虚函数,派生类需重写。
当用调用一个基类的成员函数时,
可用如下方式打印基类对象和派生类对象的费用:
在这里插入图片描述
上文提到的item的静态类型是Quote,而动态类型取决于item所绑定的对象类型。即使传递给item一个派生类Bulk_quote对象,也能通过引用类型转换转为基类完成参数传递。
如果表达式不是引用也不是指针,根据定义,其动态类型和静态类型是一样的。

能把指针从派生类转为基类是因为派生类含义基类的全部成员,转化后的基类指针可以指向基类部分。所以不存在从基类指针/引用转换为派生类指针/引用的隐式转换(强转可以)派生类自己的成员基类是没有的

即使基类指针指向一个派生类对象(这样就不会缺少成员),也不能把基类指针转为派生类。因为编译器只能通过检查静态类型判断是否合法,规定是基类类型不能转为派生类,所以即使这样做是完全没问题的,编译器也会报错。如果一定要转,可以用static_cast强制覆盖掉编译器的检查工作。

基类和派生类的对象是不能进行类型转换的。假设将派生类对象赋值/初始化给基类对象,企图完成类型转换:
1.对象间的赋值调用赋值运算符,赋值运算函数接收引用作为参数,所以派生类的引用会被转为基类引用,然后被赋值运算函数处理,然而这里的赋值运算函数是基类的成员,只会处理派生类中基类的部分
2.初始化过程调用构造函数。同样的,调用的是基类的构造函数,只会处理基类的成员
如果是基类赋值/初始化派生类,用的派生类版本的构造函数/赋值运算函数,那会直接找不到派生类的成员

在这里插入图片描述

2. 虚函数

必须为每个虚函数提供定义,无论是否被用到,因为编译器也无法确定哪些虚函数会被使用。

用哪个版本的虚函数取决于与指针/引用参数绑定的对象的类型。

动态绑定只有当用指针或者引用作为参数调用虚函数时才会发生。前面加粗的两个条件缺少任何一个,都会导致编译器能在编译期间确定函数版本,无法动态绑定。

当派生类覆盖某个虚函数时,可以使用关键字virtual再次指出该函数性质,但这并不是必须,虚函数在派生类中都是虚函数

派生类中的函数的形参和返回值必须和基类中该虚函数一样,如果不一样会被认为是不同函数,无法覆盖。有一个例外,当虚函数返回的是类本身的指针或者引用时(要求派生类对象指针到基类对象指针的转换是可访问的),上述规则无效。

为了防止试图覆盖虚函数但是由于参数或者返回值和基类的虚函数不同导致覆盖失败而不自知,可以使用override关键字,一旦使用override标记了某个函数而没有成功覆盖以往的虚函数,编译器会报错。如下:
在这里插入图片描述
如果把某个函数指定为final,任何试图覆盖该函数的操作都将引发错误
在这里插入图片描述

override在派生类中,final在基类中。

虚函数的默认实参:
在这里插入图片描述
使用作用域运算符可以禁止动态绑定,指定使用函数的版本:
在这里插入图片描述
在这里插入图片描述

3. 抽象基类

函数体位置书写=0即可将一个虚函数声明为纯虚函数。告诉用户本类中这个函数是没有意义的,但是为了它派生的类能用这个虚函数,所以将其声明为纯虚函数。
在这里插入图片描述
可以为纯虚函数提供定义,不过必须在类外定义。

含有纯虚函数的类称为抽象基类。不能创建抽象基类的对象。

派生出的类只初始化它的直接基类,然后依次往上初始化,分别初始化本对象中各自的成员

4. 访问控制和继承

类的访问分为两种:成员访问和用户访问。成员访问就是类的成员函数访问数据。用户访问就是在程序中通过类对象访问。

类似private,本类用户不能访问自己的protected成员。
类似public,派生类的成员和友元可访问protected成员。
一些约束:派生类的成员或友元只能通过派生类对象访问基类的protected成员,不能通过基类访问protected成员。如果可以通过基类访问protected成员,那只要把一个类声明为派生类的友元,protected的保护就形同虚设了。
在这里插入图片描述
派生类对基类成员的访问控制受到两个因素的影响,一是基类中该成员的访问说明符,二是派生类派生列表的访问说明符。

派生类成员对继承的基类成员访问只受第一条的影响。
派生类用户(包括派生类的派生类的成员,这就不是成员访问了)对继承的基类成员的访问受第一、二条影响:
如果派生列表的访问说明符是public,用户会遵循成员的原有访问说明符。如果是private,所有成员都是private的。如果是protected,则所有成员都是protected的。

派生访问说明符会影响派生类向基类的转换(三大层面:普通用户,成员和友元,二次派生的成员和友元):
在这里插入图片描述
友元关系不能传递,也不能继承。友元的派生类不能继承友元的访问能力。基类的友元在访问基类的派生类时不具有特殊性,派生类的友元在访问基类时也不具有特殊性(前面说的派生类的友元和成员可访问protected成员是指访问派生类继承下来的成员,而不是直接通过基类访问)

可使用using关键字改变某个成员的访问级别:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. 继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在基类的作用域内。当遇到一个名字时,逐层向外查找定义。

将派生类指针/引用转化为基类指针后,得到的基类指针无法访问派生类新增的成员

派生类可以重用定义在其基类(直接基类和间接基类)中的名字,此时内层作用域的名字将隐藏外层作用域的名字。如果一定要使用某一层的名字,可以使用作用域访问符指定从哪个类的作用域开始查找
在这里插入图片描述
名字查找过程:
1.首先确定使用该名字的类的静态类型。
2.在该类的静态类型对应的类里查找名字,如果找不到,在基类里找,如果还没找到,报错。
3.找到名字以后,进行类型检查,确认本次使用是否合法。
4.如果合法,根据是不是虚函数产生不同代码:如果是虚函数且用指针或者引用进行的调用,则在运行时根据对象的动态类型决定用哪个版本的虚函数;否则的话编译器产生一个常规函数调用。

内层的函数不会重载基类的成员。只会隐藏外层的成员(不管形参是否一致)。同样的,可以用作用域运算符指定要哪个类中的成员函数。
内层的虚函数只有当参数和返回值和基类一样时才会重载,否则会被视为非虚函数并隐藏外层函数。

示例:
在这里插入图片描述
在这里插入图片描述
前三个指针都是基类指针,在进行函数调用时,首先根据名字fcn从指针的静态类型Base类里查找名字,发现fcn是一个虚函数,且是用指针进行调用,所以要根据指针所指的具体对象决定用哪个版本的虚函数(不会使用重名的非虚函数)。
bp1就用base::fcn,D1没有重载fcn,所以用的基类的base::fcn,而D2重载了fcn,所以用的D2::fcn。

后三个:对bp2,静态类型为Base,从D1里找名字f2,没有,报错。
对d1p,静态类型为D1,找f2,发现f2是一个虚函数,且是用指针进行调用,所以要根据指针所指的具体对象决定用哪个版本的虚函数(不会使用重名的非虚函数)。d1p指向一个D1对象,所以就用D1::f2,对d2p,静态类型为D2,找f2,找到了,是个虚函数,同上述过程,最终用的是D2::f2版本的虚函数。

关键是理解名字查找过程

派生类要想使用基类的n个重载函数,要么把这n个全部覆盖一次,要么一个也不覆盖。因为一旦派生类覆盖了一个,基类的所有重载函数都被隐藏(同名)
有时希望覆盖一些重载函数,但是又不希望其他的被隐藏,这时就要把所有的重载函数都覆盖一边,有点麻烦,可以使用using语句,using语句指定一个名字而不指定形参列表,只要一条using就可以把该函数的所有重载版本加入到派生类作用域内,接下来只用覆盖想覆盖的即可,而不用担心被隐藏。

6. 构造函数与拷贝控制

6.1 虚析构函数

由于基类指针可以指向派生类,在调用析构函数时,需要调用能删除正确对象的指针。比如delete一个基类指针,以释放基类对象的方式释放内存,但是该基类指针指向的是一个派生类对象,那就会出问题。

将析构函数定义成虚函数可以确保执行正确版本的析构函数

前面说了:如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作。然而基类的析构函数无需遵循上述准则

如果一个类定义了析构函数,即使是用=default的形式使用了合成的虚函数版本,编译器也不会为这个类合成移动操作虚函数将阻止合成移动操作

6.2 合成拷贝控制与继承

删除的拷贝控制成员与基类的关系:
在这里插入图片描述

6.3 派生类的拷贝控制成员

在这里插入图片描述

析构函数只负责销毁派生类自己分配的资源,派生类的对象成员以及基类部分都是隐式销毁的。基类又会隐式销毁它的直接基类,直至继承链顶端。

销毁顺序与创建顺序正好相反:派生类的析构函数首先执行,然后是基类的析构函数,依次类推。创建时,先执行完基类的构造再完成派生类成员的构造

当执行基类的析构函数时,派生类成员已经全部销毁,当执行基类的构造函数时,派生类成员还处于未初始化状态。

6.3.1 派生类的拷贝/移动构造函数

和普通构造函数一样,派生类的拷贝构造函数和移动构造函数会在初始值列表调用基类的拷贝/移动构造函数完成基类部分的拷贝/移动
在这里插入图片描述
如果未提供基类初始值,即:
在这里插入图片描述
那么基类部分会调用默认构造函数完成初始化。
在这里插入图片描述

6.3.2 派生类的拷贝/移动赋值运算符

同拷贝和移动构造函数,派生类的赋值运算符也需要显式的为基类赋值:
在这里插入图片描述

6.3.3 派生类的析构函数

派生类的析构函数只需要销毁自己分配的资源即可。成员以及基类的销毁会隐式执行。
在这里插入图片描述
也就是基函数的构造/析构函数调用基函数版本的虚函数,派生类的构造/析构函数调用派生类的虚函数版本。
在这里插入图片描述

6.4 继承的构造函数

不能继承默认/拷贝/移动构造函数,如果派生类没有直接定义这些函数,编译器将为派生类合成。

派生类可重用直接基类定义的构造函数(可于本类构造函数初始值列表里调用)。

派生类继承基类构造函数的方式是提供一条注明直接基类名的using声明语句。如下:在这里插入图片描述
一般情况using只是让名字在当前作用域可见,而当using作用于构造函数时会真正令编译器为每个基类的构造函数在派生类中生成一个与之对应的构造函数。形如:
在这里插入图片描述
前面提到using可用于改变成员的访问级别,但using作用于构造函数不会改变该函数的访问级别。且explicitconstexpr属性也完全继承。

基类的构造函数含有默认实参时,这些实参并不会被继承,派生类会获得多个继承的构造函数,每个构造函数省略掉某一个含有默认实参的形参
在这里插入图片描述

大多数情况下,派生类会继承所有基类的构造函数。有两个例外:
1.派生类可以继承一部分构造函数,自定义一部分构造函数。如果自定义的构造函数和基类的某个构造函数具有相同的参数列表,则该构造函数不会继承,而是使用自定义的版本替换
2.默认、拷贝、移动构造函数不会被继承。

如果一个类只有继承的构造函数,编译器会为它合成一个默认构造函数。

7. 容器与继承

在这里插入图片描述
如果一定要把存在继承关系的对象放在一个容器内时,可以存放基类指针而不是直接存放对象。

静态联编(函数重载)和动态联编(虚函数)。虚函数的优势在于处理存在继承关系指针或者引用对象时更有灵活性,函数重载只能根据静态类型完成函数选定,而真正运行时的动态类型很可能和静态类型不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值