本文主要介绍了C++中面向对象三大特性之一的多态的相关概念。主要介绍了多态的原理,如何实现多态以及虚函数等相关概念。
通俗来说,多态就是去完成一个行为不同的对象去完成时会有不同的状态。 例如,买火车票这一个行为,不同的对象去完成会有不同的状态: 普通人:成人票 学生:学生票 军人:优先买票
多态,是指在不同继承关系中的类对象,去调用同一函数,产生了不同的行为。比如,Student继承了Person,Person对象买票是全票,Student对象买票是半价票。 在继承种构成多态要满足两个条件:
被关键字virtual修饰的类成员函数称为虚函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
派生类中有一个与基类完全相同的虚函数(函数名,参数列表返回值类型等完全相同),称子类的虚函数重写了父类的虚函数。 注意:在重写虚函数时,子类的虚函数前可以不加virtual关键字,因为它是继承自父类的虚函数,其虚函数的属性是被继承了下来,但是一般还是写上更加规范。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测函数是否重写。
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz" << endl;
}
};
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
在虚函数的后面写上 = 0,这个虚函数就是纯虚函数。包含纯虚函数的类,称为抽象类。 抽象类不能实例化处对象,它的派生类也不能实例化处对象,只有派生类重写纯虚函数之后才能实例化出对象。 纯虚函数规范了派生类必须重写,纯虚函数体现出接口继承。
普通类的继承是实现继承,派生类继承了基类,可以使用基类的函数;虚函数的继承是一种接口继承,派生类继承基类目的是为了重写,达成多态,继承的是接口。 所以,不是实现多态就不要定义虚函数。
测试下面代码,计算类Base的大小:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
如图可以看到Base类的大小为8,为什么会是8呢? 我们调试观察:
可以看到对象b中除了成员变量_b以外还有一个__vfptr指针变量。放在对象的前面(注意:有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。 一个含有虚函数的类中都至少含有一个虚函数表指针,虚函数表指针指向一个虚函数表,虚函数表也称为虚表,虚函数的地址都被放在虚函数表中。 那么派生类中虚函数表里放了什么呢?
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base//基类
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base//派生类
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过调试观察监视窗口,我们有以下结论:
总结一下派生类虚表的形成:
虚表存在哪里,是存放在对象中吗?虚表里面存放的是什么,是存放着虚函数吗? 答:虚表存放在代码段,对象中存放的是虚表指针;虚函数存放在代码段,虚表里存放的是虚函数指针。
分析了这么多,多态的原理到底是什么呢?
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;
}
运行结果:
可以看到Person对象和Student对象调用同一个函数Func得到不同的结果,这是因为基类调用函数的传参基类对象,而派生类对象调用函数时的传参是派生类对象中基类的那一部分。导致基类的指针p是调用基类的成员函数,派生类的指针p是调用派生类的成员函数。
简单来说:
静态绑定是指在编译期间就确定的程序行为。比如,函数重载;
动态绑定是指在运行时确定的程序行为,根据具体拿到的类型确定程序的具体行为,调用具体的函数。比如,多态。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过调试观察监视窗口我们发现派生类对象的虚函数表中没有派生类自己的虚函数func3和func4,这是怎么回事呢? 我们可以将这个现象理解为一个Bug,并不是派生类的虚表里没有它自己的虚函数,而是这两个虚函数被监视窗口隐藏了。那么如果我们想查看派生类的虚函数都有那些该如何进行查看呢? 我们可以用代码将虚表中的虚函数打印出来。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
typedef void(*VFPTR) ();//函数指针
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
继承几个基类就有几张虚表,派生类自己的虚函数直接放在第一个继承基类部分的虚表中。
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 d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
以上就是今天要讲的内容,本文介绍了C++中多态的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。 最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!