目录
前言
一、多态的概念
二、多态的定义与使用
1、多态的构成条件
2、虚函数
3、虚函数的重写(覆盖)
4、虚函数重写的两个例外
(1)协变
(2)析构函数的重写
5、子类的指针或者引用调用
6、C++11的override与final关键字
7、重载、重定义(隐藏)、重写(覆盖)之间的对比
三、抽象类
四、多态的原理
1、虚函数表
2、虚函数表的打印
3、多态的原理
4、静态绑定与动态绑定
五、单继承和多继承关系的虚函数表
1、单继承中的虚函数表
2、多继承中的虚函数表
前言
我们都知道类的三大特性分别为类的封装、继承与多态;前面我们介绍过了类的封装与继承,本章主要介绍类的多态这一性质,其实类的多态这一性质基于类的继承,可以说没有类的继承也就没有类的多态;
一、多态的概念
所谓多态即多种形态,指不同的对象取调用一个函数(看起来像一个函数)会产生多种不同的效果;举个例子,在我们日常通勤中,我们可能会乘坐公共汽车、高铁等等交通工具;但是当不同人去完成买票这一动作时,会产生不同的效果;学生去买票会以学生票的价格卖给学生,成人去买票会以成人的价格卖给成人,我们今天学的多态也是如此;
二、多态的定义与使用
1、多态的构成条件
我们想构成一个多态,必须有以下条件(缺一不可);
1、虚函数的重写
2、必须通过基类的指针或者引用调用虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person BuyTicket" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "Student BuyTicket" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student stu;
func(p);
func(stu);
return 0;
}
上述代码实现了多态,该代码仅仅只是为了让大家看看多态,下面会介绍相关概念;
2、虚函数
虚函数即为用关键字virtual修饰的成员函数;如上面的BuyTicket函数便是虚函数;
class A
{
public:
virtual void func() {}; // 虚函数
};
3、虚函数的重写(覆盖)
虚函数的重写,也叫覆盖,是构成多态的条件之一,子类中有一个与父类完全相同的虚函数,我们就称子类对该虚函数进行了重写(覆盖);这里的完全相同指的是返回值、函数参数、函数名相同,其中函数的参数只要参数类型相同即可,形参名可以不相同;
class A
{
public:
// 虚函数
virtual void func(int a1 = 1, double d1 = 2.0)
{
cout << "A: " << endl;
cout << a1 << ": " << d1 << endl;
};
};
class B : public A
{
public:
// 虚函数
virtual void func(int a2 = 10, double d2 = 20.0)
{
cout << "B: " << endl;
cout << a2 << ": " << d2 << endl;
};
};
void Func(A* a)
{
a->func();
}
int main()
{
A a;
B b;
Func(&a);
Func(&b);
return 0;
}
仔细观察上图与代码,我们发现虚函数的重写仅仅只是对实现进行了重写,我们父类的函数缺省值为1和2.0,而子类函数的缺省值为10和20.0,可打印出来的确实1和2,说明了虚函数的重写仅仅只是对实现部分进行了重写;
4、虚函数重写的两个例外
上面我们说过,要实现虚函数的重写必须实现三同,可是,这其实也有特殊例外的语法;以下分别一一介绍;
(1)协变
重写的虚函数可以返回值不同,但是他们的返回值必须为父子类关系的指针或引用;这种重写虚函数我们称为协变;如下所示;
class Person
{
public:
virtual Person& BuyTicket()
{
cout << "Person BuyTicket" << endl;
return *this;
}
};
class Student : public Person
{
public:
virtual Student& BuyTicket()
{
cout << "Student BuyTicket" << endl;
return *this;
}
};
void func(Person& p)
{
p.BuyTicket();
cout << endl;
}
int main()
{
Person p;
Student stu;
func(p);
func(stu);
return 0;
}
其中返回父子类的指针和引用并非必须返回本类的父子类,还可以返回别的父子类;
class A
{
public:
};
class B : public A
{
public:
};
class Student;
class Person
{
public:
virtual A* BuyTicket()
{
cout << "Person BuyTicket" << endl;
A* p = new A;
return p;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
cout << "Student BuyTicket" << endl;
B* p = new B;
return p;
}
};
void func(Person& p)
{
p.BuyTicket();
cout << endl;
}
int main()
{
Person p;
Student stu;
func(p);
func(stu);
return 0;
}
其中A、B类与Person、Student类并无关系,可是用他们作为返回值时,也可构成协变;其中父类写道父类虚函数的返回值中,子类写到子类虚函数的返回值中;
(2)析构函数的重写
析构函数的重写可以不用不用相同的名字,实际上,底层还是会将其改成相同的名字---destructor,只不过我们看着好像不同的函数名实现了多态;
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
virtual ~B()
{
cout << "~B()" << endl;
}
};
void Func(A* p)
{
// 如果传入一个子类指针,如果没有多态根本不能完全释放
delete p;
}
int main()
{
A* ptra = new A;
B* ptrb = new B;
Func(ptra);
Func(ptrb);
return 0;
}
注意:有一个小细节,父类的虚函数必须写virtual关键字,子类可以不用写virtual,直接对其重写;
5、子类的指针或者引用调用
在上述所有代码中,我们实现多态的第二个条件就是子类的指针或者引用来调用虚函数,这是实现多态的必要条件;上述代码均有体现;
6、C++11的override与final关键字
在C++11中,新加了这两个关键字;
1、final用于修饰该虚函数不能在被重写了;
class A
{
public:
virtual void func()final { }
};
class B : public A
{
public:
virtual void func() {};
};
2、override关键字用于检查子类中的某个虚函数是否被重写;
class A
{
public:
virtual void func(int x) { }
};
class B : public A
{
public:
virtual void func(double x)override {};
};
7、重载、重定义(隐藏)、重写(覆盖)之间的对比
这三个是我们之前学过的概念,很容易混淆,此处对其一一进行对比;
重载:
两个函数必须在同一个作用域中,且函数名相同,参数不同,底层使用函数名修饰规则实现;
重定义:
两个函数作用域必须分别在派生类与基类中,函数名相同即可;
重写:
两个函数作用域必须分别在派生类与基类中,函数名、参数、返回值都必须相同(除了那两个特例除外),两个函数必须是虚函数(基类必须加virtual关键字);
三、抽象类
虚函数后买你加 = 0,这个函数就被称为纯虚函数,包含纯虚函数的类被称为抽象类;抽象类不能实例化处对象,继承后派生类也不可实例化处对象,除非对纯虚函数进行重写;
class Base
{
public:
// 纯虚函数
virtual void func() = 0;
};
class Derive : public Base
{
// 重写纯虚函数
//virtual void func(){}
};
int main()
{
//Base b;
Derive d;
return 0;
}
纯虚函数可以有自己的函数体;只是有纯虚函数的类不能实例化处对象;
四、多态的原理
1、虚函数表
首先,给大家一道面试题,以下代码结果是多少;
// 32位机器下
class Base
{
public:
virtual void func()
{}
int _a;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
结果是否超出你的预料呢? 当我们将一个函数声明为虚函数时,该类内会多出一个指针,这个指针被称为虚函数表指针,简称虚表指针;因此结果为8字节;
我们再通过内存窗口来观察这个类,如下所示;
虚函数表实际上是一个函数指针数组,里面储存的是虚函数指针,在VS系列编译器中通常以 nullptr 结尾;后面我们打印虚表也是通过这一特性进行打印;我们接着加入继承,继续观察;
class Base
{
public:
virtual void func1()
{
cout << "Base func1" << endl;
}
virtual void func2()
{
cout << "Base func2" << endl;
}
void func3() { }
int _a;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive func1" << endl;
}
int _b;
};
int main()
{
Base b;
Derive d;
return 0;
}
上述代码,我们新增了一个派生类,派生类对第一个虚函数进行了重写,然后在基类上增加了一个虚函数,一个普通成语函数;
首先,通过监视窗口,我们发现派生类确实继承了父类的虚函数表,但是我们仔细观察发现虚函数表的地址不同,表中存放的虚函数地址也不同,其中,第一个虚函数我们对其进行了重写,因此第一个函数指针的地址不同,第二个虚函数我们并未对其进行重写,因此地址相同,而父类除了增加虚函数外,还增加了一个普通成员函数,普通成员函数并没存进虚函数表中;
总结一下:
关于虚函数表(也称虚表),虚表也会被子类继承,只不过虚表被继承是子类拷贝父类的虚表,然后判断子类是否对其中某个虚函数进行重写,若重写,则用新的函数地址覆盖在原来虚表上的地址,若派生类新增了虚函数,也会继续依次填充在虚表后,虚表只会存放虚函数的地址;
还有一些问题,我带着大家一起验证,关于虚表存在哪里、虚函数又存在哪里?有许多小伙伴对其充满疑惑;
首先解答,虚表存在哪里?
// 该测试代码仅仅限于32位机器下
class Base
{
public:
virtual void func1()
{
cout << "Base func1" << endl;
}
virtual void func2()
{
cout << "Base func2" << endl;
}
void func3() { }
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive func1" << endl;
}
int _b = 2;
};
int main()
{
Base ba;
Derive d;
int a = 10;
int* pa = new int;
static int b = 20;
const char* c = "xxxxxxxxxxxxxxxxxx";
printf("栈区: %p\n", &a);
printf("堆区: %p\n", pa);
printf("静态区: %p\n", &b);
printf("常量区: %p\n", c);
printf("父类对象虚表地址: %p\n", *(int*)&ba);
printf("子类对象虚表虚表地址: %p\n", *(int*)&d);
return 0;
}
不难看出,虚表的地址更接近与常量区,因此不难推测虚表存放在常量区,当然,往上也有一部分人说虚表存在静态区;
虚函数又存放在哪呢?
与大部分函数一样,虚函数也是存在代码段中的,此处补充一个小知识,代码段中存的并不是我们写的代码,而是二进制机器代码,我们写的代码首先转换成汇编代码,然后再由汇编代码转换成二进制机器代码;
2、虚函数表的打印
前面我们也说了,在VS系类的编译器下,虚函数表的最后一位会补上 nullptr ,因此我们可通过这一特性打印我们的虚函数表,还是上面那个类,此处就不重复了;
// 函数指针重定义
typedef void (*Vf_ptr)();
void PrintVf_ptr(Vf_ptr table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("table[%d]: %p ->", i, table[i]);
Vf_ptr f = table[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
Base* pa = &b;
Derive* pd = &d;
// 写法一
PrintVf_ptr(*(Vf_ptr**)pa);
PrintVf_ptr(*(Vf_ptr**)pd);
// 写法二 只是用于32位机器
//PrintVf_ptr((Vf_ptr*)*(int*)pa);
//PrintVf_ptr((Vf_ptr*)*(int*)pd);
return 0;
}
3、多态的原理
前面说了这么多,也只是为多态的原理进行铺垫;不止小伙伴们是否记得构成多态的基本条件,其中有一个是必须用父类的指针调用,那么为什么必须用父类的指针进行调用呢?我们假设不用父类的指针或引用,我们就使用值传递;拿以下类讲解;
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person BuyTicket" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "Student BuyTicket" << endl;
}
};
// 这里使用值传递
void func(Person p)
{
p.BuyTicket();
cout << endl;
}
int main()
{
Person p;
Student stu;
func(p);
func(stu);
return 0;
}
如果是值传递这里必然涉及拷贝的问题,那么虚表指针拷贝么,如果是拷贝,那么是拷贝父类虚表指针还是拷贝子类的虚表指针呢?这是不确定的,因为我们在调用这个函数里,不知道将来会被父类调用,还是子类调用,因此无法确定拷贝哪个虚表指针,因此必须要用父类的指针或者引用;而不能用切片直接传值;
那么是如何实现的呢?当我们传指针或者引用时,加入原对象是父类对象,则直接传,若是子类对象,则会发生我们之前讲过的赋值兼容(切片);
如果传过去的是父类,则类中存的是父类的虚表指针,而传过去的是子类,类中存的是子类的虚表指针;所以有了如下调用逻辑;
4、静态绑定与动态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
五、单继承和多继承关系的虚函数表
1、单继承中的虚函数表
单继承中,父类的虚函数放在父类的虚函数表中,子类重写于父类的虚函数放在子类的虚函数表中,子类定义的虚函数也放在子类的虚函数表中;(放在理解为其指针存在虚函数表中);
class A
{
public:
virtual void func1()
{
cout << "A::func1" << endl;
}
virtual void func2()
{
cout << "A::func2" << endl;
}
int _a = 1;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1" << endl;
}
virtual void func3()
{
cout << "A::func1" << endl;
}
int _b = 2;
};
int main()
{
A a;
B b;
return 0;
}
父类的虚函数表中存放了func1与func2地址,而子类虚函数表中,存放了重写的func1,因此地址不同;以及继承父类没有重写的func2,因此地址相同;还有在子类定义的虚函数func3;
2、多继承中的虚函数表
在多继承的体系下,又是如何继承的呢?首先我们好奇的是,多继承体系下,会有几张虚表呢?即派生类会有几个虚表指针呢?我们做了如下测试;
class Base1
{
public:
virtual void func1() { cout << "Base1 void func1()" << endl; }
virtual void func2() { cout << "Base1 void func2()" << endl; }
int _b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2 void func1()" << endl; }
virtual void func2() { cout << "Base2 void func2()" << endl; }
int _b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive void func1()" << endl; }
virtual void func3() { cout << "Derive void func3()" << endl; }
int _d;
};
int main()
{
Derive d;
return 0;
}
通过监视窗口不难看出,d类中有两个虚表指针,意味着有两种张虚表;那么问题又来了,我们在派生类定义的虚函数func3存在先继承的Base1中的虚表,还是存在Base2中的虚表呢?我们使用前面的虚表打印的代码,进行测试;结果如下;
class Base1
{
public:
virtual void func1() { cout << "Base1 void func1()" << endl; }
virtual void func2() { cout << "Base1 void func2()" << endl; }
int _b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2 void func1()" << endl; }
virtual void func2() { cout << "Base2 void func2()" << endl; }
int _b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive void func1()" << endl; }
virtual void func3() { cout << "Derive void func3()" << endl; }
int _d;
};
// 函数指针重定义
typedef void (*Vf_ptr)();
void PrintVf_ptr(Vf_ptr table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("table[%d]: %p ->", i, table[i]);
Vf_ptr f = table[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
// 打印Base1虚表
PrintVf_ptr(*(Vf_ptr**)&d);
// 打印Base2虚表
Base2* ptr = &d; // 切片
PrintVf_ptr(*(Vf_ptr**)ptr);
return 0;
}
经过测试,我们发现我们在Derive新定义的虚函数,存在了先继承的Base1的虚表中,仔细观察的小伙伴们注意到了,Derive对func2没有进行重写,因此,我们继承的两张虚表的func2函数的地址不同可以理解,因为是两个不同的函数,可是为什么重写的func1的地址也不同呢?我们可以看到,他们明明是调用的同一个函数(—>后面是函数执行结果),那为什么地址不同呢??
int main()
{
Derive d;
Base1* ptr1 = &d;
Base2* ptr2 = &d;
// 分别调用两个虚函数表中重写的func1
ptr1->func1();
ptr2->func1();
}
关于调用同一个函数,这个函数却有两个地址的问题,我们还得观察汇编代码;接下来我带着大家一起观察我们程序的汇编代码;(call为汇编指令中的函数调用指令,后面接函数地址,而jmp为跳转指令,后接地址)
接着我们看Base2中的func1是在汇编代码中是如何调用的;
重新捋一下思路,在多重继承下,子类继承了两个及以上的来自父类的虚表,我们重写来自父类的虚函数(且这个虚函数多个父类都有)时,我们会对其重写,重写的虚函数只有一个,两个虚表中,各保存一份,上述问题讨论的是,为什么同一个虚函数地址不同;
观察汇编代码,我们发现,保存在Base2中的func1会多经过几次跳转;但最终还是会来到最终的函数入口地址;完成调用;其实与this指针有关;当我们分别用Base1*与Base2*类型的指针调用func1时,模型图如下所示;
当我们用Base2*调用func1时,其中有一个动作时减8,其实那个动作正是调整this指针的指向; 由于Base1*的指向本来就是子类Derive的开始,因此不用调整,所以两个虚函数表中的func1地址不同的本质原因是,Base2*调用fucn1时还需要调用一个调整this指针的动作;