多态的概念:
- 多态(polymorphism)的概念:通俗来说,就是多种形态。
- 多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。
- 编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
- 运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就''(>^ω^<)喵'',传狗对象过去,就是"汪汪''。
以下主要是以运行时多态为主:
多态的定义与实现:
多态的构成条件:
实现多态的两个必要条件:
- 必须是指针或者引用来调用虚函数;
- 被调用的函数必须是虚函数;
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。
虚函数:
在认识运行时多态时,我们首先要先认识什么是虚函数:
class Person
{
public:
virtual void BuyTicket()//在成员函数前加virtual构成虚函数,注意是在成员函数
{
cout << "买票-全价" << endl;
}
};
虚函数的重写/覆盖:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
动物叫声:
class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}
下面题目是对以上注意事项的考察:
以下程序输出结果是什么()A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
#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;
}
题目解析:
- B继承了A;
- B当中的func有没有重写A当中的func?
虽然B的func函数没有+virtual,但是满足三同:返回值类型,函数名字,参数列表(指的是形参类型),缺省值不重要,构成重写,因此,满足重写;
- 虚函数可以重写也可以不重写,因此A的test虚函数被B继承下来了;
所以new的B对象,p指针可以去调用test(走的是继承)
- test的this需要传一个对象,这个this是A*还是B*?
这时候有两种方向的思考:
第一种:test在A里边,那么this就是A*!!!
第二种:这里的调用不是A对象的指针在调用,是B对象的指针在调用,test又被B继承下来了,那么this应该是B*!!!
虽然test被B继承下来,但是this还是A*,继承是一个形象的说法。编译器真的会在继承时,把A的成员函数,成员变量,不是重写的这种都会在B里拷贝一份吗?这显然是不会的。继承下来的的是搜索规则,从隐藏上讲,在不构成重写的时候,会在B里边搜索有没有test,没有的话再去A里搜索,所以说继承并不会真的将test弄下来,他只是一个形象的说法
- 接着this调用func(),也就是this->func(),那么构成多态吗?
是构成的。
第一:this是A的指针,是基类的指针去调用func();
第二:满足虚函数的重写(三同);
- 因为满足多态,func被B的指针对象调用,调用B的虚函数;
- 但是,答案并不是D
对于重写,对于B重写A的虚函数来说,对于派生类B来说,重写可以理解为重写的本质是重写实现,这也是为什么重写的虚函数前可以不写virtual,也就是派生类B的重写的虚函数func()是由基类A的声明和B重写虚函数的内容构成的:
- 也就是绝不重新定义继承而来的缺省参数值
在编程中,如果你在子类中重写了一个继承自父类的方法,并且这个方法有参数,那么在子类中重写的方法不应该改变父类方法的默认参数值。这是因为默认参数值是在函数定义时确定的,如果在子类中改变默认参数值,可能会导致调用时的行为与预期不符。
扩展题:
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对
#include<iostream>
using namespace std;
class A
{
public:
A() :m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << " "; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A
{
public:
B() { test(); }
virtual void func()
{
++m_iVal;
std::cout << m_iVal << " ";
}
};
int main(int argc, char* argv[])
{
A* p = new B;
p->test();
return 0;
}
分析:new B时先调用父类A的构造函数,执行test()函数,在调用func()函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0,构造完父类后执行子类构造函数,又调用test函数,然后又执行func(),由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1, 最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2, 所以答案为C 0 1 2
协变:
class A {};
class B : public A {};//继承(这是必须的,因为要构成多态的形式需要以继承形式)
class Person {
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "买票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
析构函数的重写:
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
A* p1 = new A;//指向父类
A* p2 = new B;//指向子类
delete p1;
delete p2;
return 0;
}
这里的带来的问题原因是:一个父类的指针有可能指向父类也有可能指向子类
在对于释放操作时,delete p1的时候没有问题,因为调用的是父类的析构函数;
在delete p2时,我们期望的是调用子类的析构函数,假设我们A的析构函数没有virtual修饰,也就是A与B的析构函数不构成多态,这时候,delete p2会因为p2是A*类型的指针而去调用A的析构,就会导致B类当中的动态开辟的_p数组没有被释放,导致内存泄漏;
当A与B的析构函数满足多态的条件,就可以实现指向谁去析构谁;
据上图:其实也是不需要显示调用父类的析构的,因为他调不到,运行时会跳过
override和final关键字:
override的使用:
// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
class Car {
public:
virtual void Dirve()
{}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
final的使用:
// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
类比:一个类用final修饰就是最终类,无法被继承,以上是为了不能被重写
重载/重写(覆盖)/隐藏(重定义)的对比:
纯虚函数和抽象类:
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;
}
};
int main()
{
// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
Car car;
//当然也可以是构成多态
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
多态的原理:
虚函数表指针:(虚表指针)
以下是一个小程序,我们可以通过调试窗口来进行观察理解:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
所以,以上代码的类的大小应该为12
多态的实现:
我们可以使用更加丰富的代码来理解:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
protected:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
protected:
int _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
通过代码的调试窗口观察:我们可以每个对象里边都有一个虚函数表指针:
说明:Person有自己的虚函数表指针,Student继承了Person,也有了自己的虚函数表指针,Soldier继承了Person,也有了自己的虚函数表指针,子类就是重写了父类的虚函数表,这是重写的其中意义
那我们是如何实现指向谁调用谁的呢?
所有的代码语句都会转化成汇编指令,编译在检查语法时,会先判断该程序代码满不满足多态的要求,如果满足多态,就在这段指令的时候,就把这段指令变成从虚函数表指针指向的虚函数表中去找对应虚函数的地址,从而找到对应的虚函数;
然而继承关系是具有切片行为的,所以ptr无论是指向Person·Student·Soldier哪种场景,从内存的角度,都是看到的是对应Student·Soldier切片出的Person对象,所以无论是哪种场景,只管是去找对应的Person对象,去找指向虚函数表的指针,找到对应的虚函数表里找到虚函数指针调用;
总之:指向谁,调用谁:指向哪个对象,运行时,到指向对象的虚函数表中找到对应虚函数的地址,进行调用。
我们可以从汇编角度来观察满足与不满足多态的汇编语言的区别:
可以看出,如果不满足多态,就跟指向的对象没有关系,这时,ptr只是单单的Person指针,调用Person的BuyTicket()函数,不管ptr指向谁,都只会调用Person的函数,这是在汇编时就已经确定了;
而满足多态, 中间的代码指令我们可以理解为是指针指向虚函数表,找到虚函数的地址,将虚函数的地址给eax,在运行的时候再去调用虚函数,这是在运行时确定的;
动态绑定与静态绑定:
(根据上图)
- 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
- 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax, dword ptr[ptr]
00EF2004 mov edx, dword ptr[eax]
00EF2006 mov esi, esp
00EF2008 mov ecx, dword ptr[ptr]
00EF200B mov eax, dword ptr[edx]
00EF200D call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx, dword ptr[ptr]
00EA2C94 call Student::Student(0EA153Ch)
虚函数表:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base
{
public:
// 重写基类的func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
- 基类对象的虚函数表中存放基类所有虚函数的地址。
- 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
- 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
- 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分。
- 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
- 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
- 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}