📖 前言
C++是一门面向对象的语言,其三大特性,封装、继承、多态。 本文将介绍来好好讲讲多态…
1. 多态的概念
多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
2. 多态的定义及实现
2.1 虚函数:
虚函数: 即被 virtual 修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
2.2 虚函数的重写(覆盖):
虚函数的重写(覆盖):
- 派生类中有一个跟基类完全相同的虚函数
- 即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同
- 称子类的虚函数(重写)了基类的虚函数,也叫(覆盖)
下面我们实现一个简单的买票系统 —— 要求实现不同的对象做同一件事产生不同的状态:
class Person
{
public:
Person(const char* name)
:_name(name)
{}
virtual void BuyTicket() { cout << _name << " Person: 买票-全价 100¥" << endl; }
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name)
:Person(name)
{}
virtual void BuyTicket() { cout << _name << " Student: 买票-半价 50¥" << endl; }
};
class Soldier : public Person
{
public:
Soldier(const char* name)
:Person(name)
{}
virtual void BuyTicket() { cout << _name << " Soldier: 优先买预留票-88折 100¥" << endl; }
};
void Pay(Person* ptr)
{
ptr->BuyTicket();
delete ptr;
}
void Pay(Person& ptr)
{
ptr.BuyTicket();
}
int main()
{
int option = 0;
cout << "=========================================" << endl;
do
{
cout << "请选择身份:";
cout << "1、普通人 2、学生 3、军人" << endl;
cin >> option;
cout << "请输入名字:";
string name;
cin >> name;
switch (option)
{
case 1:
{
Person p(name.c_str());
Pay(p);
break;
}
case 2:
{
Student s(name.c_str());
Pay(s);
break;
}
case 3:
{
Soldier s(name.c_str());
Pay(s);
break;
}
default:
cout << "输入错误,请从新输入" << endl;
break;
}
cout << "=========================================" << endl;
} while (option != -1);
return 0;
}
上述代码总结:
- 多态是同样是买票,不同的人买票,结果不一样
- 父类的指针,父类的引用,可以指向子类的对象,也可以指向父类的对象
2.2 - 1 多态的两个条件(重点)
- 必须是父类的指针或者引用去调用虚函数
- 子类虚函数重写父类的虚函数(重写:三同 【函数名/参数/返回值】 + 虚函数)
1.为什么一定是父类的指针和引用去调用虚函数,父类的对象可以吗?
先看运行结果:
- 因为都调用到父类对象去了 —— 不构成多态
- 原理要到多态的原理中才能理清楚
2.如果有一个条件不满足多态的话(当参数不满足相同时):
我们来看运行结果:
- 我们看到结果是有问题的,并没有达到我们想要的情况,军人的虚函数不符合重写的规则
- 到讲解多态的原理时,我们再来深入学习
- 现在只需要记住多态的两个条件(重点)
3. 虚函数重写的两个例外
3.1 协变 - 基类与派生类虚函数返回值类型不同:
1.协变的概念:
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
2.父子关系的指针:
class A
{};
class B : public A
{};
class Person
{
public:
virtual A* f()
{
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* f()
{
cout << "virtual B* Student::f()" << endl;
return nullptr;
}
};
int main()
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
3.父子关系的引用:
4.父子关系的对象:
可见父子关系的对象是不行的。
C++在这里搞麻烦了,意义不是很大。
5.如果同为父类:
这里则是继承中的隐藏。
补充:
- 子类的虚函数没有写virtual,f依旧是虚函数
- 因为先继承了父类函数接口的声明
- 重写的是父类虚函数的实现
- 所以父类有virtual的属性子类也就有了
- 这样写不太规范
建议:
- 最好不要协变或者子类不加virtual
- 我们自己写的时候子类虚函数也写上virtual
3.1 - 1 一道非常考验基础知识的笔试题
1.以下程序输出结果是什么?
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
B* p = new B;
p->test();
return 0;
}
运行结果:
重写其实是一种接口继承。
2.看下面代码的运行结果是什么? 运行结果: 这里就是类成员的普通调用,并不是多态的调用。
3.再来看一组: 这才是多态的调用,时刻记住多态的两个条件!
多态:拿一个类的指针去调用另一个类的函数。
3.2 析构函数的重写 - 基类与派生类析构函数的名字不同:
1.析构函数不构成多态的情况:
普通调用看的指针类型,指针类型是Person * ,就去调用Person的析构函数去了
- 如果不构成多态这是一个普通调用
- 普通调用都是看调用变量或者指针的类型
只要完成重写(覆盖),指向子类调子类,指向父类调父类,构成了多态。
问题:
- 这里调用就会出现调用不到子类的析构,会存在内存泄露的风险。
2.解决办法 - 多态:
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person p;
Student s;
return 0;
}
(1) 对于普通对象没有影响:
- 子类对象析构的时候是先子后父,初始化是先父后子
- 调用子类的析构函数会自动调用父类的析构函数
析构函数默认是隐藏关系,如果要实现多态 – 析构函数的函数名都要被处理成destructor,才能满足多态的条件。
- 如果Person析构函数加了virtual,关系就变了
- 加上virtual之后就从隐藏关系变成了:重写关系 – 隐藏/重定义->重写/覆盖
(2) 但是指针的切片调用就会有不同,触发了多态:
建议:
- 如果设计的一个类,可能作为基类。
- 其析构函数最好定义为虚函数。
4. 新增的两个关键字
4.1 final的使用:
1.final:修饰虚函数,表示该虚函数不能再被重写。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz" << endl; }
};
int main()
{
return 0;
}
2.在继承那一章节我们讲到,如何实现一个不能被继承的类:
- 当时我们讲到了一种方法,那就是将父类构造函数私有化 👉 复习传送门
- 但是这不是一种很好的方式
(1) C++98 – 间接的方式(通过语法的规则走的间接方式)
正常继承并没有报错。
- 这里不是说子类继承不了,而是直接继承不了,而是通过间接实现的
而是通过间接的方式:
- 因为子类的构造函数必须要去调用父类的构造函数
- 而父类的构造函数私有子类不可见
- 是通过联合的方式来调用的
- 通过语法规则走了一层间接
(2) C++11 – 直接的方式
final的类不能被继承 – 最终类,不能继承,更直观一些。
3.final的两个作用:
4.2 override的使用:
1.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car
{
public:
virtual void Drive() {}
};
class Benz :public Car
{
public:
virtual void Drive() override { cout << "Benz" << endl; }
};
int main()
{
return 0;
}
override是写在子类中的,要求严格检查是否完成重写,如果没有就报错。
这里并没有完成重写,所以报错了:
5. 重载、覆盖(重写)、隐藏(重定义)的对比
一张图讲清楚这三者各自的特点:
补充:
6. 抽象类概念 + 使用
概念:
- 在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。
- 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
代码演示:
class Car
{
public:
virtual void Drive() = 0;
};
int main()
{
Car c;
return 0;
}
包含纯虚函数的类都叫做抽象类。
抽象就是不具体(虚拟层面),不具象化。
在现实中一般没有具体对应的实体,不想让类实例化出对象
看下述代码: 为什么BMW不能实例化出对象?
- 这是因为BMW也是一个抽象类
- 因为BMW类从Car类中继承了纯虚函数
- 根据定义,有纯虚函数的类叫做抽象类,抽象类是不能实例化对象的。
正确的写法:
这时候就可以实例化出对象了。
从另一个角度分析:
- 纯虚函数除了让基类不能实例化出对象
- 同时又做了另一件事情
- 那就是间接的让子类强制重写
- 而override是放在子类中查重写,没重写就检查报错
基类不能定义对象,但是基类可以定义指针:
- 但是这里可以new出子类对象。
- 这里指向谁调用谁,构成了多态。
同样的指针可以进行如上操作:
- 引用当然也可以进行以上操作
- 因为它们俩本质在底层是一样的
- 那么实现起来还是有意义的
- 因为父类的指针指向父类对象(多态的场景或普通场景)或者是父类的指针指向子类对象(非多态场景)
- 父类的对象可以调用的到
- 那么就是没有人能调用
- 因为父类没有对象,子类对象直接去调用的是自己的函数
- 如果是多态的场景,调用的也是自己的
- 因为父类的指针不会指向父类的对象,父类对象new不出来
综上所述:
- 纯虚函数的函数体实现没有任何意义,因为没人能用到它,因为纯虚函数没人能调用得到。
- 所以我们一般情况下,纯虚函数不回去实现,直接给一个声明就可以了
- 纯虚函数本身也就是一个接口继承
|