目录
什么是多态
多态不是孙悟空的七十二般变化,也不是八戒的三十六变。C++中的多态是不同对象完成某个行为时的不同形态。例如:当春节返乡需要购买火车票时,普通的成年人要全价购票、而学生是半价、军人可以优先购票!同样是在买票,不同对象的购票形态是不一样的。
构成多态的条件
●虚函数重写
在类成员函数的前面加上virtual关键字,该函数就是虚函数。基类的虚函数必须加virtual修饰,派生类的虚函数前可以不加。
class person
{
public:
virtual void BuyTicket()
{
cout << "person::成人--全价购票" << endl;
}
};
在派生类中重写虚函数需要满足三同,派生类函数和基类函数名相同、参数相同、返回值相同(特殊情况协变除外),派生类虚函数可以不加virtual关键字(最好基类和派生类都加上)。
class person
{
public:
virtual void BuyTicket()
{
cout << "person::成人--全价购票" << endl;
}
};
class student : person
{
public:
virtual void BuyTicket()
{
cout << "student::学生--半价购票" << endl;
}
};
派生类中的虚函数可以不加virtual的原因是继承了基类虚函数的属性,为了代码的规范和可读性给派生类中重写的虚函数加上virtual是一个不错的习惯。
这里不要把虚函数重写(覆盖)和函数重载、隐藏(重定义)混淆:
●基类的指针或者引用调用虚函数
class person
{
public:
virtual void BuyTicket()
{
cout << "person::成人--全价购票" << endl;
}
};
class student : public person
{
public:
virtual void BuyTicket()
{
cout << "student::学生--半价购票" << endl;
}
};
class soldier : public person
{
public:
virtual void BuyTicket()
{
cout << "soldier::军人--优先购票" << endl;
}
};
//基类的指针
void Test1(person* pp)
{
pp->BuyTicket();
}
//基类的引用
void Test2(person& p)
{
p.BuyTicket();
}
int main()
{
person p1;
student s1;
soldier d1;
//传指针
cout << "指针调用" << endl;
Test1(&p1);
Test1(&s1);
Test1(&d1);
//传引用
cout << "引用调用" << endl;
Test2(p1);
Test2(s1);
Test2(d1);
return 0;
}
注意:普通调用只跟调用的类型有关。多态调用和指针指向的对象(派生类对象指向切割后的部分)&引用的对象有关(派生类对象引用切割后的部分)。
虚函数重载的两种特殊情况:
1、协变
上面提到派生类重写基类虚函数需要三同,函数名相同、参数相同、返回值类型相同。协变是其中的特例,返回值可以不同,但是基类要返回基类的引用或者指针,派生类要返回派生类的引用或者指针。
class A
{
public:
//virtual A& Fun() { return *this; }
virtual A* Fun()
{
cout << "A::Fun" << endl;
return nullptr;
}
};
class B : public A
{
public:
//virtual B& Fun(){return *this;}
virtual B* Fun()
{
cout << "B::Fun" << endl;
return nullptr;
}
};
注意:协变返回的基类和派生类返回值要指针和指针对应或者引用和引用对应。否则就不是协变会有如下报错:
2、析构函数的重写
在下面的代码中,两个基类指针分别指向基类对象和派生类对象(指向切割的部分),但是当delete调用析构清理资源时,只调用了基类的析构,没有构成多态。
class A
{
public:
~A()
{
cout << "A::基类析构" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "B::基类析构" << endl;
}
};
int main()
{
A* ptr1 = new A;
A* ptr2 = new B;
delete ptr1;
delete ptr2;
}
看到这你撇了撇嘴,基类和派生类的析构函数名都不一样,就算把基类的析构加上virtual修饰成虚函数也还是不能构成三同的条件呀!在上篇博客中提到析构函数的名称会被特殊处理,编译器编译后析构函数的名称统一处理成destructor。
class A
{
public:
virtual ~A()
{
cout << "A::基类析构" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "B::基类析构" << endl;
}
};
int main()
{
A* ptr1 = new A;
A* ptr2 = new B;
delete ptr1;
delete ptr2;
}
final 和 override
final关键字修饰虚函数后,虚函数不能被重写!
class A
{
public:
virtual void Fun() final
{
cout << "A::Fun()" << endl;
}
};
class B : public A
{
virtual void Fun()
{
cout << "B::Fun()" << endl;
}
};
override关键字是检查派生类中是否重写了基类的某个虚函数,没有重写编译阶段会报错:
class A
{
public:
virtual void Fun()
{
cout << "A::Fun()" << endl;
}
};
class B : public A
{
virtual void Fun (int a) override
{
cout << "B::Fun()" << endl;
}
};
抽象类
抽象类也叫接口类,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是重写实现多态。在虚函数后面加上 =0;这个函数就叫做纯虚函数,包含纯虚函数的类叫做抽象类。抽象类的特点是不能实例化对象,派生类只有重写纯虚函数后才能实例化对象。
class A
{
virtual void Fun() = 0;
public:
};
class B : public A
{
public:
int _a;
};
int main()
{
A a;
B b;
return 0;
}
派生类重写纯虚函数后,派生类可以实例化对象!
class A
{
virtual void Fun() = 0;
public:
};
class B : public A
{
virtual void Fun() override
{
cout << "B::Fun()" << endl;
}
};
int main()
{
B b;
return 0;
}
多态的原理
虚函数表
先来看一段代码:
class A
{
public:
virtual void Fun()
{
cout << "A::Fun()"<<endl;
}
private:
int _a;
char _b;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
按照以往的计算思路,按照内存对齐的规则很快计算出答案8,但是运行发现结果是12:
你看着镜子中稀少的头发陷入了沉思,多出来的4字节是怎么回事?接着打开调试窗口准备看下对象a的结构:
_vfptr(virtual function)叫做虚函数表指针,指向虚函数表(函数指针数组),虚函数的地址要被放到虚函数表中。这样一来就明白上述代码为什么是12字节了(vs下x86环境测试)。
带着对_vfptr的好奇,继续测试在派生类的虚表中放了什么:
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
void Fun3()
{
cout << "A::Fun3()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1() override
{
cout << "B::Fun1()" << endl;
}
protected:
int _b;
};
int main()
{
A a;
B b;
return 0;
}
多态的原理
上面讲到,构成多态需要满足两个条件,一个是虚函数重写、另一个是通过基类的指针或者引用调用虚函数。分析下述代码,调试探索多态的实现原理:
class person
{
public:
virtual void BuyTicket()
{
cout << "preson::成人--全价购票" << endl;
}
virtual void Fun()
{
cout << "preson::Fun()" << endl;
}
int _person;
};
class student : public person
{
virtual void BuyTicket()
{
cout << "student::学生--半价购票" << endl;
}
protected:
int _student;
};
class soldier : public person
{
virtual void BuyTicket()
{
cout << "soldier::军人--优先购票" << endl;
}
protected:
int _soldier;
};
void Test(person& p)
{
p.BuyTicket();
}
int main()
{
person p1;
Test(p1);
student s1;
Test(s1);
soldier d1;
Test(d1);
return 0;
}
1.观察上图,p引用的是peson对象时,p.BuyTicket()在person的虚表中找到虚函数serson::BuyTicket,成人全价购票。
2.当p引用的是studentd对象中继承基类的切割部分时,p.BuyTicket()在student的虚表中找到虚函数是student::BuyTicket,学生半价购票。 3.当p引用的是solider对象中继承基类的切割部分时,p.BuyTicket()在soldier的虚表中找到虚函数是 soldier::BuyTicket,军人优先购票。 4.静态绑定和动态绑定:
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
void Fun3()
{
cout << "A::Fun3()" << endl;
}
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "B::Fun2()" << endl;
}
void Fun3()
{
cout << "B::Fun3()" << endl;
}
};
int main()
{
A aa;
B bb;
//普通调用 -- 静态绑定 -- 编译时绑定
A* ptr = &aa;
ptr->Fun3();
ptr = &bb;
ptr->Fun3();
//多态调用 -- 动态绑定 -- 运行时绑定
ptr = &aa;
ptr->Fun1();
ptr->Fun2();
ptr = &bb;
ptr->Fun1();
ptr->Fun2();
return 0;
}
静态绑定又称为前期绑定、静态多态,在程序编译期间确定了程序的行为,比如函数重载就是静态绑定。
动态绑定又称为晚期绑定、动态多态,在程序运行阶段完成绑定,根据具体拿到的类型确定程序的行为,调用具体的函数。
单继承中的虚函数表
在下述代码中,基类有两个虚函数Fun1和Fun2,派生类中除了重写后的Fun1和Fun2还有一个虚函数Fun3():
//单继承虚函数表测试
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void Fun1()
{
cout << "B::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "B::Fun2()" << endl;
}
virtual void Fun3()
{
cout << "B::Fun3()" << endl;
}
protected:
int _b;
};
int main()
{
A aa;
B bb;
return 0;
}
观察发现,在派生类的虚函数表中没有存放指向虚函数Fun3的指针,这里不要被窗口误导,这是vs为了我们观察方便进行的优化,实际上Fun3()的地址是存在虚表中的。_vfptr是指向函数指针数组的指针,下面写个VfptrPrint接口遍历_vfptr指向的数组。
typedef void(*VFPTR)();
void VfptrPrint(VFPTR vft[])
{
for (int i = 0; vft[i] != nullptr; i++)
{
printf("[%d]:%p->",i,vft[i]);
vft[i]();
}
cout << endl;
}
int main()
{
A aa;
cout << "基类A虚函数表" << endl;
VfptrPrint((VFPTR*)(*(void**)&aa));
B bb;
cout << "派生类B虚函数表" << endl;
VfptrPrint((VFPTR*)(*(void**)&bb));
return 0;
}
虚函数表本质上是一个存虚函数指针的数组,数组的最后放了一个nullptr。
小总结:对于单继承,继承基类的虚函数(重写或者不重写)和派生类独有的虚函数都会存在虚函数表中。
多继承中的虚函数表
class A
{
public:
virtual void Fun1()
{
cout << "A::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "A::Fun2()" << endl;
}
protected:
int _a;
};
class B
{
public:
virtual void Fun1()
{
cout << "B::Fun1()" << endl;
}
virtual void Fun2()
{
cout << "B::Fun2()" << endl;
}
protected:
int _b;
};
class C : public A, public B
{
public:
virtual void Fun1()
{
cout << "C::Fun1()" << endl;
}
virtual void Fun3()
{
cout << "C::Fun3()" << endl;
}
protected:
int _c;
};
int main()
{
C cc;
return 0;
}
观察上面的窗口发现有两个虚表指针,虚表A中包含了重写后的虚函数Fun1和继承A的虚函数Fun2。虚表B中包含了重写的函数Fun1和继承B的虚函数Fun2()。两个表中均不见派生类C中的虚函数Fun3()。在用自己写的VfptrPrint接口打印下两虚表中虚函数的地址!
typedef void(*VFPTR)();
void VfptrPrint(VFPTR vft[])
{
for (int i = 0; vft[i] != nullptr; i++)
{
printf("[%d]:%p->", i, vft[i]);
vft[i]();
}
cout << endl;
}
int main()
{
C cc;
VfptrPrint((VFPTR*)(*(void**)&cc));
B* ptr = &cc;
VfptrPrint((VFPTR*)(*(void**)ptr));
//VfptrPrint((VFPTR*)(*(int*)((char*)&cc + sizeof(A))));
return 0;
}
最终发现,派生类C中的独有虚函数Fun3(),放在第一个继承基类部分的虚函数表中。