当前栏目
【C++】多态
目录
1.概念和理解
多态:多种形态,完成某种行为时,不同的对象会产生的不同状态
多态一定是继承关系
- 多态的构成条件
首先介绍虚函数的概念
关键字virtual,之前学过,他可以修饰继承方式,表示虚继承,在菱形继承的情形下,虚继承的数据存储方式和对应的虚基表可以很好解决代码冗余和二义性的问题
这里的虚函数长这样
虚函数重写/覆盖,必须满足三同:函数名,参数,返回值
这两个虚函数构成重写关系
- 多态的条件
1. 虚函数的重写
1.1父类必须加virtual,但是子类可以不用
1.2协变:虽然构成虚函数重写,但是两个函数可以返回值不同,但必须是子/父类的指针/引用,不一定是自己的父/子的指针/引用,用别的类的也可以
2.父类指针或引用去调用虚函数
- 多态调用 (运行时/动态 决议/绑定)
指针/引用与指向的对象有关
class Person
{
public:
virtual void func()
{
cout << "Person::全价" << endl;
}
};
class Student :public Person
{
public:
virtual void func()
{
cout << "Student::半价" << endl;
}
};
class Solder:public Person
{
public:
void func()
{
cout << "Solder::免费" << endl;
}
};
void Fun(Person& p)
{
p.func();
}
int main()
{
Student s;
Person p;
Fun(p);
Fun(s);
}
对于买票的动作,三种对象有三种行为,现在Fun函数的参数是父类指针,该指针调用虚函数,此时满足子类和父类的同名函数func构成虚函数重写,满足多态调用
运行结果如下
根据多态的条件,可以省去子类的virtual,也可以写成协变
运行结果还是不变,两个同名函数依然构成虚函数重写
class Person
{
public:
virtual Person* func()
{
cout << "Person::全价" << endl;
return this;
}
};
class Student :public Person
{
public:
virtual Student * func()
{
cout << "Student::半价" << endl;
return this;
}
};
void Fun(Person& p)
{
p.func();
}
int main()
{
Student s;
Person p;
Fun(p);
Fun(s);
}
当然不是自己的父/子类也可以
运行结果一样的
class A{};
class B:public A {};
class Person
{
public:
virtual A* func()
{
cout << "Person::全价" << endl;
return (A*)this;
}
};
class Student :public Person
{
public:
virtual B* func()
{
cout << "Student::半价" << endl;
return (B*)this;
}
};
void Fun(Person& p)
{
p.func();
}
int main()
{
Person p;
Student s;
Fun(p);
Fun(s);
}
- 普通调用 (编译时/静态 决议/绑定)
不满足多态条件时的调用就是普通调用
与调用的对象类型有关
class Person
{
public:
void func()
{
cout << "Person::全价" << endl;
}
};
class Student :public Person
{
public:
virtual void func()
{
cout << "Student::半价" << endl;
}
};
void Fun(Person& p)
{
p.func();
}
int main()
{
Student s;
Person p;
Fun(p);
Fun(s);
}
只有子类同名成员函数加上了virtual ,不构成虚函数重写,不满足多态的条件,此次调用为普通调用,与调用对象的类型有关,调用对象都是Person类型 所以 结果如下
2.多态的析构函数
class Person
{
public:
void func()
{
cout << "Person::全价" << endl;
}
~Person()
{
cout << "~Person" << endl;
}
};
class Student :public Person
{
public:
virtual void func()
{
cout << "Student::半价" << endl;
}
~Student()
{
cout << "~Student" << endl;
}
private:
int a[10];
};
int main()
{
// Fun(p);
// Fun(s);
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
}
这两个类内的析构函数不加virtual,不构成多态
delete的原理是使用指针调用析构函数
所以ptr1和ptr2是普通调用,这时候运行结果是
因为普通调用,与调用对象类型有关,所以都是Person*指向Person,但是这会造成Student里面的int _a[10]内存泄漏
想调用到子类的析构函数怎么办?多态调用
把父类析构函数加上virtual,两个析构函数的名字会被编译器处理成同一个名字——destructor,子类加不加virtual都行
代码是这样的
class Person
{
public:
virtual ~Person()
{
cout << "~Person" << endl;
}
};
class Student :public Person
{
public:
~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
}
结果
小总结:实现父类的时候可以无脑给析构加上virtual
3.关键字final override
- final修饰类(C++11),让类无法被继承
- final修饰虚函数,让其不能被重写
如何构造一个私有类?
1.构造私有 但是把构造函数也写成私有的根本new不动,所以这个方法不可行
2.类定义的时候加final(父类加上final)
final修饰的虚函数不能被重写
关键字:override
- 放在子类虚函数之后,检查虚函数是否重写
可以用在代码很多的时候,如果忘记了是否重写,用override检查一下
如果重写了,会正常运行,否则,直接报错
class A
{
virtual void Fun1()
{
cout << "A" << endl;
}
};
class B :public A
{
virtual void Fun2() override
{
cout << "B" << endl;
}
};
int main()
{
A a;
}
4.抽象类
在虚函数后面加上=0,叫做纯虚函数
包含纯虚函数的类叫做抽象类
抽象类不能实例化出对象
助记:
抽象:不现实 ,所以抽象类不能具体的表示出来,即 不能实例化出对象
- 应用
可以用在一个泛泛的类,比如一个叫car的类,很多车的品牌可以继承car类,变成很多名车,但是没有一个汽车就叫car品牌,所以car不需要实例化出对象,可以搞成抽象类
抽象类的子类仍无法实例化
但是子类对虚函数重写就能实例化,加不加virtual都可以
5.接口继承和实现继承
我们写的普通继承都是实现继承,虚函数重写是接口继承
6.重写/覆盖,重定义/隐藏,重载的对比
- 作用域角度
重载:在同一作用域中
重写:在基类和派生类的独立作用域中
重定义: 在基类和派生类的独立作用域中
- 条件
重载:函数名相同
(虚函数)重写:三同(函数名,参数,返回值),返回值可以不同,但必须是子/父类关系的指针/引用,可以不是自己的子/父类
重定义: 函数名相同
ps:在基类和派生类的独立作用域中,两个同名函数不是重写就是重定义
7.多态底层深挖
下面代码的输出结果是?
4!这也太简单了
那你就错了~答案是8
通过调试,发现b对象中还有一个指针(32位下4字节),难怪答案是8,一般_vfptr放在成员变量前,有些平台也放在成员变量后,根据平台不同不一样
其实这个指针是虚函数表指针,指向一个虚函数表,含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
那么虚函数表里面有什么?
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;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
看调试
先看b
再看d
发现如下问题
- 虚函数指针地址变成了d对象的地址 ,并且没有重新生成虚函数指针
说明继承之后 虚函数指针改变了,变成指向d,并且虚表被继承、
- 虚函数表里面Func1(被重写的函数)地址改变
这时候的重写,说成覆盖更合适,完全把原来Func1的地址覆盖,现在通过虚表只能找到重写过的虚函数
- 虚表里面Func2(没被重写的函数)地址不变
说明没有被重写的虚函数地址直接继承
(此时我们对重写的定义应更深刻!!!!!!!!!!!!!!!!!!)
- Func3并不在b/d虚表里
因为虚表里只放虚函数地址,Func3不是虚函数
总结:虚函数表无论有几个虚函数,都只有一个指针,只不过继承之后表里面的函数变多,表示函数指针数组 ,里面放着虚函数的地址
现在深刻认识一下下面代码
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Func(p);
Student s;
Func(s);
return 0;
}
首先对于p,他有自己的虚函数指针,指向一张虚函数表,表里面是买票函数地址
通过这个地址,我们找到Person类的买票函数,调用
符合多态调用条件,走到Func(s),虚表里面买票函数的地址已经被重写,变成Student类的买票函数地址,自然可以通过这个地址,调用Student类的买票函数
反过来思考我们要达到多态,为什么有两个条件:-个是虚函数覆盖,一个是对象的指针或引用调
用虚函数
虚函数覆盖,保证子类通过虚表可以找到重写的虚函数
对象指针/引用调用:切片,让虚表指针可以找到子类
下面从反汇编的角度 理解一下多态调用是在运行时完成的
p中存的是mike对象的指针,将p移动到eax中
[eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
[edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
call eax中存虚函数的指针,这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
把代码变成普通调用之后
这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
现在,我们大可以猜测,这个虚函数表放在代码段上!
因为这个表是一脉继承的,并不会每次重写
现在用一个简单代码测试
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;
}
private:
int _d = 2;
};
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int* p1 = new int;
cout << "堆:" << p1 << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
static int b = 0;
cout << "静态区/数据段:" << &b << endl;
Base be;
cout << "虚表:" << (void*)*((int*)&be) << endl;
Base* ptr1 = &be;
int* ptr2 = (int*)ptr1;
cout << "虚表:" << (void*)*((int*)&be) << endl;
Derive de;
cout << "虚表:" << (void*)*((int*)&de) << endl;
Base b1;
Base b2;
return 0;
}
所以最接近的就是代码段
相关文章
- centos cpu load过高排查方法
- 用好这个任务管理工具,轻松躲避职场明枪暗箭
- 防火墙功能(锐捷安全篇)
- 二分法
- [SCOI2005] 栅栏【题解】
- 简单排序
- springboot通过Referer防止跨站点请求伪造
- 洛谷 P6580 [Ynoi 2019] 美好的每一天~ 不连续的存在 题解
- 无线投屏(智慧教室)
- 二分的边界问题
- DOM 之 Node和Element的区别
- JNPF实操│来,一起体验一流程多表单到底有多便捷
- 前端炫酷特效合集
- 网络的相关概念
- windows10 netsh wlan命令连接新wifi
- 北航计算机网络实验复习——设计性实验汇总
- Blazor和Vue对比学习(进阶.请求WebAPI):通讯协议和HTTP协议
- 超级好看的 Edge 浏览器新标签页插件:好用、好看、免费浏览器必备
- 智慧树视频课件课程下载工具,如何在电脑端下载智慧树视频课件PDF,PPT到本地
- 重学c#系列——linq(4) [三十]