几乎所有面向对象语言我们总能在其中听到这么几个特点,封装,继承,多态,对于Cpp也不例外,那么什么是多态,Cpp又是如何实现多态的呢?
什么是多态?多态就是当不同的对象去完成某种相同的事务时却展现出完全不同的行为状态。在生活中也存在着各种各样的多态,例如当我们要买票时不同的人有着不同的票,学生票,成人票等等;又比如说当别人家的孩子到你家来做客时,你父母对待你和对待ta之间的差距也体现了多态。具体点说,多态可以让我们定义的对象根据对象的不同调用不同的函数,尽管这些函数名、参数、返回值都是相同的。这一点是不是听上去类似于重载?或者说类似于之前继承中的重定义?但接下来你会发现这其中有着很大的差别。
以下是一个简单的实现了多态的例子,我们看看这些类和哦我们往常所写的类有哪些不同。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "full ticket!" << endl;
}
};
class Student: public Person
{
virtual void BuyTicket()
{
cout << "student ticket!" << endl;
}
};
int main()
{
Person* p = new Person();
p->BuyTicket();
Person* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;
delete p2;
}
full ticket!
student ticket!
这段代码中我们用相同类型的指针通过指向不同类型的对象从而调用了不同的函数,实现了多态。要构成多态,必须满足两个条件:
1、必须通过基类的指针或者引用调用构成多态的函数。
2、被调用的构成多态的函数必定是虚函数,并且在子类中完成了重写/覆盖。
那么什么是虚函数呢?
被virtual
关键字修饰的成员函数就是虚函数,在上面的例子中基类和派生类中的BuyTicket()
就是虚函数,并且如果基类中的函数为虚函数,派生类中的重写函数不用加virtual
也会默认被视为虚函数。但是这里又出现了一个概念,重写。
之前我们有讲过重载,重定义,这里又提出了重写的概念,那么什么是重写,又如何构成重写呢?
重写是构成多态的前提,而构成重写要求必须是派生类中有一个虚函数与基类中的虚函数函数名、参数、返回值完全相同,才能构成重写。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "full ticket!" << endl;
}
};
class Student: public Person
{
//参数不同无法构成重写
virtual void BuyTicket(int)
{
cout << "student ticket!" << endl;
}
};
int main()
{
Person* p = new Person();
p->BuyTicket();
Person* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;
delete p2;
}
full ticket!
full ticket!
重写中要求派生类虚函数必须和基类虚函数函数名参数返回值都完全相同才能构成重写,但是也有例外,而且是两个。
第一个例外是协变,它可以让返回值不同的两个虚函数也构成重写,但是要求基类虚函数必须返回基类对象的指针或者引用,派生类虚函数必须返回派生类对象的指针或者引用。
#include
using namespace std;
class Person
{
public:
virtual Person* BuyTicket()
{
cout << "full ticket!" << endl;
return new Person;
}
};
class Student: public Person
{
//参数不同无法构成重写
virtual Student* BuyTicket()
{
cout << "student ticket!" << endl;
return new Student;
}
};
int main()
{
//这里肯定会造成内存泄露,这里为了演示暂时不考虑内存泄露的问题
Person* p = new Person();
p->BuyTicket();
Person* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;
delete p2;
}
full ticket!
student ticket!
协变的原理也是跟切割有关系,因为切割才使得派生类对象、指针、引用可以隐式转换为基类对象、指针、和引用。
如果我们用基类指针去指向一个派生类对象,当我们要释放这个对象的内存空间,势必会调用析构函数,但是如果调用的是基类的析构函数那么必然会导致派生类中一些成员变量无法释放空间,所以最好一个派生类的析构函数是可以和基类构成多态的,这样才能使基类指针可以根据对象的不同调用对应的析构函数不会导致内存泄露。但是派生类析构函数名和基类的析构函数名不同啊,如果构成重写呢?在这里编译器做了一些处理,只要基类的析构函数是虚函数,派生类的析构函数无论是否有virtual
修饰都会成为析构函数。从表面上看函数名不同违背了重写的规则,但是在底层实现上编译器会将析构函数名统一处理为destructor
。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "full ticket!" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student: public Person
{
//参数不同无法构成重写
virtual void BuyTicket()
{
cout << "student ticket!" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p = new Person();
p->BuyTicket();
Person* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;//调用Person析构函数
delete p2;//调用Student析构函数,由于Student继承于Person所以还会再调用基类析构函数
}
full ticket!
student ticket!
~Person()
~Student()
~Person()
这两个关键字在Cpp11中出现,他们用于帮助我们判断我们的虚函数是否还需要重写或者虚函数必须完成重写,可以帮助我们检查错误,规范代码。
凡是被override
修饰的虚函数表示其必然重写了基类的某个虚函数,如果没有重写则报错,因此override
是用在派生类的虚函数中的。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "full ticket!" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student: public Person
{
//参数不同无法构成重写
virtual void BuyTicket()
{
cout << "student ticket!" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
virtual void BuySomething() override
{
cout << "buy something" << endl;
}
};
int main()
{
Person* p = new Person();
p->BuyTicket();
Person* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;//调用Person析构函数
delete p2;//调用Student析构函数,由于Student继承于Person所以还会再调用基类析构函数
}
.\mian.cpp:27:18: error: 'virtual void Student::BuySomething()' marked 'override', but does not override
virtual void BuySomething() override
被final
关键字修饰的函数表示其不能再被重写,如果派生类重写则报错,所以这个关键字用在基类虚函数中。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "full ticket!" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
virtual void Buysomething() final
{
cout << "buy something" << endl;
}
};
class Student: public Person
{
virtual void BuyTicket()
{
cout << "student ticket!" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
virtual void Buysomething()
{
cout << "buy something too" << endl;
}
};
int main()
{
Person* p = new Person();
p->BuyTicket();
Person* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;//调用Person析构函数
delete p2;//调用Student析构函数,由于Student继承于Person所以还会再调用基类析构函数
}
.\mian.cpp:30:18: error: virtual function 'virtual void Student::Buysomething()'
virtual void Buysomething()
^~~~~~~~~~~~
.\mian.cpp:14:18: error: overriding final function 'virtual void Person::Buysomething()'
virtual void Buysomething() final
重载是同一作用域内,多个同名函数通过参数列表的不同编译器经过底层命名规则的处理来构成重载。重定义是派生类与基类两个作用域内,派生类成员与基类成员重名构成重定义(隐藏)。重写是派生类与基类两个作用域内,两个虚函数函数名、参数、返回值完全相同才构成重写(覆盖)。因此我们可以总结如下。
重载:同一作用域内,函数名相同,参数不同。
重写:在派生类和基类两个作用域内,函数为虚函数,且函数名参数返回值都完全相同。
重定义:在派生类和基类两个作用域内,函数名相同,只要不构成重写的同名函数即构成重定义。
在一个虚函数的声明后面加上=0
这个虚函数就会变成纯虚函数,带有纯虚函数的类被称为抽象类,抽象类也叫接口类,这个类不能实例化出对象,只有继承它的子类将其类内所有的纯虚函数全部重写后,其子类才可以实例化对象。
#include
using namespace std;
//抽象类
class Abstract
{
public:
virtual void BuyTicket() = 0;
virtual void Ticket()
{
}
};
class Person : public Abstract
{
public:
//重写纯虚函数
virtual void BuyTicket()
{
cout << "full ticket!" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student: public Abstract
{
virtual void BuyTicket()
{
cout << "student ticket!" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
//Abstract* pt = new Abstract();//报错,抽象类不能实例化对象
//之后所有的继承自抽象类的派生类就可以使用抽象类作为统一接口
Abstract* p = new Person();
p->BuyTicket();
Abstract* p2 = new Student();//切割,将子类对象指针赋值给父类指针
p2->BuyTicket();
delete p;//调用Person析构函数
delete p2;//调用Student析构函数,由于Student继承于Person所以还会再调用基类析构函数
}
使用抽象要注意派生类要重写抽象类中的所有纯虚函数,否则会报错,抽象类中的非纯虚函数,不会强制重写。
如今大多数Cpp的书中关于多态也仅仅局限在如何使用上,但是这是肯定不够的,于是通过一些资料和实验在这里总结出Cpp实现多态的原理,这对于多态的使用有极强的帮助。
我们首先看一个小例子,体会一下在存在虚函数的类中所存在的一个神秘的成员。
#include
using namespace std;
class Base
{
virtual void Func()
{
cout << "Func1" << endl;
}
private:
int _a;
};
class Deliver: public Base
{
virtual void Func()
{
cout << "Func1" << endl;
}
private:
int _b;
};
int main()
{
cout << sizeof(Base) << endl;;
cout << sizeof(Deliver) << endl;;
}
8
12
这里可能就会产生疑惑,在基类中命名只有一个成员_a
应该只有四个字节啊哪里来的八个字节空间,难道虚函数也占空间么?派生类中继承了从基类来的成员_a
自己还有一个成员_b
应该只有八个字节哪里来的12,为什么他们都多出来了四个字节的空间?
接下来我们继续对其进行改进,多新增几个虚函数和普通函数。
#include
using namespace std;
class Base
{
virtual void Func()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
void Func3()
{
cout << "Func3" << endl;
}
private:
int _a;
};
class Deliver: public Base
{
virtual void Func()
{
cout << "Func1" << endl;
}
void Fun3()
{
cout << "Func3" << endl;
}
private:
int _b;
};
int main()
{
cout << sizeof(Base) << endl;;
cout << sizeof(Deliver) << endl;;
}
8
12
我们发现我们增加了虚函数后类的大小并没有改变,永远只是增加了四个字节。这又是为什么呢?
通过一些高级ide对内存的查看及反复实验得出结论,在每一个有虚函数的类中会多生成一个成员虚函数表指针,这个指针指向一张表称其为虚函数表也叫虚表,虚表可以看作是一个数组,它的元素类型为虚函数指针,也就是说这个数组是一个虚函数指针数组,其中的每一个元素指向一个类内的虚函数,最后一个元素为nullptr
作为结尾的标记。
那么为什么编译器要生成这么一个虚函数表,它又是怎么生成的呢?
每个含有虚函数的类都会自动生成一个虚函数表,如果是没有发生继承的类,则虚函数表中所存储的都是自己类中的所有虚函数的指针,并且类内会自动生成一个成员虚函数表指针去指向这个虚函数表。如果一个派生类类继承了一个拥有虚函数的类,则相当于自己本身这个类也拥有虚函数,因此也会生成一个虚函数表,但是在最开始生产时会将基类的虚函数表一模一样拷贝一份作为自己的虚函数表,然后类内生成虚函数表指针指向这个虚函数表,此时它的虚函数表中的每个指针都指向了在基类中定义的虚函数,一旦这个派生类重写了基类中的某个虚函数,则会用重写后的虚函数的指针去覆盖对应的虚函数表中父类虚函数的指针,并且派生类中新添加的虚函数会放在虚函数表的最后,非虚函数不会放入虚函数表,以此完成虚表的构建,虚表是Cpp中完成多态的关键。
关于虚表可能会疑惑它存存放在哪里,虚函数又存放在哪里,虚表中存放的是虚函数么?
注意:类中所存放的不过是虚表指针,所以32位操作系统只有4个字节,用于找到虚表位置。虚表中存放的只是虚函数的指针,虚函数和普通函数一样是存放在代码段的,只是它的指针存放在虚表中方便我们找到他。而虚表在vs环境下是存放在代码段的,根据编译器的不同处理方式也有所不同。
知道了虚表的存在后就要明白为什么要有虚表,它又是如何在多态的实现中起到重要作用的。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "Full ticket" << endl;
}
};
class Student : public Person
{
public:
//发生重写/覆盖
virtual void BuyTicket()
{
cout << "Student ticket" << endl;
}
};
int main()
{
Person* person = new Person();
Person* student = new Student();
person->BuyTicket();
student->BuyTicket();
}
Full ticket
Student ticket
以上这个例子是一个再普通不过了的多态的例子,我们结合虚表解析一下编译器实现多态的过程。
student
和person
虽然都是Person
类型的指针,但是却指向了类型不同的对象,而不同类型的对象在初始化时虚表指针也会根据自己的类型随之生成因此虽然student
类型为Person
但是其指向的Student
类型对象中的虚表指针是指向Student
类的虚表。而编译器在调用虚函数前都会通过对象中的虚表指针去虚表中寻找对应函数的地址,Student
类对函数进行了重写,重写后的函数地址覆盖了原来的函数地址,因此student
对象在调用函数时虚表中找到的就是Student::BuyTicket()
,同理person
对象也会根据虚表找到自己应该调用的Person::BuyTicket()
,这样即可完成多态。
类中的非虚函数并不会写入虚函数表,因此无法完成重写更别说多态,所以实现多态的前提重写中就以要求两个函数必须是虚函数。
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "Full ticket" << endl;
}
void fun()
{
cout << "fun1" << endl;
}
};
class Student : public Person
{
//发生重写/覆盖
public:
virtual void BuyTicket()
{
cout << "Student ticket" << endl;
}
void fun()
{
cout << "fun2" << endl;
}
};
int main()
{
Person* person = new Person();
Person* student = new Student();
person->BuyTicket();
student->BuyTicket();
person->fun();
student->fun();
}
Full ticket
Student ticket
fun1
fun1
以上的fun
因为不是虚函数并不会被写入虚表,更无法完成重写,所以构不成多态,这解释了为什么说虚表才是实现多态的关键。
在程序编译期间就已经确定了程序的行为的方式称为静态多态,函数的重载也是一种多态,但是函数重载的多态是利用了命名规则,我们调用不同参数的函数等于调用了不同函数的函数,与调用普通函数没有差别,其调用规则在程序编译期间就已经决定,这种绑定方式也称为早绑定。
在程序运行期间根据具体类型动态决定程序执行行为的方式称为动态绑定,例如利用虚函数重写的方式达成的多态,就是在程序执行期间根据类型去不同的虚表中调用不同的函数,以此达成多态功能,这种绑定方式也成为晚绑定。
在我们知道了多态的原理后已经明白了为什么要构成重写才能完成多态,那么对于第一条规则,为什么必须通过基类的指针或者引用才可以使用多态?
首先为什么是基类我觉得不用多说了,因为切割的存在派生类类对象、指针和引用可以赋值给基类的对象、指针和引用从而根据指针和引用指向不同的对象类型调用不同的函数。那么不用指针和引用,对象能不能完成多态呢?毕竟派生类类对象也是可以给基类对象赋值的。
答案是肯定不行的。在原理的介绍中有提到过虚表指针是在对象初始化时根据对象类型创建的,建立什么类型对象就有指向什么类虚表的虚表指针。而指针和引用所完成的切割与对象完成的切割有所不同,基类指针指向派生类对象是可以完成的,但是此时在内存中派生类对象依然是派生类对象,独属于派生类的成员依然存在着,只是无法通过基类指针对其访问(引用同理,引用底层实现也是利用指针)。当通过指向派生类的基类指针访问派生类对象的虚函数时,所利用的虚表指针也是在派生类对象初始化时创建的指向派生类虚表的指针,因此才可以找到派生类的虚表从而访问我们想要的函数。这一切都得益于指针之间的赋值不过是对同一块内存的不同理解罢了,并没有改变内存中的数据,才能找到属于派生类的虚函数表。而对象之间的赋值必然会通过赋值运算符重载或者拷贝构造,这其中不过是把派生类成员变量的值拷贝给了基类中存在的成员变量,并没有完成虚表指针的拷贝,虚表指针在对象初始化时已经根据自身类型确定死了,因此通过对象的赋值基类对象无法获得派生类对象的虚表指针,无法访问到派生类虚表也就无法调用我们动态绑定的函数,也就无法完成多态。
单继承是我们之前之前一直讨论的模型,派生类的虚函数表如果是未发生重写状态则会完全复制一份基类的虚函数表,并将其指针作为成员放在类中,并且会放在开头,我们以以下这个类作为例子分别画出派生类内成员继承的模型和基类与派生类虚函数表内的数据。
我们首先用一种非常规的方式打印基类和派生类中虚函数表的内容
#include
using namespace std;
class Base1
{
public:
virtual void func1()
{
cout << "Base1::func1()" << endl;
}
virtual void func2()
{
cout << "Base1::func2()" << endl;
}
private:
int _b1;
};
class Derive : public Base1
{
//发生重写/覆盖
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
virtual void func3()
{
cout << "Derive::func3()" << endl;
}
virtual void func4()
{
cout << "Derive::func4()" << endl;
}
private:
int _d;
};
typedef void (*VFPtr)();//虚函数指针
//打印虚函数表内容
void PrintVTable(VFPtr vTable[])//虚表本质上是一个虚函数指针数据
{
cout << "vTable address:" << vTable << endl;
for(int i = 0; vTable[i] != nullptr; i++)
{
printf("%dst vfptr of the vTable:0X%x -> ", i, vTable[i]);
VFPtr func = vTable[i];
func();
}
}
int main()
{
//取出虚函数表指针
//对于单继承虚函数表指针就是对象的前4个字节,取出前四个字节再强转为VFPtr*类型即可
Base1 b;
Derive d;
VFPtr* vTable1 = (VFPtr*)(*(int*)&b);
PrintVTable(vTable1);
VFPtr* vTable2 = (VFPtr*)(*(int*)&d);
PrintVTable(vTable2);
}
vTable address:0x4051e4
0st vfptr of the vTable:0X403cf0 -> Base1::func1()
1st vfptr of the vTable:0X403d24 -> Base1::func2()
vTable address:0x4051f4
0st vfptr of the vTable:0X403d88 -> Derive::func1()
1st vfptr of the vTable:0X403d24 -> Base1::func2()
2st vfptr of the vTable:0X403dbc -> Derive::func3()
3st vfptr of the vTable:0X403df0 -> Derive::func4()
打印的第一个虚表是基类的虚函数表,我们对Func1
进行了重写因此可以看到派生类虚表中函数地址与基类的不一样,而Func2
我们并没有重写因此还是拷贝的基类的呢一份并没有变,Func3
和Func4
是派生类新加上去的,因此放在派生类虚表的最后。
我们将派生类中成员与虚表的对应关系表示出来。
对于多继承来说,派生类每继承一个基类都会从基类中拷贝一份虚表,因此一个派生类在多继承中可以拥有多个虚表,也就拥有多个虚表指针。我们将以上的例子稍微变更一下,再打印基类与派生类中虚表的内容。
#include
using namespace std;
class Base1
{
public:
virtual void func1()
{
cout << "Base1::func1()" << endl;
}
virtual void func2()
{
cout << "Base1::func2()" << endl;
}
private:
int _b1;
};
class Base2
{
public:
virtual void func1()
{
cout << "Base2::func1()" << endl;
}
virtual void func2()
{
cout << "Base2::func2()" << endl;
}
private:
int _b2;
};
class Derive : public Base1, public Base2
{
//发生重写/覆盖
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
virtual void func3()
{
cout << "Derive::func3()" << endl;
}
private:
int _d;
};
typedef void (*VFPtr)();//虚函数指针
//打印虚函数表内容
void PrintVTable(VFPtr vTable[])//虚表本质上是一个虚函数指针数据
{
cout << "vTable address:" << vTable << endl;
for(int i = 0; vTable[i] != nullptr; i++)
{
printf("%dst vfptr of the vTable:0X%x -> ", i, vTable[i]);
VFPtr func = vTable[i];
func();
}
}
int main()
{
//取出虚函数表指针
//对于单继承虚函数表指针就是对象的前4个字节,取出前四个字节再强转为VFPtr*类型即可
Base1 b1;
Base2 b2;
Derive d;
VFPtr* vTable1 = (VFPtr*)(*(int*)&b1);
PrintVTable(vTable1);
VFPtr* vTable2 = (VFPtr*)(*(int*)&b2);
PrintVTable(vTable2);
VFPtr* vTable3 = (VFPtr*)(*(int*)&d);
PrintVTable(vTable3);
//这里的多继承的派生类中的第二个虚表指针紧跟在第一个派生类成员之后
VFPtr* vTable4 = (VFPtr*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTable4);
}
vTable address:0x405218
0st vfptr of the vTable:0X403d30 -> Base1::func1()
1st vfptr of the vTable:0X403d64 -> Base1::func2()
vTable address:0x405228
0st vfptr of the vTable:0X403dc8 -> Base2::func1()
1st vfptr of the vTable:0X403dfc -> Base2::func2()
vTable address:0x405238
0st vfptr of the vTable:0X403e60 -> Derive::func1()
1st vfptr of the vTable:0X403d64 -> Base1::func2()
2st vfptr of the vTable:0X403e94 -> Derive::func3()
vTable address:0x40524c
0st vfptr of the vTable:0X403f00 -> Derive::func1()
1st vfptr of the vTable:0X403dfc -> Base2::func2()
这里打印了两个基类的虚表和派生类中的两个虚表,可见派生类如果多继承确实是会有多个虚表存在的,并且如果重写则会对类内所有的虚表中的对应函数都会进行重写,并且会将自己新增的虚函数放进第一个虚表中,也就是最先继承的类所的来的那个虚表。