继承和多态
为什么我们要使用继承?
使用继承是为了减少代码的冗余,增强代码的可扩展性,并且当两个类具有相同的特征(属性)和行为(方法)时,可以相同的部分抽取出来放到一个类中作为父类,其他两个类继承这个父类。
文章目录
1.继承和派生基本概念
(1)继承
-
以现有的类为基础来构建新类
-
新类(派生类)(子类)继承现有类(基类)(父类)的属性和行为
-
派生类可以修改继承的行为
-
派生类可以增加新的属性和行为
-
派生类对象也是基类对象
-
派生类对象和基类对象可以被统一管理
(4)对于基类和派生类的理解
基类就是父类,派生类就是子类
你可以理解为生活中的父子关系,
也就是说儿子继承了所有的父亲的样貌特征,但是儿子也有父亲所没有的一些特点
专业点讲,就是子类可以继承并使用父类的属性和方法,也可以有自己的属性和方法!
(3)例子
家具和柜子, 建筑和房子 ,道路和高速公路, 动物和猫 ,计算机和笔记本电脑
class Mammal {
public:
Mammal();
~Mammal();
......
};
class Lion : public Mammal {
public:
Lion();
~Lion();
......
}
2.继承的定义
在 C++ 中,当定义一个新的类 B 时,如果发现类 B 拥有某个已写好的类 A 的全部特点,此外还有类 A 没有的特点,那么就不必从头重写类 B,而是可以把类 A 作为一个“基类”(也称“父类”),把类 B 写为基类 A 的一个“派生类”(也称“子类”)。这样,就可以说从类 A “派生”出了类 B,也可以说类 B “继承”了类 A。
(1)继承的方式
- 共有继承(public)
- 受保护继承(protected)
- 私有继承(private)
(2)派生类(子类)中重定义基类的函数
- 派生类自动继承基类所有成员
- 若基类成员函数不满足需要,在派生类中重新定义函数
- 派生类对象可以直接访问自己定义的函数,也可以访问定义于基类被覆盖的函数
- 调用方式:基类名 :: 函数名
(4)基类(父类)中的私有成员
- 能被派生类继承
- 不能被派生类的成员函数直接访问
- 可通过定义于基类的共有和受保护成员函数访问
(5)在 C++ 中,从一个类派生出另一个类的写法
class 派生类名: 继承方式说明符 基类名{
...
};
继承方式说明符可以是 public(公有继承)、private(私有继承)或 protected(保护继承)。一般都使用 public。protected 或 private 方式很少用到。
class CBase
{
int v1, v2;
};
class CDerived: public CBase
{
int v3;
};
(6)基类的构造函数和析构函数不被继承,但会被派生类的构造函数和析构函数自动调用
- 派生类的构造函数先调用基类的构造函数,再执行自己的函数体
- 派生类的析构函数先执行自己的函数体,再调用基类的析构函数
(4)调用方式的区别
- 调用基类函数:
- 基类名 :: 函数名
- 调用派生类函数:
- 直接调用
#include <iostream>
#include <string>
using namespace std;
class CStudent
{
private:
string name;
string id; //学号
char gender; //性别,F 代表女,M 代表男
int age;
public:
void PrintInfo();
void SetInfo(const string & name_, const string & id_, int age_, char gender_);
string GetName() { return name; }
};
class CUndergraduateStudent : public CStudent //本科生类,继承了 CStudent 类
{
private:
string department; //学生所属的系的名称
public:
void QulifiedForBaoyan() { //给予保研资格
cout << "qulified for baoyan" << endl;
}
void PrintInfo() {
CStudent::PrintInfo(); //调用基类的 PrintInfo 函数
cout << "Department: " << department << endl;
}
void SetInfo(const string & name_, const string & id_, int age_,
char gender_, const string & department_) {
CStudent::SetInfo(name_, id_, age_, gender_); //调用基类的 SetInfo 函数
department = department_;
}
};
void CStudent::PrintInfo()
{
cout << "Name: " << name << endl;
cout << "ID: " << id << endl;
cout << "Age: " << age << endl;
cout << "Gender: " << gender << endl;
}
void CStudent::SetInfo(const string &name_, const string & id_, int age_, char gender_)
{
name = name_;
id = id_;
age = age_;
gender = gender_;
}
int main()
{
CStudent s1;
CUndergraduateStudent s2;
s2.SetInfo("Harry Potter", "118829212", 19, 'M', "Computer Science");
cout << s2.GetName() << " ";
s2.QulifiedForBaoyan();
s2.PrintInfo();
cout << "sizeof(string) = " << sizeof(string) << endl;
cout << "sizeof(CStudent) = " << sizeof(CStudent) << endl;
cout << "sizeof(CUndergraduateStudent) = " << sizeof(CUndergraduateStudent) << endl;
return 0;
}
运行结果:
Harry Potter qulified for baoyan
Name: Harry Potter
ID: 118829212
Age: 19
Gender: M
Department: Computer Science
sizeof(string) = 4
sizeof(CStudent) = 16
sizeof(CUndergraduateStudent) = 20
3.继承关系中类指针的使用
- 类名可以用来声明变量(类的对象)、数组、指针等
- 类的指针可以操作类的对象,也可以操作派生类的对象 (派生类对象也是基类对象)
- 派生类对象和基类对象可以通过指针统一操作和管理
- 类指针操作类对象的几种可能 :
- 基类指针操作基类对象(自然)
- 派生类指针操作派生类对象(自然)
- 基类指针操作派生类对象——把派生类对象作为基类对象看(安全)
- 派生类指针操作基类对象——把基类对象作为派生类对象看(危险)
4.构建继承关系的线性表
(1)用链表实现可变长(整型)数组
// 文件VLArray.h: 类VLArray的定义
#pragma once
class Node {
friend class VLArray;
private:
int data;
Node* next;
};
class VLArray {
public:
VLArray();
~VLArray();
void set(int i, int m); //修改数组元素的值
int getLength(); //获取数组的长度
int get(int i); //获取数组第i个元素的值
void del(int i); //删除数组的第i个元素
private:
Node* head;
int len;
};
// VLArray.cpp: 类VLArray的实现
#include <stdlib.h>
#include "VLArray.h"
VLArray::VLArray() {
head = NULL;
len = 0;
}
VLArray::~VLArray() {
Node* p = head;
while (p) {
head = head->next;
delete p;
p = head;
}
}
int VLArray::getLength() {
return len;
}
int VLArray::get(int i) {
if (i >= 0 && i < len) {
Node* p = head;
for (int k = 0; k < i; k++)
p = p->next;
return p->data;
}
return 0;
}
void VLArray::set(int i, int m) {
if (len == 0) {
//在空数组中插入第一个元素
head = new Node;
head->data = m;
head->next = NULL;
len++;
}
else if (i >= 0 && i < len) {
//修改数组第i个元素
Node* p = head;
for (int k = 0; k < i; k++)
p = p->next;
p->data = m;
}
else if (i < 0) { //在数组首部插入元素
Node* p = new Node;
p->data = m;
p->next = head;
head = p;
len++;
}
else { //在数组尾部插入元素
Node* q = new Node;
q->data = m;
q->next = NULL;
Node* p = head;
for (int k = 0; k < len - 1; k++)
p = p->next;
p->next = q;
len++;
}
}
void VLArray::del(int i) {
if (len == 0)
return;
if (i == 0) {
Node* p = head;
head = head->next;
delete p;
len--;
}
else if (i > 0 && i < len) {
Node* p = head;
for (int k = 0; k < i - 1; k++)
p = p->next;
Node* q = p->next;
p->next = q->next;
delete q;
len--;
}
}
// 文件ex.cpp: 使用类VLArray
#include "VLArray.h"
#include <iostream>
using namespace std;
int main() {
int m, i;
VLArray a;
for (i = 0; i < 10; i++) {
cin >> m;
a.set(a.getLength(), m);
}
for (i = 0; i < a.getLength(); i++) {
m = a.get(i);
cout << m << " ";
}
cout << endl;
a.del(4);
for (i = 0; i < a.getLength(); i++)
{
m = a.get(i);
cout << m << " ";
}
cout << endl;
return 0;
}
运行结果:
1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
1 2 3 4 6 7 8 9 0
(2)使用可变长数组构建栈类Stack
- push(k):将整数k压栈
- isEmpty():判断是否栈空
- pop():弹出栈顶元素并返回,栈空则返回0
// 文件Stack.h: 类Stack的定义
#pragma once
#include "..\8_2\VLArray.h"
class Stack :protected
VLArray {
public:
Stack() {};
~Stack() {};
void push(int k);
bool isEmpty();
int pop();
};
// 文件Stack.cpp: 类Stack的实现
#include "Stack.h"
void Stack::push(int k) {
set(-1, k);
}
bool Stack::isEmpty() {
if (getLength() == 0)
return true;
return false;
}
int Stack::pop() {
if (getLength() == 0)
return 0;
int m = get(0);
del(0);
return m;
}
// 文件8_3.cpp: 使用类Stack
#include "Stack.h "
#include <iostream>
using namespace std;
int main() {
Stack s;
int i, m;
for (i = 0; i < 10; i++) {
cin >> m;
s.push(m);
}
for (i = 0; i < 10; i++) {
m = s.pop();
cout << m << " ";
}
cout << endl;
return 0;
}
程序运行结果
1 2 3 4 5 6 7 8 9 0
0 9 8 7 6 5 4 3 2 1
5.多态性
多态性:具有继承关系的类,其对象对同一个函数调用可 以作出不同的响应
- 静态类型与动态类型
- 对象的静态类型:
- 对象在声明时候的类型,是在编译时期确定的
- 对象的动态类型:
- 目标所指向的对象,是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
- 对象的静态类型:
- 静态绑定与动态绑定
- 静态绑定:
- 绑定的是静态类型,比如函数依赖于对象的静态类型,发生在编译期
- 在编译时确定函数或方法的调用方式。
- 适用于静态方法和非虚函数
- 编译器会根据函数或方法的名称和参数类型来确定调用方式
- 动态绑定:
- 绑定的是动态类型,比如函数依赖于对象的动态类型,发生在运行期
- 在运行时确定函数或方法的调用方式。
- 适用于虚函数和多态。
- 根据对象的类型来确定调用哪个函数或方法。
- 静态绑定:
#include <iostream>
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
void display();
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
People *p = new People("王志刚", 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。
我们直观上认为,如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数,这符合人们的思维习惯。但是本例的运行结果却告诉我们,当基类指针 p 指向派生类 Teacher 的对象时,虽然使用了 Teacher 的成员变量,但是却没有使用它的成员函数,导致输出结果不伦不类(赵宏佳本来是一名老师,输出结果却显示人家是个无业游民),不符合我们的预期。
换句话说,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数
6.虚函数
为了上面代码的这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。
-
虚函数的定义:
- 函数原型前加上关键字virtual
- 如果一个函数在基类中被声明为虚函数,则他在所有派生类中都是虚函数(包括重定义函数) virtual void show();
-
只有通过基类指针或引用调用虚函数才能引发动态绑定
#include <iostream>
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
virtual void display(); //声明为虚函数
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
virtual void display(); //声明为虚函数
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
People *p = new People("王志刚", 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<“今年”<<m_age<<“岁了,是一名教师,每月有”<<m_salary<<“元的收入。”<<endl;
}
int main(){
People *p = new People(“王志刚”, 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
```cpp
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。