目录
一、什么是多态
多态的两个条件?
练习
?二、多态的原理
一、什么是多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比方说去买票:普通成年人买普通的票,学生买半价的学生票,军人不需要排队买票
这里的普通人,学生和军人就是三个不同类型的对象。
①正常卖票 ? ? ? ?②半价买票 ? ? ? ?③优先买票
还有比方说红包
新用户要鼓励使用 所以红包多给一点 rand()%99
老用户并且经常使用?????????????????????????????rand()%2
老用户且不经常使用?????????????????????????????rand()%66
如何用代码来实现多态?
多态的实现条件是虚函数!(这个virtual是虚函数,不是上一节的虚拟继承!!)
只用成员变量才能添加virtual,变成虚函数。
在继承的时候,三同(函数名、参数、返回值)都相同的时候,就构成了虚函数重写(覆盖条件)
不符合重写,就是隐藏关系
多态的两个条件?
多态的两个条件
1、虚函数的重写
2、父类指针或者引用去调用虚函数
#include <iostream>
using namespace std;
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
class Student : public Person {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "优先-买票" << endl; }
};
void Func(Person &P)
{
P.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
?这里我们发现只要传入的指针或引用指向的对象不同,调用的函数和方法也不同!!
1、不是父类的指针或者引用
如果我们改成这样,也就是没有使用父类指针或者引用去调用函数,我们就发现我们的多态效果就失效了。
void Func(Person P)
{
P.BuyTicket();
}
2.不符合重写 --(不是虚函数)
我们将父类的virtual,让其不是虚函数,破坏了我们上面的条件,多态就失效了
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
void BuyTicket() { cout << "买票-全价" << endl;}
};
特例:子类的方法的virtual去掉了还是虚函数,因为默认是将父类的虚函数继承下来了。所以子类的函数不加virtual依旧构成重写。(实际中最好还是加上)
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
};
2.不符合重写--参数不同(三同中去掉一个)
#include <iostream>
using namespace std;
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual void BuyTicket(int) { cout << "买票-全价" << endl;}
};
class Student : public Person {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual void BuyTicket(char) { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
virtual void BuyTicket(long) { cout << "优先-买票" << endl; }
};
void Func(Person &P)
{
P.BuyTicket(1);
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
?特例:重写的协变。返回值可以不同,要求必须是父子关系的指针或者引用,这样也是多态,是可以的!!!!。
#include <iostream>
using namespace std;
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual Person* BuyTicket() {
cout << "买票-全价" << endl;
return this;
}
};
class Student : public Person {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual Student* BuyTicket() {
cout << "买票-半价" << endl;
return this;
}
};
void Func(Person &P)
{
P.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
只要是父子指针或者引用就可以,不一定是要当前类的父类和子类!,比方说下面我们的返回值就是A和B的指针也是可以构成多态的!
#include <iostream>
using namespace std;
class A {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual A* BuyTicket() {
cout << "买票-全价" << endl;
return this;
}
};
class B : public A {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual B* BuyTicket() {
cout << "买票-半价" << endl;
return this;
}
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual A* BuyTicket() {
cout << "买票-全价" << endl;
A a;
return &a;
}
};
class Student : public Person {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual B* BuyTicket() {
cout << "买票-半价" << endl;
B b;
return &b;
}
};
void Func(Person &P)
{
P.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
如果不是父子关系的指针或者引用就会报错
同时子类和父类的指针不能颠倒!!
#include <iostream>
using namespace std;
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual Person BuyTicket() {
cout << "买票-全价" << endl;
return *this;
}
};
class Student : public Person {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual Student BuyTicket() {
cout << "买票-半价" << endl;
return *this;
}
};
void Func(Person &P)
{
P.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
练习
以下程序输出结果是什么()
#include <iostream>
using namespace std;
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*p = new B;
p->test();
return 0;
}
?A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
p->test()子类的指针调用test(),多态的条件没有一个是满足的,所以将虚函数当初普通函数处理(虚函数和普通函数也没啥区别,就是虚函数能完成多态的处理)
调用test,需要传参,test()(这里我们的test是无参的,所以就是将自身的this指针传进去,也就是一个A*this贺子珍)需要将A*this传递进去,调用func();是this指针调用func()(this->func());符合多态调用(父子指针)。
这个this指向谁就调用谁。而A*的后面这个this是指向子类的,因为我们的p就是指向子类的,所以调用子类的func函数。
但是这道题的子类B中虽然并没有加virtual,但是按照我们上面的说法,这个子类依旧构成重写,是我们上面讲过的一个特例,所以调用的依旧是我们B中的func(),也就是B->
然而虚函数重写是接口继承,重写实现
普通函数继承是实现继承。
这里的父类就像是一个接口,就是我们子类将父类的接口给拿过来。
缺省参数一不一样是不会影响我们的继承的,但是这里的传入的类型不写是不行的。
所以我们的接口是这一段,所以默认的缺省参数也就是1!!!
(这里的p传给A*this的时候会发生切片,将B中A的部分切割出来)
B
?(不推荐写这样的代码!!!!)
?正常的话应该改成这样,这样的结果还是B->1
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func(1);}
};
class B : public A
{
public:
void func(int val){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
那如果我们将指针p改成A类型的指针,结果会是什么?
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}
void test(){ func(1);}
};
class B : public A
{
public:
void func(int val){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
A*p = new B;
p->test();
return 0;
}
test()处的this指针还是A* this,这里的this指针也就是p,变成了父类的指针,也就是this->func(1)
多态的两个条件,
①父类指针或者引用调用虚函数(这里的this指针是父类的指针)
②func是虚函数的重写,这里就是多态调用,跟this的类型根本没有关系)指向谁调用谁,这个父类的指针p所指向的对象是子类!!!
所以我们看的是指针所指向的对象是不是子类和父类的关系,并不是指针的类型是父类的指针还是子类的指针!!!?
?
?这样写调用的才是父类
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}
void test(){ func(1);}
};
class B : public A
{
public:
void func(int val){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
A*p = new A;
p->test();
return 0;
}
?
?二、多态的原理
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout<<"Func1()"<<endl;
}
private:
int _b=1;
char _ch='A';
};
int main()
{
cout<<sizeof (Base)<<endl;
}
?这里的Base中会多存一个指针,称为虚表指针!虚函数是会进入虚表的。
虚函数是会把它的地址放入虚表的 virtual function table
所以我们这里int为四个字节,char为一个字节,这样就变成了我们的第一个8字节对齐,然后再加上一个虚表指针,就变成了16字节。
对象里面没有虚表,有的是虚表的指针
虚表的本质上是一个数组,是一个函数指针的数组。
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 Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。 2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。 3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。 4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。?
多态调用
p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,
// 不是在编译时确定的,是运行起来以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
普通调用
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,
// 所以这里是普通函数的调用转换成地址时,
// 是在编译时已经从符号表确认了函数的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
总结:多态的本质原理,符合多态的两个条件
那么调用时,会到指向对象的虚表中找到对应的虚函数地址,进行调用。
多态调用,程序运行时去指向对象的虚表中找到函数的地址,进行调用。
普通函数的调用,编译链接时确定函数的地址,运行时直接调用。
|