前言:
在现代软件开发中,面向对象编程(OOP)已经成为了主流。其中一个强大的概念就是多态性(Polymorphism),它不仅仅是一种技术,更是一种设计思想和实现方式,为软件开发带来了巨大的灵活性和可维护性。
多态性允许我们使用统一的接口来处理不同类型的对象,同时根据对象的实际类型来调用适当的方法。这种动态绑定的特性不仅提高了代码的复用性和可扩展性,还使得软件系统能够更好地适应变化和需求的增加。
本博客将深入探讨C++语言中多态性的各个方面:从基本概念到实际应用,从虚函数到虚函数表,从继承到接口隔离。我们将探索多态如何通过简单而强大的机制实现复杂的程序设计目标,以及它如何成为你面向对象编程旅程中的重要里程碑。
一、多态的概念
多态多态,简单地说,就是多种形态。具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子,同样是某个app的用户,一个是普通用户,一个是vip用户,当他们两个去点击操作同样的功能时,前者会提示你是普通用户,需要额外付费使用,后者则会提示您是vip用户,可直接使用。
更具体的来说,就是生活中去买票,军人去买票有着优先的特权,学生去买票可以优惠,而大多数普通人就只能全额购买。这三种不同的结果,就是我们说的多态。
同样是叫这个动作,狗叫是汪,猫叫是喵,羊叫是咩,这也是不同的结果。
二、多态的构成条件
多态是在继承的基础上出现的:不同继承关系的类对象,去调用了同一函数,却产生了不同的行为。
在此基础上,要构成多态还需要两个条件:
1、必须通过基类的指针或者引用调用虚函数。
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
class Person
{
public:
void fun()
{
cout << "原价" << endl;
}
};
class Student :public Person
{
public:
void fun()
{
cout << "半价" << endl;
}
};
void Get_ticket()
{
people.fun();
}
int main()
{
Person a;
Student b;
Get_ticket(a);
Get_ticket(b);
return 0;
}
class Person
{
public:
virtual void fun()
{
cout << "原价" << endl;
}
};
class Student :public Person
{
public:
virtual void fun()
{
cout << "半价" << endl;
}
};
结果就不在相同了,a运行的结果就是原价,b的结果则为半价。
因为这满足了多态运行的条件,也就是说,这就是多态的典型运行,明明调用的同一个函数Get_ticket,得到的结果却截然不同。
以上满足的三个条件:1、两个类是继承关系,在继承体系中。
2、两个fun都是虚函数,满足派生类对虚函数进行了重写。
3、通过基类的指针或者引用调用函数,也就是Person& people,我们把Student类型引用给了一个person类型,随后调用的就是Student重写后的fun函数。
注意:
当基类虚函数返回基类对象指针或者引用,派生类虚函数对象返回派生类指针或者引用时,也是特殊的重写关系,被称之为:协变。
另外,如果基类的析构函数是虚函数,那么此时的派生类虚函数只要定义,不管是否加了virtual关键字,都会与基类的虚函数构成重写,二者名字虽然不一样,但是编译器对析构函数的名称做了特殊处理,编译后的析构函数的名称统一处理为destructor。
三、override 和 final
1、final:修饰虚函数表示该虚函数不能再被重写
class Person
{
public:
virtual void fun ()final
{
cout << "原价" << endl;
}
};
2、override:检测派生类虚函数是否重写了基类的某个虚函数,若没有则编译报错。
class Student :public Person
{
public:
virtual void fun()override
{
cout << "半价" << endl;
}
};
四、纯虚函数与抽象类
在虚函数后面加上一个“=0”,那么这个虚函数就叫做纯虚函数,拥有纯虚函数的类叫做抽象类,抽象类不能实例化出对象。
派生类继承抽象类后,也不能实例化出对象,只有对该纯虚函数进行重写后,才能实例化出对象。这样纯虚函数就规范了派生类必须进行重写。
class A
{
public:
virtual void fun() = 0;//纯虚函数
};
class B :public A
{
public:
int a;
};
int main()
{
A a;//无法实例化,会编译报错
B b;//无法实例化,会编译报错
return 0;
}
以上A是一个抽象类,无法实例化,B继承了A,但没有对纯虚函数进行重写,所以仍然不能实例化,二者都会编译报错。
正确的解决方法是在B里对纯虚函数进行重写:
class A
{
public:
virtual void fun() = 0;//纯虚函数
};
class B :public A
{
public:
virtual void fun()
{
;
}
};
int main()
{
A a;//无法实例化,会编译报错
B b;
return 0;
}
这样B就能实例化出对象了。
五、多态的原理
1、虚函数表
class Base
{
public:
virtual void fun()
{
cout << "Base" << endl;
}
private:
int a;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
对于以上代码,输出的结果是什么呢?
在32为环境下,打印出的结果为8,我们知道,int类型的a占据4个字节,那么还有四个字节是在哪来的呢?
这就是虚函数表的原因,因为我们类中存在虚函数,就会生成一个专门的虚函数表指针,这个指针就占据了四个字节大小(32位系统下)。
派生类对象中也会有一个虚表指针,这个对象由两部分组成,一部分是继承父类的的成员,另一部分是派生类自身定义的属性和方法。在继承的时候,派生类会复制一份父类的虚表,如果在派生类中对某个虚函数进行了重写,就会对复制下来的虚表中的相应的虚函数进行覆盖操作(这也是重写被叫做覆盖的原因)。虚函数表本质上就是存放虚函数指针的数组,通常情况下这个数组最后存放了一个nullptr。
注意,虚函数与普通函数一样,都存放在代码段,只是它的函数指针存放在了虚表中,而对象中存放的是虚表指针,通过虚表指针找到虚表,随后又找到对应的虚函数。
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Core c;
printf("Base虚表指针地址:%p\n", *((int*)&b));
printf("Core虚表指针地址:%p\n", *((int*)&c));
printf("Base对象地址:%p\n", &b);
printf("Core对象地址:%p\n", &c);
return 0;
}
由此可知对象在栈区,虚表在常量区代码段,虚函数表的指针在对象里,虚函数也在常量区代码段。
2、多态的原理
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;
}
当p指向的是mike对象时,p->BuyTicket在mike的虚表中找到的函数是:Person::BuyTicket。p指向johnson对象时,p->BuyTicket在johnson的虚表中找到的函数是:Student::BuyTicket。这样一来,就实现了不同对象去完成同一行为时,展现出不同的情况。
3、动态绑定与静态绑定
面向对象的多态性表现在两大类:静态与动态。
静态绑定又称前期绑定,在程序编译期间就确定了程序的行为,例如函数重载。
动态绑定又称后期绑定,是在程序运行期间,通过具体拿到的类型确定程序的具体行为,调用具体的函数,也就是本文所主要讲的多态。