c++进阶--多态
目录
编辑
1 多态的概念
2 形成多态的条件定义
条件:
虚函数
一个bt的面试题
虚函数的一些问题:
协变:
析构函数的重写
为什么会这样设计:(容易考)
3 两个关键字
final
override
4 重载/重写/隐藏的对⽐
5 纯虚数和抽象类
6 多态的原理
虚函数表指针
如何实现的多态
动态绑定和静态绑定
1 多态的概念
多态的概念:简单来讲就是我们的多种形态。 多态又分为我们的静态多态(编译时多态)和动态多态(运⾏时多 态)。
静态多态包括我们的模版和重载,他们传不同类型的参数就可以调⽤不同的 函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种 形态。 我们今天主要讲的是动态多态(运行时的多态)。
比如我们去进行买票的时候,学生会有学生票,儿童会有儿童票,成人会有成人票。
2 形成多态的条件定义
条件:
- 发生在继承关系中,不是继承关系,即使满足下面两个条件也不成立。
- 必须要是父类的指针或者引用。
- 子类必须重写/覆盖我们的父类的虚函数。
重写/覆盖条件:三同:参数、函数名和返回值相同。
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣ 类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多 态的不同形态效果才能达到。
虚函数
定义:类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
例子:
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
虚函数的覆盖/重写:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值 类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数
我们在构成重写时: 我们的子类可以不写virtual(但是不建议这么写)(容易埋坑),但是我们的父类是不可以不写的。
原因:关于我们的多态的重写,本质是重写我们的父类的实现(即函数体),函数的名字和返回值是从父类继承下来的,是父类的。也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性。在我们的一本名为effective c++这本书中有一节就是关于我们的我们的这个问题的,绝不重新定义继承而来的缺省值。
一个bt的面试题
以下程序输出结果是什么()
A: A->0 B: B->1 C:A->1 D: B->0 E: 编译出错 F:以上都不正确
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)
分析:首先我们先来判断一下是否构成多态:(1)我们的B中的func重写了我们的A中的func。(参数相同是指类型,名称无所谓,缺省值也不重要)
(2)我们的main()函数里面,我们的B调用我们的test()(走的是继承)。这里的test()函数里面是A*。:我们的这里的test继承下来是一种形象的说法,编译器不会将我们继承下来的东西在我们的派生类中写一遍,而是在搜索的时候是会在我们的B中先搜索,如果B中有的话就就不会到A中搜索,B中没有就会在我们的A中寻找。并不会真正意义上的把他写在我们的派生类中。所以这里是A*,test(A*)。
多态的条件满足构成多态。
然后,我们这里是指向的是B,所以调用的是我们的B中的func,但是我们的虚函数重写本质是重写我们的函数体,绝不重新定义继承而来的缺省值。所以我们的这里调用的func 函数原型是:
void func(int val=1){ std::cout<<"B->"<< val <<std::endl; }。这种情况只发生在我们的多态调用里面。
int main()
{B *p =new B;p->func();return 0;
}
这种就是一个普通调用。不是父类的指针或者引用所以不满足我们的多态的条件。
虚函数的一些问题:
协变:
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引 ⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。(可以是自己的类也可以是其他的类,但是得是继承关系)
例子:
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;
}
析构函数的重写
基类的析构函数建议为虚函数(容易考),此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写。
为什么会这样设计:(容易考)
下面这种情况
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;
}
当我们去析构的时候是p1没有争议就是调用我们的A的析构函数,但是我们的p2就会有了,他指向的是派生类,需要我们去调用我们派生类的析构函数才不会导致我们的内存泄漏。我们的delete底层其实是 调用我们的析构函数+operator delete 。
这里我们想我们的指向的基类调用基类的,指向派生类的就调用派生类的析构函数。构成多态才能保证我们不会内存泄漏。
3 两个关键字
final
如果我们不想让派 ⽣类重写这个虚函数,那么可以⽤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; }
};
int main()
{return 0;
}
override
C++对函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写 错等导致⽆法构成重载,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果 才来debug会得不偿失,因此C++11提供了override,可以帮助⽤⼾检测是否重写
// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}
4 重载/重写/隐藏的对⽐
5 纯虚数和抽象类
当我们的虚函数后面加=0,此时就是一个纯虚数。纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现)只要声明即可,
包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了 派⽣类重写虚函数,因为不重写实例化不出对象。
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;
}
但是我们可以有抽象类的指针:例子: Car * B=new Benz;
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()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}
父类的指针指向那个调用哪个函数的虚函数。
6 多态的原理
我们先来算一下这个题目:
下⾯编译为32位程序的运⾏结果是什么()
A.编译报错 B. 运⾏报错 C. 8 D. 12
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;
}
答案:D
除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能 会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代 表function)。
做这个题目需要我们去学习我们的多态的原理。
虚函数表指针
⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要 被放到这个类对象的虚函数表中,虚函数表也简称虚表。实际上是一个函数指针数组
- 基类对象的虚函数表中存放基类所有虚函数的地址。
- 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表 指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基 类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴ 的。
- 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函 数地址。• 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地 址三个部分。
- 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)
- 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以 对⽐验证⼀下。vs下是存在代码段(常量区)
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()
{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;
}
如何实现的多态
从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicket, ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层 不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的 地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函 数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对 象,调⽤的是Student的虚函数。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
class Soldier: 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;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
我们底层是:我们的编译器先看你满不满足多态?如果满足多态的话,我们就到你指向的对象里面的虚表里面去找到对应的虚函数。
汇编:
动态绑定和静态绑定
对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤ 函数的地址,叫做静态绑定。
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数 的地址,也就做动态绑定。