一、多态的概念:
多态是面向对象第三大特征,多态是具有表现多种形态的能力的特征。
多态就是多种形态,当不同的对象完成某个相同的行为就会产生不同的状态。
二、抽象类:
1、概念:
在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫做接口类。抽象类无法实例化出对象。抽象类的子类也无法实例化出对象,除非重写父类的虚函数。
class Car
{public:virtual void fun() = 0;
};
如上就是一个抽象类,
作用:
1,强制派生类重写虚函数,完成多态。
2,表示某些抽象类。
2、实现继承和接口继承:
实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
三、多态的实现:
1、构成条件:
1、必须通过基类的指针或者引用调用虚函数
2、被调用的函数是虚函数,且必须在派生类中完成对基类虚函数的重写如下举个例子:
class Person
{
public:virtual void func(){cout << "买全价票" << endl;}
};
class Student :public Person
{
public:virtual void func(){cout << "买半价票" << endl;}
};
void functest(Person& p)
{p.func();
}
int main()
{Person p;functest(p);Student st;functest(st);return 0;
}
如上所示,在构成条件成立的时候,就会进行成多态,两者调用同一个函数,p和st先通过functest传参过去,st的话会进行切片,然后二者调用同一个函数,但是出现的结果是不同的,这就是多态。
如果不使用关键字virtual那么就不会构成多态的条件,此时就只会调用Person的func,父类的成员函数
这说明了如果函数满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。
这就是多态的调用看指向的对象。
如果不满足多态,也就是普通对象,编译器会直接根据当前类型去调用虚函数。
2、虚函数:
1、被virtual修饰的函数
2、这个函数必须是类中的,非静态的成员函数
3、虚函数的重写:
重写的条件:
在派生类中有一个和基类三同(函数名相同,函数返回值相同,函数形参的类型相同)的虚函数,
就称为派生类中的虚函数重写了基类中的虚函数
class Person
{
public:virtual void func(){cout << "买全价票" << endl;}
};
class Student :public Person
{
public:virtual void func(){cout << "买半价票" << endl;}
};
如上就是在派生类中重写了虚函数func,
注意:
在派生类中,重写虚函数的时候virtual可以不用写,但是最好还是写上,这样增加了可读性
注意:
虚函数重写中,重写的是虚函数的实现,就是{}里面的部分,但是缺省参数是没有重写的那部分的值
但是在虚函数重写中有两个例外:
例外1、协变:
1、定义:
如果一个虚函数在基类中返回的是基类类型的指针或引用,
那么在派生类可以重写该虚函数并返回该派生类类型的指针或引用。
2、使用条件:
基类中的函数必须是虚函数;
派生类中重写的虚函数的返回类型必须是基类虚函数返回类型的子类型 如下的B是A的子类型
class A
{};
class B :public A
{};
class Person
{
public:virtual A* func(){cout << "买全价票" << endl;return nullptr;}
};
class Student :public Person
{
public:virtual B* func(){cout << "买半价票" << endl;return nullptr;}
};
例外2、析构函数的重写:
如果将基类中的析构函数加上virtual修饰成虚函数,那么在派生类中就不需要加上virtual也构成虚函数重写,它们参数,返回值(都没有)都是一样的,至于函数名,编译器会将它们都处理成destructor,这样就三同了,就构成重载了
class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;}
};
class Student :public Person
{
public:virtual ~Student(){cout << "~Student" << endl;}
};
int main()
{Person* p = new Person;delete p;p = new Student;delete p;return 0;
}
如上所示Student和Person的析构函数构成虚函数重载,析构函数的调用跟指针指向的对象有关,而跟指针的类型无关,这样就是多态调用。
如果向上面一样,不使用virtual修饰而不进行多态调用,取而代之的是普通调用,这样p指向的Student就不会析构,就会造成内存泄漏。
4、C++11引入的final和override
1、final:
final修饰的虚函数不能够进行重写,final修饰的类不能够被继承。
2、override:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
5、重载&&覆盖(重写)&&隐藏(重定义)
重载:
两个函数重载,必须在同一个作用域中,这两个函数必须是函数名相同,函数形参不同
覆盖(重写):
两个函数重写,一个函数要在基类的作用域,另外一个要在这个基类的派生类的作用域中,
且重写的两个函数必须是虚函数,
返回值,参数列表,函数名必须完全相同(协变除外)。
简单来说就是虚函数+三同
隐藏(重定义):
两个函数重定义,一个函数要在基类的作用域,另外一个要在这个基类的派生类的作用域中,派生类和基类的成员变量相同或者函数名相同,子类隐藏父类的对应成员或者对应函数
注意:
只要是基类和派生类的同名函数,不是重写就是重定义。
四、多态的底层实现原理:
1、虚函数表:
首先从代码引入看看:
从上图的是三个红框可以看到,在test类中本来只有int _a,但是实际上还多一个_vfptr这个指针,这个指针叫做虚函数表指针(virtual function pointer),在一个含有虚函数的类中,至少含有一个虚函数表的指针,因为虚函数的地址要被放在虚函数表中,当我们调用虚函数的时候,编译器就会通过这个指针找到虚函数表(虚表),在里面再调用这个函数。
关于虚函数表的存储位置可以用代码来证明:
class Person
{
public:virtual void func(){cout << "买票全价" << endl;}
protected:int _a;
};class Student : public Person
{
public:virtual void func(){cout << "买票半价" << endl;}
protected:int _b;
};int main()
{Person ps;Student st;int a = 0;printf("栈:%p\n", &a);static int b = 0;printf("静态区:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* str = "xxxxxxx";printf("常量区:%p\n", str);printf("虚表1:%p\n", *(int*)&ps);printf("虚表2:%p\n", *(int*)&st);return 0;
}
通过上述可以看到,虚表和常量区最接近,也就是说虚表存储在常量区的
2、在继承中的虚函数表:
class test
{
public:virtual void func1(){cout << "func1()" << endl;}virtual void func2(){cout << "func2()" << endl;}void func3(){cout << "func3()" << endl;}private:int _a = 1;
};class test1 : public test
{
public:virtual void func1(){cout << "test1::func1()" << endl;}virtual void func4(){cout << "test1::func4()" << endl;}
private:int _b = 2;
};int main()
{test T1;test1 T2;return 0;
}
1、在如上的监视窗口中,我们可以看到在基类中,如果用virtual修饰成为虚函数后,就会在虚表中找到对应的函数,如func1和func2,那么在test1继承test后,同样也会有一个指针指向派生类的虚表,里面放着部分基类继承下来的成员,另一部分就是自己的成员
2、如果在派生类中进行重写,那么这个重写后的地址就会被覆盖,如下面的func1,其地址就会发生变化,如果在派生类中没有被重写,那么在派生类的虚表中就不会管,如下的func2的地址就没变。
3、如果在基类中不是虚函数,那么继承后就不会放在虚表中,如上面的func3
4、派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
但是在派生类中明明还有一个自己的虚函数,那么在监视窗口中却没有看到。
那么将指向虚函数表的指针在内存中看看,
通过下图可以看到,在内存中,存在一个00x0035126c这个地址的函数,可以初步怀疑这个就是派生类中的fun4,只不过在监视窗口没有显示出来罢了。
ps:虚函数表其实就是一个存虚函数指针的指针数组,在vs编译器中,虚表的最后一个是以nullptr结尾的,但是在别的环境中就不一定是。
那么这个00x0035126c是不是派生类中的虚函数呢?
接下来通过打印虚表函数来进行验证:
typedef void(*FUNC_PTR) ();
void PrintVFP(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PTR f = table[i];f();}printf("\n");
}
以上是一个通过[ ]访问这个指针数组,来进行打印的,后面通过函数的调用来进行显示打印
通过上述的试验结果可以看到,这个地址就是func4,这个就是在监视窗口没有显示的函数。
3、多态的底层原理:
如上,当是满足多态的时候,就会根据指针找到对应对象的虚函数表,然后找虚函数表里面虚函数的地址,然后访问虚函数。
4、动态绑定与静态绑定:
静态(编译时)的多态:函数重载。
动态(运行时)的多态:继承,虚函数重写,实现的多态。
运行时,去虚表里面去找,找到后进行调用。
如下:
T1就是静态的时候,在编译时就知道要调用func1
但是PT2就是直接找到所调用函数的地址,编译时不知道调用的是谁。
五、继承中的虚函数表:
1、单继承中的虚函数:
单继承其实上面就是的,将父类的虚表拷贝过来,然后如果有重写就重写后将父类的原虚函数进行覆盖,如果子类还有其他的虚函数就加到后面,最后以nullptr结尾
2、多继承中的虚函数:
首先引入多继承中的虚函数的代码:
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;
};int main()
{Derive d;int pt1 = *((int*)&d);PrintVFP((FUNC_PTR*)pt1);Base2* ptr = &d;int pt2 = *((int*)ptr);PrintVFP((FUNC_PTR*)pt2);return 0;
}
如上所示,在调试窗口中,可以看到,如下所示的结构,Base1和Base2里的虚表都继承给了Derive,之后将func1函数重写。
通过上面分析,有两个问题,一个是在监视窗口并没有看到Derive中的func3,另一个问题是重写后,在Base1和Base2中func1函数的地址不一样,但是调用的函数是一样的。
接下来我们通过打印函数来看看第一个问题:
如下,可以看到func3放在Base1里面的。
并且通过内存也可以看到func3是在Base1中fun2之后的。
另一个问题:为什么在Base1和Base2中func1函数的地址不一样,但是调用的函数是一样的。接下来上测试代码:
int main()
{Derive d;Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();return 0;
}
上面是直接通过地址调用func1的,接下来看汇编来进行理解
通过上图可以看到,Base1调用func1是直接调用的,但是Base2却不是,如下所示,func1是Derive重写的函数,在调用的时候Base1类型的指针是和Derive类型正好重合了,这样直接调用它就可以了,
但是Base2却不是,那么就需要多一步,将指针向上偏移Base1的大小,这样就可以重合了,就可以调用了