目录
7.析构函数的重写-析构函数名统一会被处理成destructor()
(5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到
一.多态的概念
具体点就是去完成某个行为,当不同的对象去完成时会
。
买票这个行为
,当
普通人
买票时,是全价买票;
学生
买票时,是半价买票;
军人
最近为了
争夺在线支付市场
,支付宝年底经常会做诱人的
扫红包
–
支付
–
给奖励金
的
8
块、
10
块
…
,而有人扫的红包都是
1
毛,
5
….
。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
=
;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
= random()%1
;总结一下:
同样是扫码动作,不同的用户扫
ps
:支付宝红包问题纯属瞎编,大家仅供娱乐。
Student
继承了 Person。
Person
对象买票全价,
Student
对象买票半价。
二.多态的定义及实现
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;
}
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.接口继承和实现继承
多态的坑题目(考接口继承)
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
指向的对象正确的调用析构函数。
才能满足多态:
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;
}
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
:修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承
(2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错
override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如 果重写成功,编译 通过,否则,编译失败,因此 override作用发生在编译时。
override只能修饰子类的虚函数
override修饰子类成员函数虚函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写。(在子类里面是可以自己增加 成员函数的,如果这个成员函数不是虚函数,就不可以进行修饰)
示例:如果父类没写virtual能检查出来并报错
9.重载、覆盖(重写)、隐藏(重定义)的对比
函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。
重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。
10.抽象类
)
抽象类 -- 在现实一般没有具体对应实体
不能实例化出对象
间接功能:要求子类需要重写,才能实例化出对象
class Car
{
public:
virtual void Drive() = 0;
// // 实现没有价值,因为没有对象会调用他
// /*virtual void Drive() = 0
// {
// cout << " Drive()" << endl;
// }*/
};
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.虚函数介绍
被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
2.虚函数表
和菱形虚拟继承的虚基表不一样,那个存的是偏移量
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
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* p = &b;
p->Func1();
p->Func3();
p = &d;
p->Func1();
p->Func3();
// /*Base& r1 = b; 引用也是多态调用
// r1.Func1();
// r1.Func3();
//
// Base& r2 = d;
// r2.Func1();
// r2.Func3();*/
}
(1)虚函数重写/覆盖 语法与原理层解释
–语法层的概念: 派生类对继承基类虚函数实现进行了重写
–原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数
(2)虚表存储解释
无论是子类还是父类中只要有虚函数都会多存一个指针,这个指针叫虚表指针,他指向一个指针数组,指针数组中存着各个虚函数的地址。
Func1是重写的函数,Base[0]中存的地址并非真正的Derive中Func1的地址,而是通过call这个地址,找到这个地址的内容,这个地址的内容指令又是jump到地址2,地址2存的才是真正的Derive中Func1的地址
多态调用和普通调用底层解释(编译时多态/运行时多态)
C++语言的多态性分为编译时的多态性和运行时的多态性
①运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。
②编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载和模板实现。
-在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
void Buy() { cout << "Person::Buy()" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
void Buy() { cout << "Student::Buy()" << endl; }
};
void Func1(Person* p)
{
跟对象有关,指向谁调用谁 -- 运行时确定函数地址
p->BuyTicket();
跟类型有关,p类型是谁,调用就是谁的虚函数 -- 编译时确定函数地址
p->Buy();
}
int main()
{
Person p;
Student s;
Func1(&p);
Func1(&s);
return 0;
}
重点总结:
多态调用:运行时决议– 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议– 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)
(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)静态和动态的多态(了解)
(5)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到
class Derive : public Base
{
public:
// 重写
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
// 取内存值,打印并调用,确认是否是func4
//typedef void(*)() V_FUNC; // 不支持这种写法
typedef void(*V_FUNC)(); // 只能这样定义函数指针
// 打印虚表
//void PrintVFTable(V_FUNC a[])
void PrintVFTable(V_FUNC* a)
{
printf("vfptr:%p\n", a);
for (size_t i = 0; a[i] != nullptr; ++i) //VS下的虚表以空指针结束
{
printf("[%d]:%p->", i, a[i]); //打印虚表中的所有函数的地址
V_FUNC f = a[i]; //调用函数中打印函数,可以知道是哪个func函数
f();
}
}
int c = 2;
int main()
{
Base b;
Derive d;
PrintVFTable((V_FUNC*)(*((int*)&d))); //下有解释
}
PrintVFTable((V_FUNC*)(*((int*)&d))); 解释:
因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参
监视窗口和内存窗口:
(6)同一类型对象,共用一个虚表
一个类型公共一个虚表,所有这个类型对象都存这个虚表指针
(7)虚表存在常量区/代码段
不可能存在栈,栈区是建立栈帧,出作用域栈帧销毁, 虚表是一个永久的存在,排除栈。
不可能存在堆区,堆区是动态申请,最后动态释放
可以存在静态区或者常量区,最可能存在常量区。通过下面打印地址可见虚表存储地址离常量区地址最近
4.多继承,虚表的存储(一个子类继承两个父亲时)
大体的结论就是:func1是重写的函数,在子类的两个父类的虚表中存储的func1地址不相同,但是通过一系列的call这个地址,这个地址的内容又是jump到另一个指令,最终都会跳到子类重写的func1地址上
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
printf("%p\n", &Derive::func1);
Derive d;
//PrintVTable((VFPTR*)(*(int*)&d));
PrintVTable((VFPTR*)(*(int*)&d));
PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
}
PrintVTable((VFPTR*)(*(int*)&d));
因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参
PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )
结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值
四.虚函数使用规则
(1)虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上
(2)静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
(3)友元函数不属于成员函数,不能成为虚函数
(4)静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数)
(5)析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)
4. inline函数可以是虚函数吗?
虚函数表指针
是在
构造函数初始化列表阶段才初始化的
。虚函数的意义是多态,多态调用时到虚函数表中去找,构造函数之前还没初始化,如何去找?
虚函数表是在编译阶段就生成的
,一般情况下存在代码段(常量区)的。(
虚函数表指针初始化是指把虚函数表的指针放到对象中去,但生成仍是在编译阶段
)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/38970.html