C++学习笔记——继承与多态


继承与多态都是C++中很重要的两个概念,也是相比起C语言的优势所在,下面就来总结一下C++中继承与多态的概念以及简单用法!

一.继承

1.1 什么是继承?

面向对象思想是一种更加符合人类思维的思想,一开始我还不怎么理解这句话,但是随着对C++以及面向对象思想的学习,我越发感觉到这句话的意义所在。
因为C++中的一大特点,封装的存在,将一类对象的共同特点,共有的成员和方法封装成类,但是,不可能每一类对象都要从头进行封装,就像游戏给一个英雄添加了一项新技能技能,总不能重头在创建一个新的英雄类吧,而且,在升级系统时有一个原则,那就是保持源码的完整性,因为源码是经过很多测试的,如果修改,则需要重新测试,所以,我们可以采用继承的方式,继承原有的类,并在此基础上新增一些自己特有的成员与方法,称被继承的类为基类(父类),继承下来的类叫派生类(子类)。

1.2 通过图来理解继承

定义一个基类:Person类

class Person  //Person类
{
    public:
        Person();  //默认构造
        char* GetName();
        void SetName(char *name);
        virtual ~Person();  //默认析构

    protected:

    private:
        char *m_name;
        int   m_age;
        bool  m_gender;
};

定义还一个Person类作为基类后,在定义一个学生类,一个教师类来继承Person类如下图:
在这里插入图片描述
从图中可知。Student类,Teacher类继承了Person类,我只把派生类中的新成员与新方法写了出来,但实际上,子类继承了父类的虽有成员与方法。值得注意的是,子类虽然继承了父类的私有成员,但却不能直接访问,须通过父类继承下来的方法来获取;
在子类继承了父类的虽有成员与方法后,子类还新增了学号专业,工号部门等成员,新增了Get,Set方法;代码实现:

class Student : public Person  //学生类
{
    public:
        Student();
        char* GetSNO();
        void SetSNO(char *sno);
        virtual ~Student();

    protected:

    private:
        char *m_sno;
        char *m_major;
};

class Teacher : public Person  //教师类
{
    public:
        Teacher();
        char* GetTNO();
        void SetTNO(char *tno);
        virtual ~Teacher();

    protected:

    private:
        char *m_tno;
        char *m_depart;
};

1.3 派生类构造函数的写法

派生类虽然继承了基类,但却需要自己的构造函数与析构函数,因为子类中有特有的成员,光靠父类的构造函数是无法进行初始化的,派生类的析构函数有下面几种写法:

  1. 使用基类的默认构造
Student(形参列表):
{
    //派生类中新成员的初始化;
}

这样,程序会先调用基类的默认构造函数,在调用子类的构造函数;

  1. 调用基类重载的构造函数
Teacher(形参列表):基类名(实参列表)
{
    //派生类中的成员初始化;
}

编译器会根据实参列表选择基类相应的构造函数进行调用,其实参来自于Teacher的形参列表;

无论哪一种方法,编译器都会先执行基类的构造函数分,在调用派生类的构造函数,而析构函数的调用则与构造函数调用顺序相反。

1.4 多继承中的二义性

派生类可以继承一个基类,也可以继承多个基类,我们管这中方式为多继承:
在这里插入图片描述
这样,助教类就拥有了学生类与教师类的成员与方法:

class Assistant : public Student,public Teacher
{
    public:
        Assistant();
        virtual ~Assistant();

    protected:

    private:
};

这样,我们在初始化Assistant对象时,就会有以下步骤:

Person构造 —> Student构造 —> Person构造 —> Teacher构造 —> Assistant构造
可知Person构造构造了两次,若我们在assistant对象中,调用Person中的方法,assistant.GetName(); 那编译器如何知道,我们调用的是Student中继承而来的,还是Teacher中继承而来的呢?所以编译器会报错。
多继承的二义性,有以下两种解决方法:

  1. 添加作用域
assistant.Student::GetName();
  1. 添加virtual关键字
    在定义派生类时,通过虚拟继承的方式将基类声明为虚基类,虚基类中的成员在类的继承中只会被继承一次,从而解决了多继承中的二义性问题,语法:
class 派生类名 : virtual 继承方式 虚基类名
{
    .......
}

1.5 三种继承方式

前面提到了共有继承,也提到了基类的私有成员子类是无法访问的,下面就详细介绍继承方式以及相关成员的访问权限:
在这里插入图片描述
继承方式:
在这里插入图片描述
也就是说,基类的私有成员子类无论使用那种那个继承方式都不可以访问,而采用保护继承时,基类的共有与保护属性的变量都会变为保护属性。

1.6 函数的重定义

由上面的Person类与Student类可知,当我们想打印Person类的信息时,只需打印姓名,年龄和性别,然而当子类继承了这个方法后,子类有新增了学号,专业等信息,如果直接使用继承得来的方法,则不能显示完整的学生信息,只会显示学生信息的姓名,年龄与性别,所以要重定义函数;
重定义函数与函数的重载不同,函数的重定义只能更改函数的内部实现,而不能修改函数头。
例如,Person类的Show方法:

void Person::Show()
{
    cout << "个人信息" << endl;
    cout << "姓名: " << m_name << "\n";
    cout << "年龄: " << m_age << "\n";
    cout << "性别: " << m_gender << endl;  
}
void Student::Show()
{
    cout << "个人信息" << endl;
    cout << "姓名: " << m_name << "\n";
    cout << "年龄: " << m_age << "\n";
    cout << "性别: " << m_gender << "\n";
    cout << "学号: " << m_sno << "\n";
    cout << "专业: " << m_major << endl;
}

二. 多态

2.1 什么是多态?

多态,向不同对象发送同一条消息(函数调用),不同对象在接收到消息后会产生不同的行为,即不同的实现,执行不同的函数,或者函数名相同,但执行的细节相同。
打个比方,当我们进入召唤师峡谷后,大家都会收到一条消息,那就是“欢迎来到英雄联盟!”,然而,不同的角色听到这句话后,会有不同的动作,比如上单会往上路走,中单去中路,打野去反野或者刷自家Buff,下路双人组帮助打野击杀Buff一样,不同对象在接收到相同的信息后的行为不一样,这就是多态。

2.2 类型兼容

所谓类型兼容,是C++已经为我们实现好的一个功能,即

  1. 可以用派生类对象为基类对象赋值:
Student student;
Person person = student;
  1. 可以用派生类的对象初始化基类的引用:
Teacher teacher;
Person& refPerson = teacher;
  1. 可以用派生类对象的地址为基类类型的指针赋值:
Teacher teacher;
Person *person = &teacher;

这就是所谓的类型兼容。

2.3 虚函数

2.3.1 先期绑定

先期绑定,又称静态绑定,下面举一个例子,假设我在父类中定义了一个方法:

void Print(Person& refperson)
{
    refperson.Show();
}

因为前面提到的类型兼容的原因,在传入参数时,我可以传入Student的引用,也可以传入Teacher类的引用,当然可以传入Person类的引用,然而,当编译器编译后,便会将Show方法与Person类绑定,只要用到Show方法的地方,都会使用Person类的Show方法,即较为低级的Show方法,并不能调用到新的会者说专属的Show方法,是在编译器编译的时候就决定了,一种解决方法是使用函数重载:

void Print(Person& refperson);
void Print(Student& refstudent);
void Print(Teacher& refteacher);

在分别实现他们懂得内部细节即可,但这,都不是真正的多态,真正的多态,即“后期绑定”。

2.3.2 后期绑定

又叫动态绑定,即绑定发生于程序运行时,我们需要做的,只是在基类中定义函数时,加上一个
virtual 关键字,这样,该函数则变为虚函数:

virtual void Print(Person& refperson);virtual void Print(Person* ptrperson);

不可以是

virtual void Print(Person person);

这样,在传入不同的类后,编译器会根据类的类型选择相应的Show方法来执行,这边是多态。

总结下来,要实现,需要下面3中要求:

  1. 类型兼容
  2. 虚函数
  3. 定义函数时使用的是指针或引用

2.4 虚函数的工作原理

假设,我们创建了这样了以类:

class Person  //Person类
{
    public:
        Person();  //默认构造
        char* GetName();
        void SetName(char *name);
        virtual void Show();
        virtual void Work();
        virtual ~Person();  //默认析构

    protected:

    private:
        char *m_name;
        int   m_age;
        bool  m_gender;
};

当我们创建了一个Person类的实例后,其在内存中有一个隐藏指针,这个指针指向了一个叫虚函数表的东西:
在这里插入图片描述
当子类继承基类后,同时继承了虚函数表,当子类实现了Show方法,则该方法从表中移除。

三.抽象类

3.1 抽象类的定义

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。
父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
打个比方:
在这里插入图片描述
基类是形状类,这并不能实例化为真正的对象,而只能通过其他类继承后才能使用,因为无法实例化成具体的对象,所以里面的Draw(),Erase(),Move()都不能在本身使用。
一个类如果是抽象类,则该类中至少要有一个纯虚函数:即初始化为0的虚函数:

virtual <函数类型> <纯虚函数名> (形参列表) = 0

这就是这一次的总结啦!之后会再补充一些其他的知识点!
顺便庆祝大三下学期完结,从开学到考试全部在家中进行,
一个在家 上课+考试 的学期…

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值