文章目录
多态
多态的基本概念
多态就是不同类型的对象去完成同一件事情时会产生不同的状态。例如买票:
class Person
{
public:
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;
}
};
int main()
{
Person per;
Student stu;
Soldier sol;
Person& ref1=stu;
Person& ref2=sol;
per.BuyTicket();
ref1.BuyTicket();
ref2.BuyTicket();
return 0;
}
不同的人买票结果不一样,这就是多态。
多态的定义及其实现
多态的条件
在c++中,实现多态要满足3个条件。
- 必须要有继承关系
- 父类必须要有虚函数,并且子类要重写父类的虚函数。
- 必须是父类的指针或者引用去调用虚函数
虚函数就是被virtual修饰的成员函数,子类重写父类的虚函数,要求子类重写的虚函数和父类的虚函数返回值、参数、函数名完全一样。
class Person
{
public:
virtual void BuyTicket()
{
cout<<"全票"<<endl;
}
};
class Student:public Person
{
public:
//重写的函数名、参数、返回值均要与父类一致
virtual void BuyTicket()
{
cout<<"半票"<<endl;
}
};
重写有2个特例:
-
子类重写父类的虚函数可以可以不加virtual
class Person { public: virtual void BuyTicket() { cout<<"全票"<<endl; } }; class Student:public Person { public: //可以这样写,但不规范 void BuyTicket() { cout<<"半票"<<endl; } };
-
子类重写父类的虚函数,它们的返回值可以不同,但必须是父子关系的指针或者引用,这种方式称为协变.
返回值是父子关系的指针:
class Person { public: virtual Person* BuyTicket() { cout<<"全票"<<endl; return nullptr; } }; class Student:public Person { public: virtual Student* BuyTicket() { cout<<"半票"<<endl; return nullptr; } };
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 Person { public: virtual Person& BuyTicket() { cout<<"全票"<<endl; return *this; } }; class Student:public Person { public: virtual Student& BuyTicket() { cout<<"半票"<<endl; return *this; } };
协变在实际过程中使用很少,是满足重写的特例。
析构函数的重写
为了满足某些特殊场景,在继承体系中,父类的析构函数需要设置为虚函数且子类要进行重写。
class Person
{
public:
~Person()
{
cout<<"~Person析构函数的调用"<<endl;
}
};
class Student:public Person
{
public:
~Student()
{
cout<<"~Student析构函数的调用"<<endl;
}
};
int main()
{
Person* p1=new Person;
Person* p2=new Student;//父类的指针可以指向子类(切片)
delete p1;
delete p2;
return 0;
}
在delete p2的时候,会调用析构函数,由于this指针的类型是Person*,而且没有发生多态,所以delete p2会调用Person的析构函数(尽管p2指向的对象是Student)。
为了解决这个问题,需要把父类的析构函数设置为虚函数并且子类要进行重写。因为多态的需要,编译器在处理析构函数时,父类和子类的析构函数函数名统一被处理为destructor
class Person
{
public:
virtual ~Person()
{
cout<<"~Person析构函数的调用"<<endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout<<"~Student析构函数的调用"<<endl;
}
};
int main()
{
Person* p1=new Person;
Person* p2=new Student;
delete p1;
//this是Person*类型,指向的对象是Person,调用Person的析构函数
delete p2;
//this指针是Person*类型,指向的对象是Student,由于子类重写父类的析构函数,调用Student的析构函数。
return 0;
}
多态的调用规则是this指针指向哪一种对象,就调用这种对象的虚函数。
override和final
override和final是c++11新增的2个关键字,其中override的作用是检查子类是否重写了父类的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout<<"全票"<<endl;
}
};
class Student:public Person
{
public:
//override检测到子类的int BuyTicket()不是父类任何一个虚函数的重写,直接报错
virtual int BuyTicket() override
{
cout<<"半票"<<endl;
}
};
final的作用有2个:1是声明一个不可被继承的类,也称为最终类。2是修饰虚函数,表示这个虚函数不能被子类重写。
class A final{};
//声明为final的类不可被继承
class B:public A{};
class Person
{
public:
virtual void BuyTicket() final
{
cout<<"全票"<<endl;
}
};
class Student:public Person
{
public:
//父类的虚函数声明为final,子类不可重写
virtual void BuyTicket() override
{
cout<<"半票"<<endl;
}
};
重载、重写、重定义
重载:重载指的是在一个作用域下可以声明多个同名的函数,重载要求函数名相同,函数的参数不同。重载有函数重载和运算符重载,重载是复用函数名的一种手段。
重写:重写是实现多态的一个必要条件,指的是子类重写父类的虚函数,要求重写的函数名、参数、返回值与父类完全相同(协变除外)
重定义:重定义又称隐藏,指的是父类和子类存在同名成员,子类会屏蔽掉对父类的直接访问,对于成员函数,只要函数名相同就构成隐藏。
class A
{
public:
virtual void Print()
{
cout<<"A"<<endl;
}
};
class B:public A
{
public:
virtual void Print(int i)
{
cout<<"B:i"<<endl;
}
};
int main()
{
B b;
b.Print();//无法调用到A中的Print,B将A中的Print隐藏了
return 0;
}
b调用虚函数和调用普通成员函数的过程是一样的,都是在编译时确定要调用的函数的地址,在成员函数的前面加上virtual,只有在发生多态的时候才起作用,对于一般的调用,其调用过程和普通成员函数的调用完全一样,都是直接在这个类的公共代码区找到函数的地址,进行调用。
抽象类
含有纯虚函数的类叫做抽象类,纯虚函数指的是在虚函数的后面加上"=0",且该虚函数没有具体实现。抽象类又被称之为接口类,其最大的特点是不能实例化对象。当一个类继承了抽象类,必须要重写这个抽象类的纯虚函数,否则这个类也无法实例化对象。
class Person
{
public:
virtual void BuyTicket()=0;//纯虚函数
//Person是一个抽象类
};
class Student:public Person
{
public:
virtual void BuyTicket()
{
cout<<"半票"<<endl;
}
};
int main()
{
Person p;//错误,抽象类不能实例化对象
Student s;
Person& p=s;
p.BuyTicket();
return 0;
}
接口继承
虚函数的继承是一种接口继承。
class A
{
public:
virtual void Print(int a=1)
{
cout<<"A->"<<a<<endl;
}
};
class B:public A
{
public:
virtual void Print(int b=2)
{
cout<<"B->"<<b<<endl;
}
};
int main()
{
B b;
A& ref=b;
ref.Print();
//结果是B->1
return 0;
}
接口继承的意思是派生类继承的是基类虚函数的接口,重写基类虚函数的实现。上面的继承体系完全等价于:
class A
{
public:
virtual void Print(int a=1)
{
cout<<"A->"<<a<<endl;
}
};
class B:public A
{
public:
virtual void Print(int a=1)//派生类实际上是继承的基类虚函数的接口,重写基类虚函数的实现
{
cout<<"B->"<<b<<endl;
}
};
对于普通函数的继承是一种实现继承,把基类函数的接口和函数体的实现全部继承下来。
多态的原理
虚函数表
-
虚函数放在类的公共代码区
所有在类中声明为virtual的函数,最后它们的地址都被放进了这个类的虚函数表,虚函数表通过虚函数表指针来维护,在进行多态调用时,是从虚函数表中找到虚函数的实际地址,根据实际地址进行调用,而不是直接在类的的公共代码区找到虚函数进行直接调用。
class A { public: virtual void vir_FuncA1(){} virtual void vir_FuncA2(){} }; int main() { A a1; A a2; cout<<sizeof(A)<<endl;//4字节 return 0; }
A中有一个成员变量_vfptr,这个成员变量是所有对象共享一份的,vfptr的性质类似于static的成员变量,但是和static的变量性质不同,static的变量是不计算进对象的大小的,但是vfptr是计算入对象的大小的。每一个对象都有一份相同的vfptr,这个vfptr在对象调用构造函数在初始化列表进行初始化,只不过每一个对象的vfptr经过初始化列表以后的值都是一样的。
-
子类重写虚函数时,子类的虚函数表:
class A { public: virtual void vir_Func1() {} virtual void vir_Func2() {} virtual void vir_Func3() {} }; class B :public A { public: virtual void vir_Func1()override{} }; int main() { A a; B b; return 0; }
当子类重写虚函数以后,虚函数表中的A::vir_Func1被覆盖为B::vir_Func1。虚函数重写的本质是覆盖子类继承的虚函数表中的内容
-
子类继承父类之后,子类中虚函数表的生成过程
-
先把父类的虚函数中的内容拷贝一份到子类的虚函数表
-
如果子类重写了父类的虚函数,那么就把子类重写的虚函数的地址去覆盖父类中响应的虚函数的地址
-
子类新增的虚函数按照声明顺序依次增加到子类虚函数表的后面
验证(在vs2022下,虚表是以nullptr结束的)
typedef void(*funcptr)(); class A { protected: virtual void vir_Func1() { cout << "父类virtual void vir_Func1()" << endl; } virtual void vir_Func2() { cout << "父类virtual void vir_Func2()" << endl; } void Func3() {} protected: int a=1; }; class B :public A { protected: virtual void vir_Func1()override { cout << "子类重写的virtual void vir_Func1()" << endl; } virtual void Print() { cout << "子类新增的虚函数" << endl; } protected: int b=2; }; int main() { A testa; B testb; //打印A的虚表 funcptr* tablea = (funcptr*)*(int*)&testa; for (int i = 0; tablea[i] != nullptr; i++) { printf("tablea[%d]->%p ", i, tablea[i]); tablea[i](); } cout << "-----------------------------" << endl; //打印B的虚表 funcptr* tableb = (funcptr*)*(int*)&testb; for (int i = 0; tableb[i] != nullptr; i++) { printf("tableb[%d]->%p ", i, tableb[i]); tableb[i](); } return 0; }
A和B的对象模型:
A中的Func3被B继承了,但是不是虚函数,不进入虚函数表。
代码运行结果
tablea[0]->00BB13E3 父类virtual void vir_Func1() tablea[1]->00BB1339 父类virtual void vir_Func2() ----------------------------- tableb[0]->00BB1325 子类重写的virtual void vir_Func1() tableb[1]->00BB1339 父类virtual void vir_Func2() tableb[2]->00BB11C2 子类新增的虚函数
-
虚函数与虚函数表
虚函数和普通成员函数唯一的区别就是虚函数的地址需要填入虚函数表。虚函数和普通函数一样都是放到代码段的,虚函数在调用的时候,如果不符合多态的条件,依然是和普通函数一样在编译的时候直接确定函数地址的。
class A
{
public:
virtual void vir_Func()
{
cout<<"A的虚函数"<<endl;
}
};
class B:public A
{
public:
virtual void vir_Func()override
{
cout<<"B重写A的虚函数"<<endl;
}
};
int main()
{
A testa;
testa.vir_Func();
B testb;
testb.vir_Func();
return 0;
}
反汇编
A testa;
0060275F lea ecx,[testa]
00602762 call A::A (0601406h) #不构成多态,调用虚函数与调用普通函数一样编译时直接确定地址
testa.vir_Func();
00602767 lea ecx,[testa]
0060276A call A::vir_Func (0601087h)
B testb;
0060276F lea ecx,[testb]
00602772 call B::B (060123Fh) #不构成多态,调用虚函数与调用普通函数一样编译时直接确定地址
testb.vir_Func();
00602777 lea ecx,[testb]
0060277A call B::vir_Func (06013D9h)
虚函数只有在构成多态的时候才能触发虚函数表的机制。在构成多态的时候,调用虚函数,才会到虚函数表中去找虚函数的地址,然后进行调用,构成多态不会在编译时确定要调用的函数的地址,是在运行起来到虚函数表中去找要调用的函数的地址,所以多态也被称为运行时多态。
多态的原理
-
在调用虚函数的时候,编译器会判断是否是父类的指针或者引用在调用虚函数(这个检测工作是由编译器自动完成的)。
-
如果不是父类的指针或者引用在调用虚函数,是普通对象在调用虚函数,那么不会触发多态的机制,在编译时直接确定虚函数在代码段中的地址,call这个地址。
-
如果检测到是父类的指针或者引用在调用虚函数,那么会根据传入的this指针指向的对象来确定是在父类的虚函数表中找还是在子类的虚函数表中找要调用的虚函数。
-
只要是指针或者引用调用虚函数,必定是在运行的时候到虚函数表中找函数的地址
class A { public: virtual void vir_Func() { cout << "A的虚函数" << endl; } }; class B :public A { public: virtual void vir_Func()override { cout << "B重写A的虚函数" << endl; } }; int main() { B* p = new B; B& ref = *p; ref.vir_Func(); //子类的指针或者引用调用虚函数也是运行时到虚函数表中找要调用的函数。只是子类的指针或者引用一般指向的都是子类对象,所以到子类的虚函数表中找 return 0; }
-
只要调用的不是虚函数,一定在编译的时候就确定了要调用的函数的地址,即使是指针或者引用调用该函数。
class A { public: void vir_Func() { cout << "A的函数" << endl; } }; class B :public A { public: virtual void vir_Func() { cout << "B的虚函数" << endl; } }; int main() { B tmp; //tmp不是指针或者引用,tmp调用vir_Func直接在编译的时候call B::vir_Func tmp.vir_Func(); A& ref = tmp; //ref是父类的引用,指向子类对象。但是vir_Func在A中不是虚函数,所以直接在编译时候call A::vir_Func ref.vir_Func(); return 0; }
动态绑定与静态绑定
静态绑定:静态绑定又被称为早绑定,指的是在编译期间就确定了要调用的函数的地址。例如函数重载,函数重载是一种静态多态
动态绑定:动态绑定又被称为晚绑定,指的是在程序运行期间到虚函数表中找到要调用的函数的地址,动态绑定又称动态多态。
一般静态多态(早绑定)指的是函数重载,动态多态(晚绑定)指的是父类的指针或者引用去调用虚函数。
多继承的虚函数表
打印多继承的虚函数表。
typedef void(*funcptr)();
class A
{
public:
virtual void Func1()
{
cout << "A::virtual void Func1()" << endl;
}
virtual void Func2()
{
cout << "A::virtual void Func2()" << endl;
}
protected:
int a=1;
};
class B
{
public:
virtual void Func1()
{
cout << "B::virtual void Func1()" << endl;
}
virtual void Func2()
{
cout << "B::virtual void Func2()" << endl;
}
protected:
int b=2;
};
class C :public A, public B
{
public:
virtual void Func1()override//只重写了Func1
{
cout << "C::virtual void Func1()" << endl;
}
virtual void Func3()
{
cout << "C::virtual void Func3()" << endl;
}
protected:
int c=3;
};
int main()
{
A testa;
//打印testa的虚函数表
funcptr* tablea = (funcptr*)*(int*)&testa;
cout << "A的虚函数表" << endl;
for(int i=0;tablea[i]!=nullptr;i++)
{
printf("tablea[%d]->%p ", i, tablea[i]);
tablea[i]();
}
B testb;
cout << "-----------------------------------------------" << endl;
cout << "B的虚函数表" << endl;
funcptr* tableb = (funcptr*)*(int*)&testb;
for (int i = 0; tableb[i] != nullptr; i++)
{
printf("tableb[%d]->%p ", i, tableb[i]);
tableb[i]();
}
C testc;
//C中有2个虚表
//打印第一个虚表(继承的A的)
funcptr* tablec1 = (funcptr*)*(int*)(A*)&testc;//通过切片拿到ptr
cout << "-----------------------------------------------" << endl;
cout << "C的第一个虚表" << endl;
for (int i = 0; tablec1[i] != nullptr; i++)
{
printf("tablec1[%d]->%p ", i, tablec1[i]);
tablec1[i]();
}
funcptr* tablec2 = (funcptr*)*(int*)(B*)&testc;//通过切片拿到ptr
cout << "-----------------------------------------------" << endl;
cout << "C的第二个虚表" << endl;
for (int i = 0; tablec2[i] != nullptr; i++)
{
printf("tablec2[%d]->%p ", i, tablec2[i]);
tablec2[i]();
}
return 0;
}
结果
A的虚函数表
tablea[0]->006E1442 A::virtual void Func1()
tablea[1]->006E138E A::virtual void Func2() #006E138E
-----------------------------------------------
B的虚函数表
tableb[0]->006E10F0 B::virtual void Func1()
tableb[1]->006E1118 B::virtual void Func2() #006E1118
-----------------------------------------------
C的第一个虚表
tablec1[0]->006E13ED C::virtual void Func1() #C重写Func1
tablec1[1]->006E138E A::virtual void Func2() #006E138E
tablec1[2]->006E1320 C::virtual void Func3() #C中新增的虚函数放在继承的第一个虚函数表中
-----------------------------------------------
C的第二个虚表
tablec2[0]->006E135C C::virtual void Func1() #C重写Func1
tablec2[1]->006E1118 B::virtual void Func2() #006E1118
- 多继承的子类,会有多个虚函数表指针,对应多个虚函数表
- 子类中新增的虚函数,会被放到第一张虚函数表中
虚函数表的内容
上面的代码在vs的监视窗口下查看到的地址如下
可以看到,在2个虚表中C重写的Func1的地址不同,实际上,虚函数表中并非存放函数的真正地址。
typedef void(*funcptr)();
class A
{
public:
virtual void Test(){}
};
int main()
{
A a;
printf("虚函数表中存的地址是%p\n",(funcptr)*(int*)&a);
return 0;
}
运行结果
虚函数表中存的地址是00681244
反汇编
多继承:
class A
{
public:
virtual void Func1()
{
cout << "A::virtual void Func1()" << endl;
}
virtual void Func2()
{
cout << "A::virtual void Func2()" << endl;
}
protected:
int a=1;
};
class B
{
public:
virtual void Func1()
{
cout << "B::virtual void Func1()" << endl;
}
virtual void Func2()
{
cout << "B::virtual void Func2()" << endl;
}
protected:
int b=2;
};
class C :public A, public B
{
public:
virtual void Func1()override//只重写了Func1
{
cout << "C::virtual void Func1()" << endl;
}
virtual void Func3()
{
cout << "C::virtual void Func3()" << endl;
}
protected:
int c=3;
};
int main()
{
C testc;
//C中有2个虚表
funcptr* tablec1 = (funcptr*)*(int*)(A*)&testc;//通过切片拿到ptr
printf("第一个虚表中存的与Func1对应的地址是%p\n", tablec1[0]);
funcptr* tablec2 = (funcptr*)*(int*)(B*)&testc;//通过切片拿到ptr
printf("第二个虚表中存的与Func1对应的地址是%p\n", tablec2[0]);
A* ptra = &testc;
B* ptrb = &testc;
testc.Func1();
ptra->Func1();
ptrb->Func1();
return 0;
}
对象模型
反汇编
testc.Func1:不构成多态,直接调用。
ptra->Func1:构成多态。ptra调用Func1的过程
ptrb->Func1:构成多态。ptrb调用Func1的过程
在ptra和ptrb构成多态调用Func1函数时,实际上最终都是通过C::A::ptr进行调用的。ptra指向C::A,可以很容易的找到A::ptr并进行调用,而ptrb指向C::B,需要多一个步骤(sub ecx,8
)才能找到C::A::ptr,这个8是C::A的大小
总结:虚函数表中存放的是经过封装之后的地址(多继承下可能有多次封装),最终调用必须要借助首个继承的类的虚函数指针,上面A就是首个被继承的类。
菱形虚拟继承与多态
class A
{
public:
virtual void Func(){}
};
class B:virtual public A
{
public:
virtual void Func(){}
};
class C:virtual public A
{
public:
virtual void Func(){}
};
class D:public B,public C
{
public:
virtual void Func(){}
};
菱形虚拟继承,D中有三张虚函数表,D::A的虚函数表,D::B的虚函数表,D::C的虚函数表,要确定D::A的虚函数表中到底应该填哪一个虚函数,这就要求D重写Func,否则就有歧义。
菱形虚拟继承中构造函数的调用顺序
class A
{
public:
A(char *s) { cout<<s<<endl; }
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:
C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
{
cout<<s4<<endl;
}
};
int main()
{
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
由于D中只会有一份A,所以不会多次调用A的构造函数,是由D直接调用A的构造函数,且只调用一次。
多态的常见问题
-
内联函数可以是虚函数吗?
可以,如果是多态调用,会忽略掉inline,如果是普通调用,会使用inline
-
静态成员函数可以是虚函数吗?
不能,静态成员函数没有this指针,相当于全局函数,只不过受到类域限制,static成员函数的访问是在编译时决议的。而虚函数的调用是在虚表中找的,需要根据this指针指向的对象来确定是在谁的虚函数表中找,并且根据this指针去调用函数,this指针指向谁,调用谁的函数。
-
构造函数可以是虚函数吗?
不可以,因为虚函数要放进虚函数表,而对象中的虚函数表指针是在构造函数的初始化列表初始化的。在构造函数的初始化列表还没有走完的时候,vfptr是随机值。包括拷贝构造函数也不能是虚函数
-
赋值运算符重载可以是虚函数吗?
可以,但是赋值运算符重载设置为虚函数基本没有意义,例如:
class A { public: virtual A& operator=(const A& x) { _a = x._a; return *this; } protected: int _a=1; }; class B :public A { public: virtual B& operator=(const A& x)//协变 { A::operator=(x); return *this;//B重写A的operator=,只能处理B中的_a. } protected: int _b=2; }; int main() { B b; A a; A& ref=b; ref=a;//ref.operator=(&ref,a),this指向的对象是B类型,调用B类型的构造函数 return 0; }
-
对象访问普通函数块还是虚函数快?
如果虚函数不构成多态,都是在编译时决议,一样快。如果构成多态,则是在运行时决议,普通函数快。
-
虚表是放在哪个区域的?在什么时候生成的?
虚表放在常量区,在编译阶段就生成了。
class A { public: virtual A& operator=(const A& x) { _a = x._a; return *this; } protected: int _a=1; }; int main() { A a; static int s = 0; const char* p = "abc"; //取a的前4个字节得到vfptr printf("虚表的地址%p\n", *(int*)&a); printf("静态变量的地址%p\n", &s); printf("字符串常量的地址%p\n", p); return 0; }
虚表的地址00207B34 静态变量的地址0020A27C 字符串常量的地址00207B3C