文章目录
继承与多态都是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 派生类构造函数的写法
派生类虽然继承了基类,但却需要自己的构造函数与析构函数,因为子类中有特有的成员,光靠父类的构造函数是无法进行初始化的,派生类的析构函数有下面几种写法:
- 使用基类的默认构造
Student(形参列表):
{
//派生类中新成员的初始化;
}
这样,程序会先调用基类的默认构造函数,在调用子类的构造函数;
- 调用基类重载的构造函数
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中继承而来的呢?所以编译器会报错。
多继承的二义性,有以下两种解决方法:
- 添加作用域
assistant.Student::GetName();
- 添加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++已经为我们实现好的一个功能,即
- 可以用派生类对象为基类对象赋值:
Student student;
Person person = student;
- 可以用派生类的对象初始化基类的引用:
Teacher teacher;
Person& refPerson = teacher;
- 可以用派生类对象的地址为基类类型的指针赋值:
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中要求:
- 类型兼容
- 虚函数
- 定义函数时使用的是指针或引用
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;
这就是这一次的总结啦!之后会再补充一些其他的知识点!
顺便庆祝大三下学期完结,从开学到考试全部在家中进行,
一个在家 上课+考试 的学期…