多态及其原理
文章目录
- 一、多态的概念
- 二、多态的定义及实现
- 多态构成的条件
- 虚函数重写的问题
- 默认参数与虚函数
- 协变
- 析构函数的重写
- override和final关键字
- 三、纯虚函数和抽象类
- 四、多态的原理
- 虚函数表指针
- 虚函数的重写与覆盖
- 动态绑定与静态绑定
- 虚函数表总结
一、多态的概念
多态(ploymorphism):通俗来说,就是多种形态。
多态分为编译时多态(静态多态)和运行时多态(动态多态)。
- 编译时多态主要是函数重载和函数模板,通过传达不同的参数,以做到同一个函数名表达不同形态,由于将实参传递给形参的参数匹配是在编译时完成的,所以叫做编译时多态。
- 运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,由此达到多种形态。譬如买票这个行为,普通人是全价买票,学生是优惠买票,军人则是优先买票。
二、多态的定义及实现
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
针对上面买票的例子,创建三个类,Student
和Soldier
继承自Person
:
class Person {};
class Student : public Person {};
class Soldier : public Person {};
对于买票这个行为,创建函数BuyTicket
void BuyTicket();
为了实现多态,需要先将基类的BuyTicket
通过virtual
关键字定义成虚函数(非成员函数不能加virtual
进行修饰)。
在重写基类虚函数时,派⽣类的虚函数在不加virtual
关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤。基类虚函数必须要加virtual
修饰。
同时派生类的BuyTicket
做到返回值类型、函数名字、参数列表完全相同,达到重写的效果:
class Person {
public:// 添加virtual关键字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;}
};
自此,完成继承中构成多态的一个条件:被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写。
重写也称覆盖,是派生类对基类允许访问的方法的实现过程进行重新改写,返回值类型、函数名称及参数列表都不能改变。
接着设计一个Pay
函数,通过接收不同的身份来调用不同的BuyTicket
函数,调用虚函数需要用到指针或引用:
// 调用虚函数必须通过指针或引用
void Pay(Person* ptr)
{// 虽然都是Person指针ptr在调用BuyTicket// 但是跟ptr没关系,而是由ptr指向的对象决定的ptr->BuyTicket();
}
这是构成多态的第二个条件:必须通过基类的指针或引用调用基函数。
下面的程序展示了如何实现多态:
class Person {
public:// 添加virtual关键字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 Pay(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person p;Student st;Soldier so;Pay(&p);Pay(&st);Pay(&so);return 0;
}
程序运行结果如下:
多态构成的条件
继承中要构成多态必须要两个条件:
-
必须通过基类的指针或者引用调用虚函数。
-
被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写。
对象不可以调用虚函数,并且只有基类指针才可以既指向基类对象又指向派生类对象。
虚函数重写的问题
默认参数与虚函数
对于下面一个程序,最终输出结果是什么()
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;
}
开始分析,p
是派生类指针,随后调用test
,由于继承关系中对成员函数的搜索是先派生类再基类。B
中没有test
,调用A
中的test
,此时*this == B
,B
中已经将func
进行重写覆盖,由于构成多态时只重写实现,函数体不变,此时传入func
的val
值为1,test
是A
中的函数,调用func
时,默认参数是静态绑定的,函数实现继续实行重写内容,最终输出内容为B->1
。
若是直接p->func()
呢?
答案是B->0
,这里的关键是默认参数的值是在编译时根据调用该函数的指针或引用的静态类型来确定的,而虚函数是动态绑定的。test
是A
中成员函数,其中func
在编译时的参数是静态决定的,为1。
协变
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,了解⼀下即可。
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;}
};
析构函数的重写
基类的虚构函数需要声明为虚函数,否则会出现如下错误。
下面一段程序:
class Base {
public:Base() { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
};class Derived : public Base {
public:Derived() { cout << "Derived()" << endl; }~Derived() { cout << "~Derived()" << endl; }
};int main()
{Base* p = new Derived(); // 用基类指针指向一个派生类对象delete p; // 关键在这里!通过基类指针删除对象
}
结果如下:
创建了一个Derived
对象,所以两个类的构造函数都被调用了。但是,当delete p
时,由于p
是指向Base
类对象,并且Base
的析构函数不是虚函数,所以编译时直接决定调用Base::~Base()
。Derived::~Derived()
根本没有被调用,若Derived
类在构造函数中分配了内存,则其析构函数中的清理代码永远不会被执行,造成内存泄漏。
当类可能被继承,并且会通过基类指针来删除派生类对象时,基类的析构函数必须是虚函数,否则会导致派生类的析构函数不被调用。
仅在基类析构函数前添加关键字virtual
,其余不变:
virtual ~Base() { cout << "~Base()" << endl; }
程序输出如下:
虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor
,所以基类的析构函数加了virtual
修饰,派⽣类的析构函数就构成重写。由于p
指向派生类对象,这样才能先调用派生类的析构函数,之后自动调用基类的析构函数。
override和final关键字
从上⾯可以看出,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override
,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final
去修饰。
最后总结一下,重写/重载/隐藏的对比
三、纯虚函数和抽象类
在虚函数的后面加上=0
,则称这个函数为纯虚函数。
virtual void Drive() = 0;
包含纯虚函数的类就是抽象类。
// 抽象类
class Car{
public:virtual void Drive() = 0; // 纯虚函数
};
抽象类不可以实例化对象,其派生类只有对纯虚函数进行重写,才可以实例化对象。
通过下面的几何形状例子,可以更好地理解统一接口的便携。
// 抽象类:形状
class Shape{
public:// 纯虚函数:计算面积(接口)virtual double getArea() const = 0;// 虚析构函数(重要!)virtual ~Shape() { cout << "~Shape()" << endl; }
};// 派生类:圆形
class Circle : public Shape {
private:double radius;
public:Circle(double r):radius(r){}// 必须重写基类的所有抽象方法double getArea() const override {return 3.14159 * radius * radius;}
};// 派生类:矩形
class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h):width(w),height(h){}double getArea() const override {return width * height;}
};int main()
{// Shape s; // 错误!不能创建抽象类的对象Shape* s1 = new Circle(5.0); // 多态:基类指针指向派生类指针Shape* s2 = new Rectangle(3.0, 4.0);// 通过统一接口操作不同对象Shape* shapes[] = { s1, s2 };for (Shape* s : shapes) {cout << "Area:" << s->getArea() << endl;}delete s1;delete s2;return 0;
}
程序运行结果如下:
四、多态的原理
虚函数表指针
首先来看下面的程序,思考32位下程序输出结果是多少:
class Base {
public:virtual void func(){cout << "func" << endl;}
private:int _b;
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
输出结果竟然是8
通过调用监视窗口,可以看到除了_b
成员,还多一个__vfptr
放在对象的前面(注意有些平台可能
会放到对象的最后⾯,这个跟平台有关),对象中的这个指针叫做虚函数表指针,它是void**
类型的,一个含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中。
对于下面这个类,有两个虚函数和一个成员函数:
class Base {
public:virtual void func1(){cout << "func1" << endl;}virtual void func2(){cout << "func2" << endl;}void func3(){cout << "func3" << endl;}
private:int _b;
};
可以看到,仅有两个虚函数的地址被存放到虚表中,而成员函数func3
则不在其中,普通函数只会进符号表以方便链接,编译时就已经确定。
虚函数的重写与覆盖
多态实际上就是通过虚表来实现的,下面代码创建一个基类和派生类:
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }virtual ~Base() { cout << "~Base()" << endl; }
};class Derived : public Base {
public:void func1() { cout << "Derived::func1" << endl; } // 重写func1virtual void func3() { cout << "Derived::func2" << endl; } // 派生类新建一个虚函数func3~Derived() { cout << "~Derived()" << endl; }
};void Func(Base *ptr)
{ptr->func1();
}int main()
{Base b;Derived d;Func(&b);Func(&d);
}
派生类重写基类的func1
,不重写而是继承func2
,调试观察两个类的对象的虚表内容:
虽然传的都是Base*
类型指针,但编译器根据其指向的对象类型自己在各自虚表中寻找对应函数,经过重写的虚函数地址被覆盖,未经重写继承基类的虚函数仍然存在于派生类的虚表中。
动态绑定与静态绑定
对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
下面的代码经过反汇编可以看出静态绑定以及动态绑定的差别
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }void func3() { cout << "Base::func3" << endl; } // 基类成员函数func3virtual ~Base() { cout << "~Base()" << endl; }
};class Derived : public Base {
public:void func1() { cout << "Derived::func1" << endl; } // 重写func1,继承基类func3~Derived() { cout << "~Derived()" << endl; }
};int main()
{Derived d;Base* ptr = &d; // ptr指向派生类对象ptr->func1();ptr->func3();return 0;
}
所有函数包括虚函数的地址在编译阶段就已经确定。但是对于普通函数,直接访问该函数地址即可,对于虚函数,需要程序运行时找到虚表地址后,再查表找到虚函数进行调用。这便是动态绑定和静态绑定的区别。
虚函数表总结
- 基类对象的虚表存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
- 派生类由两部分构成,继承下来的基类和自己的成员,派生类和基类的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也是独立的。
- 派生类中重写的基类的虚函数,派生类的虚表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
- 派生类的虚表中包含,(1)基类的虚函数地址(2)派生类重写的虚函数地址完成覆盖(3)派生类自己的虚函数地址三个部分
- 虚表本质是一个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个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:// 重写基类的func1virtual 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;
}