文章目录
多态的概念
多态,通俗点来说,就是多种形态,当不同的对象要去完成某个行为时,会出现不同的状态。
C++中多态的机制是通过虚函数来实现的。关于多态的实现,就是利用父类类型的指针指向其子类的实例,然后通过父类型的指针调用实际子类的成员函数。
这种技术可以让父类的成员函数拥有多种形态。C++中存在两种多态,一种为静态多态,一种为动态多态。静态多态试图做到编译时绑定/决议,动态多态试图做到运行时绑定/决议。
虚函数
在类中,被virtual修饰的成员函数就叫做虚函数。
class Base
{
public:
virtual void func() { cout << "Base::func()" << endl; } //虚函数
};
虚函数的重写
子类中存在与父类完全相同的虚函数(函数名,函数返回值,函数参数都相同),就称子类的成员函数完成了对父类成员函数的重写。
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2(int a) { cout << "Base::func2(int)" << endl; }
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func2() { cout << "Derive::func2()" << endl; }
};
上述代码中,子类的func1完成了对父类func1的重写,因为子类的func1与父类的func1完全相同;但子类的func2并没有完成对父类func2的重写,因为二者函数参数不同。
多态的构成条件
多态存在于继承关系的父子类中,并且还有两个关键条件:
- 必须通过父类的指针/引用调用虚函数。
- 被调用的函数必须是虚函数,且子类的虚函数必须完成对父类虚函数的重写。
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func2(int a) { cout << "Derive::func2()" << endl; }
};
int main()
{
Base* ptr = new Derive;
ptr->func1();
ptr->func2();
delete ptr;
return 0;
}
上述代码中,子类的func1完成了对父类func1的重写,但func2没有;所以,当父类指针调用func1时,能够实现多态。但调用func2时就只是普通调用。如果子类没有完成重写,那么用父类指针的调用都只是普通调用,也就意味着只会调用父类的同名函数。
普通调用与多态调用的区别:
- 普通调用:跟调用对象的类型有关。
- 多态调用:跟指针/引用 所指向的对象有关。
虚函数重写的两个例外
协变
要完成虚函数重写,不一定要满足三同(函数名,返回值,参数)。其中,返回值可以不同,但必须是子类或父类的指针/引用。子类虚函数就返回子类对象的指针/引用,父类就返回父类对象的指针/引用,这种重写形式被称为协变。
class Base
{
public:
virtual Base* func1() { cout << "Base::func1()" << endl; return this; }
};
class Derive : public Base
{
public:
virtual Derive* func1() { cout << "Derive::func1()" << endl; return this; }
};
返回值所指的指针或引用不一定得是当前类对象的指针或引用,但必须满足子类对应子类,父类对应父类的关系。
class A
{
int _a;
};
class B : public A
{
int _b;
};
class Base
{
public:
virtual A* func1() { cout << "Base::func1()" << endl; return nullptr; }
};
class Derive : public Base
{
public:
virtual B* func1() { cout << "Derive::func1()" << endl; return nullptr; }
};
析构函数的重写
如果父类的析构函数为虚函数,那么只要子类的析构函数显示定义,无论是否加了virtual,都会与父类的析构函数构成重写,虽然二者的析构函数名不同。因为编译器底层对析构函数的名字做了特殊处理,编译后析构函数的函数名统一处理成了destructor。
class Base
{
public:
virtual ~Base() { cout << "~Base()" << endl; }
};
class Derive : public Base
{
public:
~Derive() { cout << "~Derive()" << endl; }
};
析构函数重写的作用
假设有如下代码,
//析构函数的重写
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
~Base() { cout << "~Base()" << endl; }
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
~Derive()
{
delete[] p;
cout << "~Derive()" << endl;
}
private:
int* p = new int[10];
};
int main()
{
//用父类指针指向子类,多态的前提条件之一
Base* ptr = new Derive;
//多态的调用
ptr->func1();
//希望析构Derive对象
delete ptr;
return 0;
}
Derive是Base类的子类,它有一块在堆上的空间。上述代码希望通过多态调用子类的func1,并且在析构的时候能够调用Derive的析构。
但很可惜,上面代码的执行结果是不满足要求的。下面是代码的运行结果,
Derive::func1()
~Base()
为什么只调用了父类的析构函数,而没有子类的析构函数呢?
原因很简单,虽然ptr->func1()是多态调用,但delete ptr却只是普通调用。调用delete会有两个行为:1.调用ptr类型的析构函数;2.析构ptr所指向的空间;由于ptr的类型是父类类型,所以只会调用父类的析构函数。
如果希望上述的析构能够正确执行,那么就需要实现析构函数的多态。
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual ~Base() { cout << "~Base()" << endl; }
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual ~Derive()
{
delete[] p;
cout << "~Derive()" << endl;
}
private:
int* p = new int[10];
};
int main()
{
//用父类指针指向子类,多态的前提条件之一
Base* ptr = new Derive;
//多态的调用
ptr->func1();
//ptr->~destructor() 多态调用
delete ptr;
return 0;
}
运行结果如下,
Derive::func1()
~Derive()
~Base()
虚函数表
C++的多态机制就是通过虚函数来实现的,而虚函数就是通过一张虚函数表(Virtual Table)来实现的。这个表中,主要存放一个类中的虚函数的地址,通过这张表来解决多态中的重写/覆盖的问题。其本质就是一个函数指针数组。当我们用父类的指针操作一个子类的成员函数时,这张表就像一张地图,指明了实际应该调用的函数。
C++编译器保证虚函数表的指针存放在了每个对象的最前面的位置。所以我们可以通过对象的地址来获取这张表,然后对其依次遍历,并调用其中的函数。
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
};
typedef void(*Vfptr)(); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
}
int main()
{
Base b;
Vfptr* ptr = (Vfptr*)*(void**)&b;
PrintVTable(ptr);
return 0;
}
上述代码中,我们先强行把&b转成void** 再解引用(转成void**可以避免32位与64位下地址大小的差异),取得虚函数表指针。由于此时地址还是void*,所以再将void的地址转换成Vfptr。在PrintVTable中,我们再根据虚函数表指针来依次调用虚函数表中的函数。
运行结果如下:
[0]->0x400998:Base::func1()
[1]->0x4009c2:Base::func2()
下面是Base的逻辑结构图:
在虚函数表中的最后一个节点,我用nullptr来表示结束节点,这个结束节点标志在不同的编译器下是不一样的,所以前面的代码中打印虚函数表的函数并不具有可移植性。但这并不是重要的问题,不会影响本章内容,所以这里不做修改。
下面将依次对单继承,多继承,棱形继承中的覆盖问题进行剖析。
单继承(无虚函数覆盖)
假设存在如下的继承关系,
其中子类没有重写任何父类的成员函数。那么其子类的虚函数表的逻辑结构图如下所示,
用代码证明:
class Base
{
public:
virtual void bfunc1() { cout << "Base::bfunc1()" << endl; }
virtual void bfunc2() { cout << "Base::bfunc2()" << endl; }
};
class Derive : public Base
{
public:
virtual void dfunc1() { cout << "Derive::dfunc1()" << endl; }
virtual void dfunc2() { cout << "Derive::dfunc2()" << endl; }
};
typedef void(*Vfptr)(); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
}
int main()
{
Derive d;
Vfptr* ptr = (Vfptr*)*(void**)&d;
cout << "Base切片中的虚函数表如下:" << endl;
PrintVTable(ptr);
return 0;
}
运行结果如下:
Base切片中的虚函数表如下:
[0]->0x4009f8:Base::bfunc1()
[1]->0x400a22:Base::bfunc2()
[2]->0x400a4c:Derive::dfunc1()
[3]->0x400a76:Derive::dfunc2()
通过示例,我们可以看到以下几点:
- 虚函数地址按照声明顺序放于表中。
- 虚函数表指针位于父类的切片当中。
- 父类的虚函数在子类虚函数的前头。
单继承(有虚函数覆盖)
如果没有虚函数覆盖,虚函数将毫无意义。假设存在下面的继承关系,
其中,子类重写了父类的func1()。子类的虚函数表将会是如下形式:
用代码证明:
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void bfunc2() { cout << "Base::bfunc2()" << endl; }
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void dfunc2() { cout << "Derive::dfunc2()" << endl; }
};
typedef void(*Vfptr)(); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
cout << endl;
}
int main()
{
Derive d;
Vfptr* ptr = (Vfptr*)*(void**)&d;
cout << "Base切片中虚函数表如下:" << endl;
PrintVTable(ptr);
return 0;
}
运行结果如下:
Base切片中虚函数表如下:
[0]->0x400a32:Derive::func1()
[1]->0x400a08:Base::bfunc2()
[2]->0x400a5c:Derive::dfunc2()
从示例中,我们可以看到以下几点:
- 重写的虚函数被覆盖到原本父类同名函数的位置上。
- 没有重写的虚函数依旧。
所以,我们也能对下面的调用做出合理的分析了,
Base* ptr = new Derive;
ptr->func1();
delete ptr;
其运行结果为:Derive::func1()
由ptr所指向的父类切片中的虚函数表的Base::func1()已经被子类中的Derive::func1()所取代,所以当我们用ptr调用时,实际上是Derive::func1()被调用。这就是多态的实现原理。
多态实现原理:使用父类指针或引用在父类的切片中寻找虚函数表指针,依靠这个指针寻到虚函数表,并查找虚函数表,调用函数名相同的函数。
多继承(无虚函数覆盖)
假设有如下继承关系,
其中Derive继承了Base1和Base2,并且没有完成重写。子类的虚函数表逻辑结构图如下,
代码证明:
typedef void(*Vfptr)(void); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
cout << endl;
}
class Base1
{
public:
virtual void b1func1() { cout << "Base1::b1func1()" << endl; }
virtual void b1func2() { cout << "Base1::b1func2()" << endl; }
virtual void b1func3() { cout << "Base1::b1func3()" << endl; }
};
class Base2
{
public:
virtual void b2func1() { cout << "Base2::b2func1()" << endl; }
virtual void b2func2() { cout << "Base2::b2func2()" << endl; }
virtual void b2func3() { cout << "Base2::b2func3()" << endl; }
};
class Derive : public Base1, public Base2
{
public:
virtual void d1func1() { cout << "Derive::d1func1()" << endl; }
virtual void d1func2() { cout << "Derive::d1func2()" << endl; }
};
int main()
{
Derive d;
Base1* ptr1 = &d;
PrintVTable((Vfptr*)*(void**)ptr1);
cout << "Base1切片的虚函数表如下:" << endl;
Base2* ptr2 = &d;
cout << "Base2切片的虚函数表如下:" << endl;
PrintVTable((Vfptr*)*(void**)ptr2);
return 0;
}
运行结果:
Base1切片的虚函数表如下:
[0]->00FD14B0:Base1::func1()
[1]->00FD14C4:Base1::b1func2()
[2]->00FD14B5:Base1::f1func3()
[3]->00FD1537:Derive::d1func1()
[4]->00FD153C:Derive::d1func2()
Base2切片的虚函数表如下:
[0]->00FD14BF:Base2::b2func1()
[1]->00FD14AB:Base2::b2func2()
[2]->00FD14CE:Base2::b2func3()
通过示例,可以发现以下几点:
- 在多继承中,子类的虚函数地址放在第一个继承的父类的虚表中。
- 每个父类切片单独维护一张虚表。
多继承(有虚函数覆盖)
假设有如下继承关系,
子类Derive继承自Base1和Base2,并且完成了对Base1::func1()和Base2::func1()的重写,则Derive的虚表逻辑结构图如下,
代码证明,
typedef void(*Vfptr)(void); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
cout << endl;
}
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void b1func2() { cout << "Base1::b1func2()" << endl; }
virtual void b1func3() { cout << "Base1::b1func3()" << endl; }
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void b2func2() { cout << "Base2::b2func2()" << endl; }
virtual void b2func3() { cout << "Base2::b2func3()" << endl; }
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void d1func2() { cout << "Derive::d1func2()" << endl; }
};
int main()
{
Derive d;
Base1* ptr1 = &d;
cout << "Base1切片的虚函数表如下:" << endl;
PrintVTable((Vfptr*)*(void**)ptr1);
Base2* ptr2 = &d;
cout << "Base2切片的虚函数表如下:" << endl;
PrintVTable((Vfptr*)*(void**)ptr2);
return 0;
}
运行结果:
Base1切片的虚函数表如下:
[0]->0010155A:Derive::func1()
[1]->001014C4:Base1::b1func2()
[2]->001014B5:Base1::b1func3()
[3]->0010153C:Derive::d1func2()
Base2切片的虚函数表如下:
[0]->00101550:Derive::func1()
[1]->001014AB:Base2::b2func2()
[2]->001014CE:Base2::b2func3()
从示例中,我们可以看到,凡是父类中被子类重写的成员函数,其父类的切片的虚表都会被子类重写的成员函数的地址进行覆盖。
所以我们可以用不同父类的指针调用子类重写的成员函数,
Derive d;
Base1* ptr1 = &d;
ptr1->func1();
Base2* ptr2 = &d;
ptr2->func1();
//运行结果:
Derive::func1()
Derive::func1()
棱形继承
假设有如下继承关系,
其中Base1和Base2继承自GParent,Derive又继承自Base1和Base2,Derive::func1()完成对Base1::func1()和Base2::func1()的重写。Derive的虚表逻辑结构图如下,
代码证明,
//棱形继承
typedef void(*Vfptr)(void); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
cout << endl;
}
class GParent
{
public:
virtual void gpfunc1() { cout << "GParent::gpfunc1()" << endl; }
int _gp_a;
};
class Base1 : public GParent
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void b1func2() { cout << "Base1::b1func2()" << endl; }
virtual void b1func3() { cout << "Base1::b1func3()" << endl; }
int _base_b1;
};
class Base2 : public GParent
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void b2func2() { cout << "Base2::b2func2()" << endl; }
virtual void b2func3() { cout << "Base2::b2func3()" << endl; }
int _base_b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void d1func2() { cout << "Derive::d1func2()" << endl; }
int _d;
};
int main()
{
Derive* d = new Derive;
Base1* ptr1 = d;
cout << "Base1切片中的GParent切片的虚函数表如下:" << endl;
PrintVTable((Vfptr*)*(void**)ptr1);
Base2* ptr2 = d;
cout << "Base2切片中的GParent切片的虚函数表如下:" << endl;
PrintVTable((Vfptr*)*(void**)ptr2);
delete d;
return 0;
}
运行结果,
Base1切片中的GParent切片的虚函数表如下:
[0]->00731087:GParent::gpfunc1()
[1]->00731294:Derive::func1()
[2]->00731410:Base1::b1func2()
[3]->007311B3:Base1::b1func3()
[4]->007313F7:Derive::d1func2()
Base2切片中的GParent切片的虚函数表如下:
[0]->00731087:GParent::gpfunc1()
[1]->007312BC:Derive::func1()
[2]->00731028:Base2::b2func2()
[3]->0073151E:Base2::b2func3()
从示例中,可以看到以下几点:
- 当有多重继承时,虚函数表指针存放在祖先的切片中。
- 虚函数表中的函数地址按照继承关系及声明顺序依次排列。
- 棱形继承会造成数据冗余,虚函数表中有两份gpfunc1的地址,Derive对象有两份_gp_a。
棱形虚拟继承
假设有如下继承关系,
Base1和Base2虚继承自GParent,Derive继承自Base1和Base2。Derive::func1()完成对Base1::func1()和Base2::func2()的重写。Derive的逻辑结构图如下,
代码证明如下,
//棱形虚拟继承多态
typedef void(*Vfptr)(void); //类型重定义
void PrintVTable(Vfptr* ptr)
{
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("[%d]->%p:", i, ptr[i]);
ptr[i](); //调用虚函数表中的所指向的函数
}
cout << endl;
}
class GParent
{
public:
virtual void gpfunc1() { cout << "GParent::gpfunc1()" << endl; }
int _gp_a = 10;
};
class Base1 : virtual public GParent
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void b1func2() { cout << "Base1::b1func2()" << endl; }
virtual void b1func3() { cout << "Base1::b1func3()" << endl; }
int _base_b1 = 20;
};
class Base2 : virtual public GParent
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void b2func2() { cout << "Base2::b2func2()" << endl; }
virtual void b2func3() { cout << "Base2::b2func3()" << endl; }
int _base_b2 = 30;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void d1func2() { cout << "Derive::d1func2()" << endl; }
int _d = 40;
};
int main()
{
Derive d;
Base1* ptr1 = &d;
Vfptr* vfptr1 = (Vfptr*)(*(void**)ptr1);
cout << "Base1切片中的虚函数表如下:" << endl;
PrintVTable(vfptr1);
Base2* ptr2 = &d;
Vfptr* vfptr2 = (Vfptr*)(*(void**)ptr2);
cout << "Base2切片中的虚函数表如下:" << endl;
PrintVTable(vfptr2);
GParent* ptr3 = &d;
Vfptr* vfptr3 = (Vfptr*)(*(void**)ptr3);
cout << "GParent切片中的虚函数表如下:" << endl;
PrintVTable(vfptr3);
return 0;
}
运行结果如下,
Base1切片中的虚函数表如下:
[0]->00A714B5:Derive::func1()
[1]->00A713AC:Base1::b1func2()
[2]->00A71177:Base1::b1func3()
[3]->00A71393:Derive::d1func2()
Base2切片中的虚函数表如下:
[0]->00A714BA:Derive::func1()
[1]->00A71028:Base2::b2func2()
[2]->00A714A1:Base2::b2func3()
GParent切片中的虚函数表如下:
[0]->00A7107D:GParent::gpfunc1()
从示例中,我们可以得出以下几点:
-
使用棱形虚拟继承后,有一块单独的祖父类的切片被放置在了子类的最尾部,该切片存放祖父类的虚函数表指针和其成员变量。
-
父类中祖父类的切片改为存放虚函数表指针(该表不再含有祖父类的虚函数地址)和虚基表指针(该表存放着该指针的地址到尾部祖父类切片中成员变量的地址的偏移量)。
几点注意事项:
-
在棱形虚拟继承中,我们可以使用GParent来获得Derive对象中尾部的祖父类切片。但是,如果是在棱形继承中,我们无法用GParent来获得祖父类切片,因为祖父类切片只在Base1和Base2中各有一份,编译器无法识别你所要的切片是哪一份。
-
在棱形虚拟继承中,如果两个父类都重写了祖父类的虚函数,那么子类也必须重写祖父类的虚函数。因为子类中的祖父类切片中的虚函数表只允许一个虚函数进行覆盖,但Base1和Base2都要对其祖父类的虚函数进行覆盖,编译器无法辨认用哪个父类虚函数地址进行覆盖。在棱形继承中,则不会有这种情况,因为Base1和Base2的切片中各有一张存放着GParent虚函数地址的虚表。