1. 封装
(1)意义:
- 将属性(成员变量)和行为(成员函数)作为一个整体。
- 对属性和行为加以权限控制。
(2) struct和class的区别(面试题!!!):
-
在C++中两者的唯一区别就是默认的访问权限不同。
-
struct默认权限为共有。
-
class默认权限为私有。
拓展: 在C中,struct是用户自定义数据类型,是一些变量的集合体。没有权限设置,不能包含函数;在C++中,struct是抽象数据类型,能给用户提供接口,能定义成员函数,能继承也能实现多态,有权限设置。
2. 继承
(1)概念
-
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称子类或者派生类,被继承的类称为父类或基类。使用继承是为了减少代码的冗余,增强代码的可扩展性。
-
继承的特性:
传递性:子类可以调用父类以及父类以上所有父类的属性和方法。 -
继承与派生其实是一回事,只是站在不同的角度。
(2)不同继承方式及类成员的访问控制
-
a.公有继承(public)时:
保护(protected)成员被公有继承时:
也就是说,从纵向(基类和派生类)看,基类的protected的成员对于派生类来说是相当于public的;从横向(类本身)看,protected的成员对于类对象来说是private。
-
b.私有继承(private)时:
-
c.保护继承(protected)时:
总结:
- 私有继承的适用场景: 当我想把基类的成员继承过来在类内部当工具用而不把继承过来的这些方法对外提供时,就可以采用私有继承。
- 保护继承的适用场景: 保护继承对于本派生类来说,就相当于上述的私有继承一样,因为对于本类对象来说,protected的性质跟private差不多。但保护继承对于本派生类以后的子子孙孙来说,它们却可以使用基类的方法,因为基类的公有和保护成员对于子子孙孙来说与public的性质相同。
(3)向上转型
简单地说,就是基类有的东西,派生类都有,所以派生类可以当作基类用;但反过来就不行,因为派生类有的基类有些会没有,所以基类不能当作派生类用。
还有一点需要注意:基类的指针可以指向各种派生类对象。
一个错误示范:
这里并不能实现派生类的地址用来初始化基类的指针。因为fun函数是在编译链接时静态绑定的,就是当上面程序在运行之前,在编译链接时,就要确定display()的代码是哪个(要调用哪个),所以display在运行之前并不知道在代码执行时,它会被调用多少次,也不知道每个对象调用时,参数会是哪种对象。所以fun函数里的display函数既然在编译链接时就要确定调用哪个类中的display,就只能依靠形参是什么类型了,形参是Base1类型,所以就只会调用Base1中的display函数。
所以绝对不要重新定义继承而来的非虚函数!!! 后续多态会解决这个问题。即把每个display函数设置为虚函数,也就是让编译器在编译到这个函数先让它不要确定函数体是哪个,而且等到运行时再确定。
(4)继承时的构造函数
使用using就相对于把基类的构造函数当成本类的构造函数。
-
a.单继承时构造函数的定义
上面无论是先写基类名,还是先写本类成员初始化列表,都会先调用基类的构造函数。 -
b.多继承时构造函数的定义
当派生类继承多个基类且在构造函数中没有列全基类的构造函数时,编译器就会自动调用那些没有类出来的基类的默认构造函数。当没有基类没有默认构造函数时就会报错。 -
c.小结:
例子:
-
复制构造函数
(5)析构函数
一般构造函数的调用次序是先基类再派生类,所以析构时就是先派生类再基类。
如果一个类中的成员中有对象,且有继承,则先析构本身类中的对象,再析构基类,而且析构的顺序是与继承的顺序相反的。
(6)虚继承
基类成员的访问
二义性问题 ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8479abd4530501813d54c2013d7d8b14.png)
反例:
如何解决上述数据冗余和数据不一致的问题:
使用虚基类
虚基类的语法和用途
虚基类及其派生类构造函数
注意: 虚基类并不是完美的解决方案,因为你每次都需要给最远虚基类去传递参数,要是没传会导致语法错误。所以这将是一个负担。所以虚基类的适用范围是,在一个可控的范围中去使用。
(7)拓展
-
在讲完继承之后说一下组合。
组合是一个类包含另一个对象。继承是一种is-a的关系。也就是说每个子类对象都有一个基类对象;
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象(一个对象里套了另外一个对象) -
注意
当组合和继承都适用的时候,首选是组合。因为继承的情况下,父类和子类的耦合性太高,当父类需要修改时,有可能会导致子类也需要修改。
3. 多态
(1)先解决2.(3)的问题
把display设置为虚函数,就是动态链接,等到传入具体对象对于的类,再调用对应的display函数。从上面的代码看,我们并没有把display函数写出内联函数(inline),而在2.(3)中的代码里的display函数是写出内联函数的,直接把函数体写在类体里的。但本例把函数体都拿到了类外,因为我们写的是虚函数,就是要在调用fun()的时候,再去考虑要调用哪个display(),就是要编译器不要太早确定要调用哪个,但要是内联函数的话,编译器就会在编译链接的时候处理好,内联函数就是编译器会在编译链接的时候把代码嵌入到调用点上,执行时就不会再发生控制转移了。
所以虚函数都不能是内联函数!!!
(2)虚函数的概念
为什么说虚函数必须是非静态的成员函数呢?
因为静态函数是关联到整个类的,不是关系到哪个对象的。只有非静态函数才是属于对象的,所以虚函数必须是非静态的成员函数。
(3)虚表与动态绑定
只要类中有一个是虚函数,这个类就是多态类。
虚表是放在对象里的,所以要找到虚表得先找到对象。
这样子就实现了动态绑定。
(4)virtual关键字
覆盖跟隐藏还不一样,覆盖是覆盖那些函数原型一样的,隐藏是只要名字相同就隐藏。
(5)哪些成员函数可以是虚函数
在程序中如果要使用多态性的话,就应该把析构函数写成虚函数。
(6)一般虚函数成员
(7)虚析构函数
这种情况,基类指针只会析构基类对象,并不会调用派生类的析构函数。
只有在Base和Derived的析构函数前加上virtual后,才能先析构Derived再析构Base。
(8)抽象类
(9)纯虚函数
要记得在后面写“=0”,并不给出函数体实现。
如果继承抽象类的类也不去实现纯虚函数的话,那么相对于这个类继承了纯虚函数,也是抽象类。
抽象类可以有其他成员,只要有一个是纯虚函数的话,这个类就是抽象类。
(10)override和final
虽然它们不是关键字,但我们还是把它们当成关键字比较安全。
-
override
反例:因为漏写了Derived中f1函数中的const,没有保持函数签名完全一致,导致的错误。
这种有时查半天还查不出来。
形参类型不一致编译器也会报错。 -
final
如果一个类很重要,不希望被继承,只允许被直接使用,就可以定义成final。或者如果一个函数实现了算法,你不希望被优化,也可以定义为final。
(11)运算符重载
-
运算符重载的意义
-
运算符重载的规定
因为运算符是用于对象之间的计算,所以只能是非静态成员函数。
而进行对象之间的运算,需要频繁访问到私有成员,如果设置为友元,就比较方便。 -
重载为类成员的运算符函数定义形式
-
重载前置++和后置++
注意: 重载运算符,并不能重载整型运算这些原有的,运算对象必须有一个是自定义对象。
常引用:传引用是为了提高效率,因为传的是对象,如果不传引用会发生对象的赋值拷贝,消耗资源,如果是传引用则不会;为了避免对对象发生修改,就在参数前加了const。
总结: