来前言:
我们之前提到过,C++是一门面向对象的语言,它有三大特性——封装、继承、多态。
封装和继承我们已经详细学习过了,本章将进入多态的学习。
目录
(一)多态的概念
(二)多态的定义和实现
(1)多态的构成条件
1、虚函数
2、虚函数的重写(覆盖)
3、多态的条件(重点)
(2)虚函数重写的两个例外
1、协变(基类与派生类虚函数返回值类型不同)
2、析构函数的重写(基类与派生类析构函数的名字不同)
(三)C++11---两个关键字之override和final
(1)final的用法
(2)override的用法
(四)重载、覆盖(重写)、隐藏(重定义)的对比
(五)抽象类的概念+使用
(1)概念
(2)接口继承和实现继承
(一)多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
(二)多态的定义和实现
(1)多态的构成条件
1、虚函数
在定义多态前,我们先要了解虚函数。
虚函数:即被virtual修饰的类成员函数称为虚函数
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; }
};
3、多态的条件(重点)
有了上面虚函数和覆盖的基础,我们给出下面继承构成多态的条件:
- 1. 必须通过基类的指针或者引用调用虚函数
- 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
我们实现一个简易的购票系统为例:
//多态只用的样例:
class Person
{
public:
Person(const char* name)
:_name(name)
{}
//虚函数
virtual void BuyTicket() { cout << _name << " Person: 买票-全价 100¥" << endl; }
protected:
string _name;
//int _id;
};
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();
}
//全部都去调用父类去了 -- 不构成多态
//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 case语句里面,是不能支持定义对象的,要加一个域{}
//加完域之后就是局部域了
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;
}
这里我们满足形成多态的两个条件:
1、必须通过基类的指针或者引用调用虚函数:
错误范例:
- 因为都调用到父类对象去了 —— 不构成多态
- 原理要到多态的原理中才能理清楚
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
我们实现多态必须严掐多态的条件!!!!(下面给出例外的情况,例外情况的次数出现较少)
(2)虚函数重写的两个例外
1、协变(基类与派生类虚函数返回值类型不同)
协变的概念:
派生类重写基类虚函数时, 与基类虚函数返回值类型不同 。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
- 虚函数重写对返回值的要求有个例外,叫作:协变
- 协变的返回值类型也不是随便的,必须是(父子关系)的指针和引用
例:
A和B构成父子关系,而Person和Student返回的正是这对父子关系的指针,此处就构成协变。
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;
}
补充:
- 子类的虚函数没有写virtual,f依旧是虚函数
- 因为先继承了父类函数接口的声明
- 重写的是父类虚函数的实现
- 所以父类有virtual的属性子类也就有了
- 这样写不太规范
建议:
- 最好不要协变或者子类不加virtual
- 我们自己写的时候子类虚函数也写上virtual
浅刷几道笔试题~
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、析构函数的重写(基类与派生类析构函数的名字不同)
- 重写又叫做覆盖
- 隐藏又叫做重定义
析构函数不构成多态的情况:
这样子调用析构函数,p1、p2一看自己的类型是Person,就自动调用了Person的析构函数,这样会导致内存泄漏!!
解决办法:
多态的实现。
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然, 这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
//~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
//~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
只有用多态构成析构函数,编译器才会正确调用析构函数,不会造成内存泄漏。
析构函数默认是隐藏关系,如果要实现多态 – 析构函数的函数名都要加virtual,才能满足多态的条件。
- 如果Person析构函数加了virtual,关系就变了
- 加上virtual之后就从隐藏关系变成了:重写关系 – 隐藏/重定义->重写/覆盖
建议:
- 如果设计的一个类,可能作为基类。
- 其析构函数最好定义为虚函数。
(三)C++11---两个关键字之override和final
引入:
- 从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和fifinal两个关键字,可以帮助用户检测是否重写。
(1)final的用法
1、final:修饰虚函数,表示该虚函数不能再被重写
2、final:修饰类,表示该类不能被继承。
在继承那一章节我们讲到,如何实现一个不能被继承的类:
- 当时我们讲到了一种方法,那就是将父类构造函数私有化
- 但是这不是一种很好的方式
final的类不能被继承 – 最终类,不能继承,更直观一些。
总结final的两个作用:
- 修饰类 - 不能被继承
- 修饰虚函数 - 不能被重写
(2)override的用法
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
override是写在子类中的,要求严格检查是否完成重写,如果没有就报错。、
这里完成了虚函数的重写,所以不报错:
这里把基类的virtual去掉,Drive就不是虚函数了,更别说完成了重写,所以报错:
(四)重载、覆盖(重写)、隐藏(重定义)的对比
我们在刚步入C++时学过函数重载,继承时候学过隐藏(重定义),刚刚学虚函数时讲解到了覆盖(重写),那么他们三到底有什么区别呢?
一张图了解:
(五)抽象类的概念+使用
(1)概念
概念:
- 在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。
- 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
代码演示:
//抽象类
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
int main()
{
Car c;
return 0;
}
子类重写后:
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
重写后,派生类才可以实例化出对象。
(2)接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
我们通过上面的抽象类也可以看出,虚函数继承是一种接口继承。
综上所述:
- 纯虚函数的函数体实现没有任何意义,因为没人能用到它,因为纯虚函数没人能调用得到。
- 所以我们一般情况下,纯虚函数不会去实现,直接给一个声明就可以了
- 纯虚函数本身也就是一个接口继承
本章基本讲解了多态的 概念、条件及其性质,下面一章我们会着重分析其底层实现和原理!
感谢您的阅读,祝您学业有成!!