多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态,比如我们买火车票,学生买票和军人买票和成年人买票的价格不一样这就是多态
在实现前先了解一下覆盖,// 虚函数 + 函数名/参数/返回值 相同等于 重写或者覆盖
class Person {
public:
// 虚函数
virtual void BuyTicket() { cout << "Person:买票-全价 100¥" << endl; }
protected:
};
class Student : public Person {
public:
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket() { cout << " Student:买票-半价 50 ¥" << endl; }
};
class Soldier : public Person {
public:
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket() { cout << " Soldier:优先买预留票-88折 88 ¥" << endl; }
};
void Pay(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person p;
Student s;
Soldier st;
int option = 0;
cout << "=======================================" << endl;
do
{
cout << "请选择身份:";
cout << "1、普通人 2、学生 3、军人" << endl;
cin >> option;
switch (option)
{
case 1:
{
p.BuyTicket();
break;
}
case 2:
{
s.BuyTicket();
break;
}
case 3:
{
st.BuyTicket();
break;
}
default:
cout << "输入错误,请重新输入" << endl;
break;
}
cout << "=======================================" << endl;
} while (option != -1);
return 0;
}
运行结果
但是这个没有实名制,就是名字,身份证之类的都没有
再写之前了解下多态
多态两个要求:
1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
2、父类指针或者引用去调用虚函数。
#include
using namespace std;
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折 88 ¥" << endl; }
};
void Pay(Person& ptr)
{
ptr.BuyTicket();
}
int main()
{
int option = 0;
cout << "=======================================" << endl;
do
{
cout << "请选择身份:";
cout << "1、普通人 2、学生 3、军人" << endl;
cin >> option;
cout << "请输入身份" << endl;
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 sr(name.c_str());
Pay(sr);
break;
}
default:
cout << "输入错误,请重新输入" << endl;
break;
}
cout << "=======================================" << endl;
} while (option != -1);
return 0;
}
运行结果
至于不满足多态或者虚函数的错误案例我就不发出来了,你们可以自己改下,比如把pay的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,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;
}
运行结果
了解下虚函数我们来做下题
上面的题答案是b,为什么?
首先先到p->test()明明是B子类为什么能调用父类A的test,因为是继承子类继承了父类的test,但是继承过来的test里面的this是A*我们要把地址传过去,这里面就涉及了子类和父类的转换,而这里test里面this指针是B的指针,而它又刚好符合多态的二个条件,所以调用B的func,可能有人讲他没virtual啊,但是我们上面讲了子类继承父类也会把它的虚函数给继承下来,而缺省值继承的也是父类的,所以打印的是B->1
总结:
子类继承重写父函数
1.接口继承(B不写virtual还是虚函数,符合多态条件,而且缺省值用的是父类的)
2.重写的函数实现
但是哦来看下一道题,和上面一样但是调用改了而已
这道题是b->1还是b->0?
是b->0为什么?因为这里是普通的调用,不是通过多态来调用的,所以没出发条件
看下面代码
class Person {
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
// Person析构函数加了virtual,关系就变了
// 重定义(隐藏)关系 -> 重写(覆盖)关系
~Student()
{
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;
}
打印结果
调用了一个person和一个student,析构的时候没有调用student,存在内存泄漏
解决办法也会简单只要里面是多态就可以解决了,在父类加上virtual就变成了多态,建议子类也加上,虽然子类会继承父类的虚函数
下面会打印什么?
按我们之前学的以为是8
结果
16为什么是16,因为是虚函数虚函数会出现一个虚函数指针,所以是32位下4+4+1对齐=12,64位下8+4对齐=16
final: 用来禁止重写的关键字
final也能写在类后面,但是写在类后面就代表Car不能当父类
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
// 抽象类 -- 在现实一般没有具体对应实体
// 不能实例化出对象
// 间接功能:要求子类需要重写,才能实例化出对象
class Car
{
public:
// 纯虚函数
virtual void Drive() = 0;
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
Car c;
BMW b;
return 0;
}
运行结果
重写可以调用子类
不重写会报错
我们上面讲了买票系统模拟实现这个节点最后讲了虚函数后面有个虚函数指针
代码
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;
return 0;
}
通过上面代码我们可以知道Base b是Dereive d的父类
而他们构成了重定义,所以它们有虚函数指针,看下面调试的图片
可以看到b和d的区别是在Func1不一样,为什么不一样
因为重写了Func1
总结:
虚函数重写 -语法层-子类对父类虚函数实现进行重写
虚函数重写 -原理层-子类的虚表拷贝了父类的虚表进行了修改,重写覆盖那个虚函数
这是怎么实现调用的,其实他这个调用看的不是类型,而是地址,
多态调用:运行时决议,去指向的对象的虚表中查函数地址
普通调用:编译试决议,编译时确定函数地址
证明
可以看到上面Func1有子类的打印,但是Func3没有,因为Func3不是虚函数,没有虚表所以不打印
因为编译时,就决定了对象实现不了多态,不满足要求,要是强行让对象能实现多态就会出现下面的情况
对象切片的时候就会调用构造函数,子类只会拷贝父类成员,不会拷贝虚表指针,要是拷贝了虚表指针就乱套了,为什么?因为这个时候父类的虚表指针到底是父类的还是子类的根本不清楚,而指针和引用他们时怎么实现多态的,它们都有虚表指针的地址,当调用那个多态的虚表指针,他们就调用什么的虚表指针。毕竟传什么地址就用什么地址
下面是从反汇编里面代表显示的时多态查询虚表的调用
下面时没构成多态的反汇编代码
西面Func4到底有没有没有在虚表里面,监视窗口也没有看到有Func4的虚表
答案是有的,因为监视窗口被编译器处理过过看的东西不是很准,真正能看情况的是内存窗口
我大概猜就在下面,但是这也只是猜不一定正确,所以我们写函数来验证验证
代码
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;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
// 取内存值,打印并调用,确认是否是func4
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)
{
printf("[%d]:%p->", i, a[i]);
V_FUNC f = a[i];//函数地址直接调用了函数
f();
}
}
int c = 2;
int main()
{
Derive d;
PrintVFTable((V_FUNC*)(*((int*)&d)));
return 0;
}
打印结果和监视窗口的地址一模一样,表示我们写的代表没错,并监视窗口没有给你显示没构成多态的func4,但func4的虚表地址是存在的,不过上面代码PrintVFTable((V_FUNC*)(((int)&d)));如果是32位就用4字节的,如果是64位就用8字节的类型
首先我们先了解,虚表,一个类型,一个虚表,所以这个类型对象都存这个虚表指针
因为这样,栈是不可能存放的,因为栈帧一出作用域就销毁了,到时还怎么找虚表指针
堆也不可能,堆开辟空间可以,但是释放怎么办?
所以只可以在静态区或者常量区里面,因为他们整个运行时都在,但是我觉得常量区更符合一点,因为不用改变虚表,但是多说不用用程序来确认吧
可以看到是在常量区