【C++研发面试笔记】2. 多态性
2.1 多态性来源
多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。
C++支持两种多态性:编译时多态性,运行时多态性。
a. 编译时多态性:通过函数重载、重写、模板来实现。
b. 运行时多态性:通过虚函数继承实现。对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
重载、重写与屏蔽的区别:
重载:在相同作用域内,函数名称相同,参数或常量性不同的相关函数称为重载。
重写:派生类重写基类中同名同参数同返回值的函数(通常是虚函数,这是推荐的做法)。
屏蔽:一个内部作用域(派生类,嵌套类或名字空间)内提供一个同名但不同参数或不同常量性的函数,使得外围作用域的同名函数在内部作用域不可见,编译器在进行名字查找时将在内部作用域找到该名字从而停止去外围作用域查找,因而屏蔽外围作用域的同名函数。尽量不要屏蔽外围作用域(包括继承而来的)名字。屏蔽所带来的隐晦难以理解。
2.2 继承、接口与组合
2.2.1 派生类的3种继承方式
(1)公有继承方式:基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里保护成员与私有成员相同。
(2)私有继承方式:基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见,派生类不可访问基类中的私有成员。尽量避免 private 继承,因为从基类继承而来的所有接口均为私有的,外部不可访问。
(3)保护继承方式:这种继承方式与私有继承方式的情况相同,两者的区别仅在于对派生类的成员而言,基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
(4)虚拟继承是多重继承中特有的概念。虚拟基类是为了解决多重继承而出现的,如类D继承自类B和类C,而类B和类C都继承自类A,此时类D中会继承两次A。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A成了虚拟基类,此时只需要继承一次。
2.2.2 继承的实质
(1)覆盖
构造函数从最初始的基类开始构造的,所以对于某个子类具体实现,其也会默认构造全体父系类。派生类的内存大小=派生类本身数据变量+父类大小+虚函数表指针+指向父类指针。各个父类的同名变量不会形成覆盖,都是单独的变量。 覆盖函数的就近调用,如果父类存在相关接口则优先调用父类接口(此时操作的是父类实例),如果父类也不存在相关接口则调用祖父辈接口。 析构函数也是从子类开始向上析构的。假设有如下情况,带非虚析构函数的基类指针 pb 指向一个派生类对象d,而派生类在其析构函数中释放了一些资源,如果我们 delete pb;那么派生类对象的析构函数就不会被调用,从而导致资源泄漏发生。因此,应该声明基类的析构函数为虚函数。
(2)重写
在继承中有三类函数:纯虚函数、虚函数、非虚函数。
非虚函数:当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。实际上,非虚成员函数表明了一种特殊性上的不变性,因为它表示的是不会改变的行为。所以,声明非虚函数的目的在于,使派生类强制继承函数的接口和实现。 纯虚函数:声明纯虚函数的目的是让派生类来继承函数接口而不是实现,而实现交由派生类来完成,派生类也必须重写该接口(如果要实例的话)。 虚函数:让派生类来继承函数接口和实现,而派生类可以重写该接口,但也可以调用基类的默认实现。
2.2.3 继承与组合间的区别:
面向对象编程讲究的是代码复用,继承和组合都是代码复用的有效方法。组合是将其他类的对象作为成员使用,继承是子类可以使用父类的成员方法。
继承在继承结构中,父类的内部细节对于子类是可见的。简单易用,使用语法关键字即可轻易实现。易于修改或扩展那些父类被子类复用的实现。编译阶段静态决定了层次结构,不能在运行期间进行改变。破坏了封装性,由于“白盒”复用,父类的内部细节对于子类而言通常是可见的。子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性。当父类的实现更改时,子类也不得不会随之更改。
组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是“黑盒式代码复用”。通过获取指向其它的具有相同类型的对象引用,可以在运行期间动态地定义(对象的)组合。不破坏封装,整体类与局部类之间松耦合,彼此相对独立。
继承体现的是一种专门化的概念而组合则是一种组装的概念。除非用到向上转型,不然优先考虑组合。
2.2.4 继承与接口的区别:
首先来说接口也是一种继承方式,所谓接口实际上指的只有声明,对应的是只有纯虚函数的抽象类,在C++中并没有关于接口的关键字(这点同Java是不一样的)。
2.3 模板与多态
模板(泛型)是一种对类型进行参数化的工具,通常有两种形式:函数模板和类模板。函数模板针对仅参数类型不同的函数。类模板针对仅数据成员和成员函数类型不同的类。使用模板的目的就是能够让程序员编写与类型无关的代码。
注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
2.3.1函数模板通式
// 模板函数定义
template < class 形参名 ... > 返回类型 函数名(参数列表)
{函数体}
// 比如定义交换函数
template
void swap(T& a, T& b){};
其中template和class是关键字,class可以用typename关键字代替,在这里typename 和class没区别,<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。
2.3.2类模板通式
template < class 形参名... > class 类名
{ ... };
类模板和函数模板都是以template开始后接模板形参列表组成,模板形参不能为空,一但声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。比如
template < class T> class A{
public:
T a; T b;
T hy(T c, T &d);
};
在类A中声明了两个类型为T的成员变量a和b,还声明了一个返回类型为T带两个参数类型为T的函数hy。
类模板对象的创建:比如一个模板类A,则使用类模板创建对象的方法为A
m;在类A后面跟上一个<>尖括号并在里面填上相应的类型,这样的话类A中凡是用到模板形参的地方都会被int 所代替。当类模板有两个模板形参时创建对象的方法为
A
m;类型之间用逗号隔开。
在类模板外部定义成员函数的方法为:
template<模板形参列表> 返回类型 类名<模板形参名>::函数名(参数列表){函数体}; template < class T1,class T2> void A< T1,T2 >::h(){}
2.3.3 非类型形参
1、非类型模板形参: