面向对象编程:继承和多态

class Shape{
	public: 
	int x; 
	int y; 
	double are(){} 
	double len(){} 
};
class Circle:public Shape{
};

继承的概念:用一类去继承另外一个类,被继承的类被称为父类或基类,继承父类的类被称为子类或派生类
通过继承之后子类会拥有父类的全部属性和函数。
继承是为了代码的复用和功能的扩展
继承的语法: class Son:继承方式 Father,继承方式 Father…{};
继承方式:默认是私有继承。
通过private继承,父类中所有的属性和方法都将成为私有的,
通过protected继承,父类中public和protected的属性和方法都将成为protected的,
通过public,父类中public的属性依然是public,protected依然是protected。
通过继承,子类拥有父类的全部属性和函数。虽然父类中的私有属性在子类中无法访问,但子类依然拥有
继承方式 父类中:public , protected , private
public 子类中:public , protected , — (private)
protected : protected , protected , —
private : 都是private

公有继承public 子类中父类的public和protected可以访问,子类的子类中也是public和protected可以访问,在类外public可以访问
保护继承protected 子类中父类的public和protected可以访问,子类的子类中也是public和protected可以访问,在类外都不可以访问
防止子类对于父类公共借口的扩散。
私有继承private 子类中可以访问父类的public和protected,子类的子类中都不能访问,在类外都不能访问

C++支持多继承,一个类同时继承多个父类。
C++支持多重继承。class A{}; class B:public A{}; class C:public B{};

子类中的构造函数
1、子类中默认的构造函数会根据继承的顺序依次调用父类的无参构造函数。如果父类中没有无参的构造函数将报错
父类子对象:每一个子类对象中都有一个父类子对象 父类的属性在子类中的呈现
2、父类中没有无参构造函数,应该在子类的构造函数的初始化列表中显式调用父类的有参构造函数 public: S(int x,int y,int z):F(x,y),z(z){}
如果父类没有提供无参构造方法,那么子类必须实现构造方法,并在初始化列表中显式调用父类中指定的有参构造函数。
3、在实现子类的构造函数过程中,如果没有显式调用父类中的构造函数,也会默认调用父类的无参构造函数。
4、子类构造函数先调用哪个父类构造函数取决于继承顺序,与初始化列表中的顺序无关。
构造函数 -> 按照继承顺序调用父类的构造函数 -> 按照属性声明顺序依次调用构造函数 -> 构造函数体 class S:public F1,F2{B b; A a;};//S s; F1 F2 B A S
析构函数调用顺序正好相反。 继承与继承顺序有关,成员与声明顺序有关。 public:(int x):F(x),a(1,1){} //父类初始化和成员初始化
哪些必须在初始化列表:常属性和引用属性、调用类属性的有参构造、调用父类有参构造

子类中的析构函数会自动调用父类的析构函数,不需要手动调用父类的析构函数

子类对象是父类类型的对象。子类对象可以转换为父类类型的对象,经过转换之后不能访问子类中的属性和函数。 F f=s;//f是父类子对象
转换之后只能访问父类的属性和函数。
父类类型不能转换为子类类型的对象。

子类的属性名称和父类的属性名称相同。 show(){x;//子类自己的 F::x;//父类的}
子类的属性会隐藏的父类的同名属性,但是可以通过 父类名::属性名 来指定访问父类中的属性
如果是通过对象 对象.父类名::属性名
void foo(int x){ x;//局部的 this->x;//子类的 F::x;//父类的 }
子类会隐藏父类中同名的函数。 如果需要调用父类的函数,需要 父类名::函数名();
如果需要访问父类的标识符,需要 对象.父类名::标识符
子类对象直接访问和调用函数时,默认是访问子类中的属性和函数。 如果子类中没有同名的标识符,将去父类中搜索。如果父类中也没有将报错。
如果子类中有同名的标识符,则会根据类型去校验,如果不一致(排除隐式转换)则将报错,不会去父类里校验。

子类的拷贝构造和拷贝赋值函数
默认的拷贝构造和拷贝赋值都会调用父类的拷贝构造和拷贝赋值函数。
构造:默认调用父类的无参构造。 自己实现构造:默认调用无参构造,除非在构造列表中指定。
拷贝构造:如果子类有需要实现拷贝构造函数,那么实现时需要注意拷贝构造父类子对象。
需要在子类的拷贝构造的初始化列表中显示调用父类的拷贝构造函数。 S(const S& s):F(s){//拷贝构造}
拷贝赋值:如果子类有需要实现拷贝赋值函数,那么实现时需要注意拷贝赋值父类子对象。需要在子类的拷贝构造函数中显示调用父类的拷贝赋值函数。
S& operato=(const S& s){ F::operator(s); //拷贝赋值} 或者 { static_cast<F*>(this)->operator=(s); //拷贝赋值}
S(const S& s):F(x){y=s.y;}//显示调用父类的拷贝构造和拷贝赋值函数。

F f=S();//没有类型转换,只是调用拷贝函数或拷贝赋值 F *f=&s; F &f=s;//没有调用任何函数,只是类型转换

父类指针指向子类对象时析构问题
父类指针指向new出来的子类对象,在用delete释放资源时子类的析构函数将不会被调用,可能造成内存泄漏。 F *ps=new S();

delete static_cast<S*>(ps);//如果直接delete ps将可能造成内存泄漏。
父类* p子类=new 子类(); delete p;只会调用父类的构造函数,造成内存泄漏。 delete static_cast<子类*>§;//调用子类的析构,子类析构自动调用父类的析构

多重继承
class A{}; class B:public A{}; class C:public B{};

钻石继承 ◇
一个类沿着不同的继承方向会有共同的基类(父类)。

 class A{
 }; 
 class B:public A{
 }; 
 class C:public A{
 }; 
 class D:public B,C{
 };

在访问基类A中成员变量时将产生歧义。因为按照不同的继承线路在最终的子类中将保留两份。
公共基类的属性按照不同的继承线路在最终的子类中将生成多份。如果直接访问将导致歧义。对存储而言也造成浪费。
如果需要避免最终问题这可以用虚继承。

虚继承 只在钻石继承中才有作用,否则不起作用。

class A{
};   
class B:virtual public A{
};  
class C:virtual public A{
};  
class D:public B,public C{//访问公共属性时不会有歧义
}; 

子类和父类
父类类型 变量=变量; //变量记录的是父类子对象 F f=S();//
父类类型* 变量=&变量; //隐式类型转换 F *f=new s();//只能调用父类的属性和函数

虚函数
用virtual关键字声明的 成员函数 称为虚函数。
子类可以重写该函数。 重写一定要是虚函数。 一个虚函数可以被继承,继承后依然是虚函数。

多态
条件:虚函数 + 指针/引用 如果子类提供基类虚函数的覆盖的版本(子类重写父类中的虚函数)
子类重写父类中的虚函数,然后用 父类类型的指针指向子类对象 或者 父类类型的引用引用子类对象 ,调用重写的函数时,不是根据指针或引用的类型来确定调用的函数,而是通过指针所指向对象的类型 和 引用所引用的对象类型来确定调用的函数。
基类类型指针指向子类对象 或者 基类引用引用子类对象,调用函数时调用的是重写的函数,叫做多态

重写、覆盖override (没有virtual时叫隐藏)
子类重写父类中同名的函数
条件:(1)父类中的函数必须是虚函数。(2)重写函数的函数名、参数列表必须相同。参数不同子类会隐藏父类中同名的方法。
(3)对于基本数据类型 或 者类类型,重写函数的返回值必须一致。如果返回值类型为类类型的指针或者引用类型时,子类重写函数的返回值可以是父类函数返回值类型的子类类型。
注意:子类函数的函数名和参数列表和父类中虚函数一致,但参数(基本数据类型或类类型)类型不一致,直接报错
class F{public: vitrual M func(){} }; class S:public F{public: N func{} }; //如果func要构成重写,必须满足 M==N
vitrual M* bar(){} N* bar(){} //如果bar要构成重载,M和N必须是类类型 且 N是M的子类
vitrual M& goo(){} N& goo(){} //如果goo要构成重载,M和N必须是类类型 且 N是M的子类
子类重写的函数不受访问控制属性的限制 重写的函数必须常属性一致 virtual void fun()/const/{}

重载、隐藏、重写的区别。 (共同点是函数名相同)
重载:在同一个作用域下面,函数名相同,参数列表不同,构成重载,与返回值无关。
重写:子类重写父类中的虚函数,函数名和参数列表相同,与返回值相关。
隐藏:子类隐藏父类中同名的方法 (1)父类中的函数为virtual函数,参数列表不同。(2)如果父类中的函数不是虚函数,只要函数名相同即构成隐藏

虚析构
析构函数为虚函数。 如果一个类的析构函数声明为虚函数,则当释放一个基类指针指向子类对象时,会调用子类的析构函数。
子类的析构函数不管怎么样都会默认自动调用父类析构函数。 能保证所有内存都释放。
所有在继承与多态过程中,把析构函数声明为虚函数非常有必要。

对于子类对象的指针或者引用 可以 隐式转换为 任一父类类型指针或者引用,
通过 静态类型转换 可以确保把每一个指向子类对象的指针或者引用转换为子类类型的指针或者引用
但是 通过重解释类型转换 把指向子类对象的基类类型转换为子类指针时,不能确保每一个都正确

纯虚函数 virtual func(){}=0;
首先是一个虚函数,其次该虚函数不需要函数体。如果一个类有纯虚函数那么该类是不完整的类,称为抽象类,抽象类不能实例化对象。
如果一个类继承了一个抽象类,那么需要重写抽象类里所有的纯虚函数,否则子类也是抽象类。
构造和析构函数不能声明为纯虚函数。 如果一个类除了构造函数和析构函数以外所有函数都是纯虚函数,那么该类称为纯抽象类。
抽象类的作用
(1)提供一个基类类型,可以声明基类类型的指针和引用来指向或引用子类对象,形成多态。
(2)封装子类共同拥有的函数
(3)定义纯虚函数提供统一的接口,子类重写这些纯虚函数

虚表指针 虚函数表指针 -> 虚函数表
如果一个类里面有虚函数,那么该类的对象会比没有虚函数的类的对象大四个字节。 这四个字节是一个指针,这个指针指向虚函数表
每一个拥有虚函数的类将生成一份虚函数表,虚函数表本质上是一个数组,数组中的成员是虚函数的指针。
每一个类有一个虚函数表,该类的对象共享一个虚函数表。 每一个对象拥有一个虚表指针,同一个类的对象虚表指针是相同的。
虚函数表:把类里面每一个虚函数的地址放到一个数组里面,如果子类没有重写父类的虚函数,则直接用父类虚函数的地址,如果子类重写,则用子类重写虚函数的地址替换

(重载:静态绑定 根据参数的个数和类型来绑定调用的函数,编译时已经决定调用哪个函数)
多态的形成 – 动态绑定
在编译 基类指针或者引用 调用虚函数时,不会立即生成调用指令(没有静态绑定),而是生成一段指令来替换调用指令
这段指令完成以下工作:
1、根据 指针指向的目标和引用的目标 找到虚表指针。
2、通过虚表指针找到虚函数表。
3、通过虚函数表找到所调用的函数的地址,完成函数调用。
这段指令只有运行时才能确定哪个函数。称之为运行时绑定,也叫动态绑定。
F *f1 = new F(); pf1->func();
F *f2 = new S(); pf2->func();

运行时信息/动态信息 #include< typeinfo >
typeid 是一个运算符 int a; typeid(a)不支持cout<< typeid(a).name()输出为i
typeid()返回一个类型信息的对象,支持 == 和 != 运算符
typeid(x).name() 获得类型的名字
可以用来判断动态运行时 指针所指目标的真实类型 引用所引用对象的真实类型,而非指针和引用变量本身的类型
如果没有虚函数,typeid得到的是 指针和引用变量 本身的类型
if(typeid(对象) == typeid(类型))//判断某个对象是否是某某类型的对象,动态绑定运行时不知道是什么类型。Shape *s1=x; Shape *s2=x;类型可能不一样

动态类型转换 dynamic_cast
关于静态类型转换 static_cast 只要源类型和目标类型在一个方向上能进行隐式类型转换,那么两个方向上就都能够静态类型转换。不会检查源类型和目标类型是否一致,从而导致一些程序bug。
用dynamic_cast替换,在运行时会检查源对象和目标类型是否可转换,是则转换成功,否则抛出异常
适用场合:有多态的父子类型指针或引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值