目录
一.多态的概念
二.多态的定义及实现
1.重写/覆盖 的要求
2.多态两个要求:
3.多态的切片示意图
4.多态演示:
买票场景下的多态 完整代码
5.虚函数重写的例外:
协变(父类与子类虚函数返回值类型不同)
6.接口继承和实现继承
多态的坑题目(考接口继承)
7.析构函数名统一会被处理成destructor()
8.C++11 override 和 final
9.重载、覆盖(重写)、隐藏(重定义)的对比
10.抽象类
三.多态的原理
1.虚函数表
2.虚表存储
(1)虚函数重写/覆盖 语法与原理层解释
(2)虚表存储解释
多态调用和普通调用底层解释
(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?
一.多态的概念
多态的概念:通俗来说,就是多种形态,
具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态
。
举个例子:比如
买票这个行为
,当
普通人
买票时,是全价买票;
学生
买票时,是半价买票;
军人
买票时是优先买票。
再举个栗子:
最近为了
争夺在线支付市场
,支付宝年底经常会做诱人的
扫红包
-
支付
-
给奖励金
的
活动。那么大家想想为什么有人扫的红包又大又新鲜
8
块、
10
块
...
,而有人扫的红包都是
1
毛,
5
毛
....
。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额
=
random()%99
;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
去使用支付宝,那么就你扫码金额
= random()%1
;总结一下:
同样是扫码动作,不同的用户扫
得到的不一样的红包,这也是一种多态行为。
ps
:支付宝红包问题纯属瞎编,大家仅供娱乐。
总结:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如
Student
继承了 Person。
Person
对象买票全价,
Student
对象买票半价。
二.多态的定义及实现
1.重写/覆盖 的要求
重写/覆盖:
子类的虚函数重写了基类的虚函数
要求:子类中有一个跟父类完全相同的虚函数
1.重写/覆盖 的要求 重写/覆盖: 子类的虚函数重写了基类的虚函数 要求:子类中有一个跟父类完全相同的虚函数
即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)
2.多态两个要求:
1、被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
2、父类指针或者引用去调用虚函数。
3.多态的切片示意图
(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数
(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func
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(int argc, char* argv[])
{
B*p1 = new B;
//p1->test(); 这个是多态调用,下有讲解 二->6
p1->func(); //普通调用
A*p2 = new B;
p2->func(); //多态调用
return 0;
}
4.多态演示:
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;
}
};
void Pay(Person& ptr)
{
ptr.BuyTicket();
}
int main()
{
string name;
cin >> name;
Student s(name.c_str());
Pay(s);
}
买票场景下的多态 完整代码
普通人
买票时,是全价买票;
学生
买票时,是半价买票;
军人 买票时是优先买票。
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折 88 ¥" << endl; }
};
// 多态两个要求:
// 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
// 2、父类指针或者引用去调用虚函数。
//void Pay(Person* ptr)
//{
// ptr->BuyTicket();
//}
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 (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、父类指针或者引用去调用虚函数,传值调用不构成多态。
用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参
void Pay(Person* ptr) //指针调用可以
{
ptr->BuyTicket();
}
void Pay(Person& ptr) //引用调用可以
{
ptr.BuyTicket();
}
// 不能构成多态
//void Pay(Person ptr) //传值调用不可以
//{
// ptr.BuyTicket();
//}
5.虚函数重写的例外:
协变(父类与子类虚函数返回值类型不同)
子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。
协变(父类与子类虚函数返回值类型不同) 子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。
虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。
子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual
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,f依旧时虚函数,因为先继承了父类函数接口声明
// 重写父类虚函数实现
// ps:我们自己写的时候子类虚函数也写上virtual
// B& f() {
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;
}
6.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现
多态的坑题目(考接口继承)
子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual
p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1
7.析构函数名统一会被处理成destructor()
只有派生类
Student
的析构函数重写了
Person
的析构函数,下面的
delete
对象调用析构函
数,才能构成多态,才能保证
p1
和
p2
指向的对象正确的调用析构函数。
函数名处理成destructor()
才能满足多态:
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,
都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
2.注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
class Person {
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
// Person析构函数加了virtual,关系就变了
// 重定义(隐藏)关系 -> 重写(覆盖)关系
virtual ~Student() //这里virtual加不加都行
{
cout << "~Student()" << endl;
delete[] _name;
cout << "delete:" << (void*)_name << endl;
}
private:
char* _name = new char[10]{ 'j','a','c','k' };
};
int main()
{
// 对于普通对象是没有影响的
//Person p;
//Student s;
// 期望delete ptr调用析构函数是一个多态调用
// 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
Person* ptr = new Person;
delete ptr; // ptr->destructor() + operator delete(ptr)
ptr = new Student;
delete ptr; // ptr->destructor() + operator delete(ptr)
return 0;
}
8.C++11 override 和 final (1)final :修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承
(2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错
示例:如果父类没写virtual能检查出来并报错
9.重载、覆盖(重写)、隐藏(重定义)的对比
10.抽象类 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW; (1)子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。 (2)父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似 ) 另外纯虚函数更体现出了接口继承。 (3)纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数
抽象类 -- 在现实一般没有具体对应实体
不能实例化出对象
间接功能:要求子类需要重写,才能实例化出对象
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;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
三.多态的原理
1.虚函数表
和菱形虚拟继承的虚基表不一样,那个存的是偏移量
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些
平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代
表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们
接着往下分析
2.虚表存储
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
cout << sizeof(Derive) << endl;
Derive d;
Base* p = &b;
p->Func1();
p->Func3();
p = &d;
p->Func1();
p->Func3();
//
}
(1)虚函数重写/覆盖 语法与原理层解释
--语法层的概念: 派生类对继承基类虚函数实现进行了重写
--原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数
(2)虚表存储解释
多态调用和普通调用底层解释
重点总结:
多态调用:运行时决议-- 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议-- 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)
(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
cout << sizeof(Derive) << endl;
Derive d;
// 父类赋值给子类对象,也可以切片。为什么实现不了多态?
Base r1 = b;
r1.Func1();
r1.Func3();
Base r2 = d;
r2.Func1();
r2.Func3();
return 0;
}
我们发现r2没有拷贝子类d的虚表,则r2虚表中存的还是父类的虚表,调用时还是调用父类的func1,而不是子类切片后的func1
(4)静态和动态的多态(了解)


