继承与派生

1. 继承与派生的概念及方式

1.1 继承形式
class 派生类名 : 基类名

1.2 继承及访问限制
private继承:class声明的类默认的继承方式是private,此时派生类不能访问基类的成员函数,即使该函数定义在基类的public域;
protected继承:派生类只能在类内部访问基类的protected和public成员;
public继承:派生类可以在类外部使用派生类对象访问基类的protected和public成员。
在这里插入图片描述
(ps:struct声明的结构体默认的继承方式是public)

2. 静态成员和继承

(待补充…)

3. 多重继承

多重继承的形式:

class 派生类名 : public 基类1, public 基类2
{
	  函数体
};

3.1 多重继承构造、析构的顺序

  • 生成对象时,先调用基类的构造函数,在调用派生类的构造 函数;处理对象时,先调用派生类的析构函数,再调用基类的析构函数;

  • 构造函数调用的顺序与多重继承的先后有关,析构相反。

3.2 多重继承与默认成员函数
3.2.1构造函数
(1)派生类、基类均采用默认构造函数,则可直接生成派生类对象,不会报错。
(2)基类中显式定义了非默认构造函数,则派生类必须显式调用基类的构造函数,这样才能正确的生成派生类对象。调用顺序:先调用基类构造函数,再调用派生类gzhs。
定义:
在这里插入图片描述
在这里插入图片描述
调用及结果:
在这里插入图片描述
tips: 对基类的构造函数使用成员列表进行初始化。
若派生类中显示定义的构造函数可以没有参数、可以有自己类型的参数列表,但要注意要给基类参数列表中填入与基类显示定义的构造函数参数列表相同(类型相同、个数相同)的常量。

3.2.2 复制构造函数
(1)派生类、基类均采用默认复制构造函数,则可直接调用派生类的复制构造函数生成对象,不会报错。
(2)基类中显式定义了非默认的复制构造函数,派生类中没有显式定义复制构造函数,则可以调用派生类的复制构造函数生成对象。调用顺序:调用基类复制构造函数,后调用派生类默认构造函数;
定义:
在这里插入图片描述
调用及结果:
在这里插入图片描述
(3)基类和派生类中均显式定义了复制构造函数,则派生类必须显式调用基类的复制构造函数,这样才能正确调用派生类的复制构造函数生成对象。调用顺序,先调用基类复制构造函数,后调用派生类复制构造函数;
定义:
在这里插入图片描述
在这里插入图片描述
调用及结果:
在这里插入图片描述
则当Sheep s1;
Sheep s2=s1;
tips:对基类的复制构造函数采用成员列表初始化。

3.2.3 赋值操作符重载函数
(1)基类、派生类均采用默认合成的赋值操作符重载函数时,无需什么特殊操作即可使用;
(2)基类显式定义了赋值操作符重载函数,派生类中未显式定义赋值操作符重载函数,该种情况下无需其他操作可以对派生类对象正常使用赋值运算符(=)。
定义:
在这里插入图片描述
调用及结果:
在这里插入图片描述
(2)基类显式定义了赋值操作符重载函数,派生类中也显式定义了,则派生类中显式定义时要显示调用基类的赋值操作符重载函数。调用顺序:先调用派生类赋值操作符重载函数,再调用基类赋值操作符重载函数。
定义:
在这里插入图片描述
在这里插入图片描述
调用及结果:
在这里插入图片描述
3.3 多重继承的二义性与虚继承
虽然C++支持多重继承,但多重继承会有一些问题,虽然这些问题也能通过一些手段解决,但可以的话还是采用单继承更好。
多重继承的二义性问题:
如:
在这里插入图片描述

在多个基类中有同名成员(成员变量、成员函数)时,派生类在使用时就无法确定使用的到底是哪个,产生二义性错误。
3.3.1 普通多重继承的二义性问题

class Top1
{
public:
    int data=1;
};

class Top2
{
public:
    int data=2;
};

class Mid:public Top1,public Top2
{
public:

};

则如下方式访问时会出现二义性错误:

Mid m;
    cout<<m.data<<endl;//二义性错误

解决方法:通过域操作符指定访问的对象,

cout<<m.Top1::data<<endl;//指定m对象访问基类Top1的成员变量data

3.3.2 菱形继承的二义性问题
在这里插入图片描述
Bottom的对象b有两条继承线路可以访问Top的成员变量data,直接访问则会产生二义性错误。
解决方法一:
通过域操作符指定继承的线路,如

cout<<b.Mid1::data<<endl;//注意:域操作符是用来指定继承线路的,所以应该对Mid1或Mid2使用。

解决方法二:
使用虚继承——每条线路都采用虚继承的方式,对于每条徐继承线路来说,只会产生一个data定义。被虚继承的类叫做虚基类。
形式:

class Top
{
public:
    int data1=0;
};

class Mid1:virtual public Top
{
public:
    int date=1;
};

class Mid2:virtual public Top
{
public:
    int date=2;
};

class Bottom:public Mid1,public Mid2
{

};

此时,可采用如下形式直接访问data:

cout<<b.data<<endl;

tips:
1)在菱形多重继承关系中,当Mid1、Mid2类中也有同名成员时,要结合普通多重继承的域操作符解决法和虚继承解决法。
2)在菱形多重继承关系中,不能在Top、Mid1、Mid2三个基类中都命名同名成员,此情况无法解决二义性问题。

4. 多态与虚函数

静态绑定——发生在编译期,指挥调用基类方法,不会调用派生类方法。
动态绑定——发生在运行期,根据实际绑定的类型调用对应的方法。
4.1 静态绑定
实际是函数重写——必须保证派生类和基类中的函数原型(返回值类型、函数名、参数列表)一模一样。(tip:与函数重载区分开,1)函数重载参数列表不同、返回值类型和函数名相同,实现相似功能。2)函数重载和继承关系不相干(不管有没有继承关系,都可能会有函数重载);函数重写一定是涉及到继承关系,才会出现的)
如:

class Animal
{
public;
    void eat()
    {
        cout<<"吃东西"<<endl;
    }
};

class Cat:public Animal
{
public;
    void eat()
    {
        cout<<"吃鱼"<<endl;
    }
};

class Sheep:public Animal
{
public;
    void eat()
    {
        cout<<"吃cao"<<endl;
    }
};

三个类中的eat()函数就叫做函数重写。
使用:

int main()
 {
    Cat kitty;
    kitty.eat();//输出 吃鱼
    Cat mie;
    mie.eat();//输出 吃草
    
    Animal *a;
    a=&kitty;
    a->eat();//输出 吃东西
    a=&mie;
    a->eat();//输出 吃东西
    return 0;
 }

代码中将基类(Animal) 指针a指向派生类对象(kitty和mie),再分别调用eat()函数,但其输出仍是基类eat()函数的输出结果。这就是因为这样的绑定是静态绑定,指针a的类型在编译时就确定为基类类型了,即使在运行时将其指向了派生类对象,但其调用重写的函数是、时还是调用基类函数。

4.2 动态绑定
通过在基类中对冲写的函数进行虚函数声明,可以实现动态绑定。此时,该基类被认为是抽象的。抽象类。
形式:

class Animal
{
public;
    virtual void eat();//注意,基类中只允许纯虚函数声明,不允许实现
};

class Cat:public Animal
{
public;
    //在派生类中对基类中声明的虚函数进行实现
    void eat()
    {
        cout<<"吃鱼"<<endl;
    }
};

class Sheep:public Animal
{
public;
    //在派生类中对基类中声明的虚函数进行实现
    void eat()
    {
        cout<<"吃草"<<endl;
    }
};

则进行以下调用时,可以得到我们期待的输出:

Animal *a;
    a=&kitty;
    a->eat();//输出 吃鱼
    a=&mie;
	a->eat();//输出 草

tips:
1) 基类中只允许纯虚函数声明,不允许实现,在派生类中对基类中声明的虚函数进行实现;
2) 该类中只要有一个纯虚函数,则该类就被认为是抽象类;
3) 抽象类是不允许进行对象创建的,只能定义指针、引用,然后将指针指向派生类对象,将引用与派生类对象绑定,然后就可以使用指针和引用访问相应类成员了。

Animal ani;//错误,不允许
Animal *a;//正确,可以使用a访问成员,
a=&kitty;
a->eat();//输出 吃鱼
Animal &animal=kitty;//正确,可以使用animal访问成员
animal.eat();//输出 吃鱼

但要注意,该指针和引用只能访问基类成员,不能访问派生类自己定义的成员。
如:`

在派生类Cat中定义了成员变量string color=“yellow”;
但要以cout<<a->color、animal.color或animal.Cat::color、a->Cat::color方式访问都是错误的,
提示找不到该变量或Cat不是Animal 的基类。`

4) 如果纯虚函数在派生类中没有实现,则该派生类也是抽象类;
5) 如果想要使用基类当中的功能,可以定义派生类对象,然后调用基类定义的方法;

如:

animal.Animal::play();//调用基类的play函数
animal.play();//语句无法解析重载函数的地址(即找不到该重载函数???)为什么不能这样访问??
a->play();//调用基类的play函数??为什么可以?
a->Animal::play();//调用基类的play函数
cout<<animal.AA<<endl;//引用方式访问基类自己定义的成员变量AA
cout<<a->AA<<endl; //指针方式访问基类自己定义的成员变量AA

6) 如果5)调用的是基类的纯虚函数,会产生链接错误;
如:`

a->Animal::eat();//链接错误

问题:
1) 抽象类中可以定义成员变量吗?可以的话,如何访问抽象类中的成员变量?
答:可以,直接定义就行,使用引用或指针访问。
2) 可否使用基类指针、引用访问派生类的成员变量?
答:不行。
3) 为什么可以将抽象基类指针、引用与派生类对象绑定?使用区别?详见tips5)的问题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值