文章目录
多态的概念
多态:通俗的来说就是多种形态,对于不同的情况,会产生不同的状态
举个例子:例如车站的卖票系统,对于学生、普通市民和军人三类人的卖票政策是完全不同的,例如:对于普通市民是全家买票、对于学生是半价买票、对于军人是全价买票,排队优先。但是这三种角色中又有继承关系,所以这里就可以用到类的多态,通过虚函数完成不同角色同一函数实现不同功能。
多态的条件
- 被调用的必须是虚函数,子类必须重写父类的虚函数
- 必须通过 基类 的指针或者引用调用虚函数
class Person //基类
{
public:
virtual void print()
{
cout << "全价买票" << endl;
}
};
class Student:public Person
{
public:
virtual void print() //studen 类中对print函数的重写
{
cout << "半价买票" << endl;
}
};
class Soldier:public Person
{
public:
void print() //soldier类中对print的重写
{
cout << "排队优先,全价买票" << endl;
}
};
void buyticket(Person& s) //这里参数形式还可以写成指针的形式 即:Person* p
{
s.print(); //p->print();
}
int main()
{
Person s1;
Student s2;
Soldier s3;
buyticket(s1);
buyticket(s2);
buyticket(s3);
}
下面来了解一下多态的实现工具——虚函数
虚函数
虚函数: 就是被virtual修饰的函数称为虚函数。上面代码中类Person中的print函数就是虚函数。
class Person //基类
{
public:
virtual void print()
{
cout << "全价买票" << endl;
}
};
虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数的返回值类型、函数名字、参数列表完全相同),称子类虚函数重写了基类的虚函数。
注意:这里virtual关键字和虚继承时virtual关键字没有半毛钱关系,一个是修饰类的,一个修饰函数的。
普通调用vs多态调用
#include <iostream>
using namespace std;
class A
{
public:
virtual void func1()
{
cout << "A::i am func1" << endl;
}
void func2()
{
cout << "A::i am func2" << endl;
}
};
class B : public A
{
public:
void func1()
{
cout << "B::i am func1" << endl;
}
void func2()
{
cout << "B::i am func2" << endl;
}
};
int main()
{
A *ptr1 = new A;
A *ptr2 = new B;
cout << "多态调用" << endl;
ptr1->func1();
ptr2->func1();
cout << endl << "普通调用" << endl;
ptr1->func2();
ptr2->func2();
}
结果如下:
所以我们可以总结出:
- 普通调用:跟调用对象指针类型有关
- 多态调用:和指向对象类型有关
但是一定要记住多态调用一定要满足多态的前提!
虚函数的三个特例
- 派生类在重写虚函数的时候可以不加virtual关键字,但是重写时必须严格保持虚函数重写的规则,即:返回值类型、函数名字、参数列表
例如上面的soldier类,print函数虽然没有加上virtual关键字,但是严格按照虚函数重写的规则,所以也保留了虚函数的特性,所以也完成了虚函数的重写
class Soldier:public Person
{
public:
void print() //soldier类中对print的重写
{
cout << "排队优先,全价买票" << endl;
}
};
- 协变:基类和派生类虚函数返回值类型不同
这个特性有点坑(实际中没有多少应用),前面刚说返回值必须相同,这里又开了一个特例:返回值可以不同,但是返回值之间必须要有继承关系,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针和引用。
class A{};
class B :public A {};
class Person
{
public:
virtual A& print()
{
cout << "全价买票" << endl;
}
};
class Student:public Person
{
public:
B& print()
{
cout << "半价买票" << endl;
}
virtual void fun3()
{
cout << "fun3()" << endl;
}
};
- 析构函数的重写
如果基类的析构函数为虚函数,此时只要派生类定义析构函数,都与基类的析构函数构成重写。虽然他们两的函数名不同但是依然构成重写,这是因为析构函数都会被编译器解释为同一个名为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;
delete p2;
}
例如上面这段代码,如果析构函数不设置成虚函数的话,由于发生了切片,所以指针p1
、p2
指向的都是Person的析构函数。所以调用的结果为
但是析构函数写成虚构函数,那么p2调用的就不是Person的虚构函数,这个的原因要等到下面学习完多态的底层原理之后才能完全了解,这里我们要明白的是:虽然两个析构函数函数名不同看似不满足函数的重写,但是编译器底层会对析构函数名统一处理成destructor,所以在编译器看来两个析构函数名是相同的。但是为什么加上析构函数后p2指针会指向student的析构函数等下面深入了解了多态的原理就会明白了。
多态C++11新增的特性
- 关键字 ——
final
1. 修饰虚函数,表示该函数不能被重写
2. 修饰类,表示该类不可以被继承(但其实让一个类不能被继承还可以把该类的构造函数放到private里面也能达到同样的效果)
class A
{
public:
virtual void Drive() final {}
};
class B :public A
{
public:
virtual void Drive(){}
};
加上final关键字之后就会出现如下报错:
- 关键字 ——
override
检查派生类虚函数是否重写基类里面的某个虚函数,如果没有重写就编译报错
class A
{
public:
virtual void Drive() {}
};
class B :public A
{
public:
virtual void Drive()override { cout << "B" << endl; }
};
重载 && 重写(覆盖)&& 重定义(隐藏)
抽象类
概念
- 纯虚函数:在虚函数后面加上
=0
,虚函数就变成了纯虚函数 - 包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,只有重写抽象类所有纯虚函数才能实例化出对象。纯虚函数规定了派生类必须重写更加体现了接口继承
class A
{
public:
virtual void fun()=0 {} //虚函数后面加上=0就变成了纯虚函数,包含纯虚函数的类A也就变成了抽象类
};
class B :public A
{
public:
B(int x=0)
:_b(x)
{
}
//virtual void fun() { cout << "over write" << endl; } //B继承于A,如果不重写继承下来的纯虚函数那么类B也是纯虚类不能初始化对象
private:
int _b;
};
int main()
{
B b;
}
抽象类实际上是一种强制接口继承,你不重写纯虚函数连对象都无法实例化
多态原理
类的多态是如何实现的呢?编译器是如何处理虚函数的呢?我们先从一个例子入手:
class A
{
public:
virtual void fun(){}
private:
char a;
int b;
};
int main()
{
A a;
cout << sizeof(a) << endl;
}
计算类A的大小,前面在学习自定义类型时说过自定义类型大小计算时本质是算偏移量遵循内存对齐,具体可以参考这边博客自定义类型。按照内存对齐可以算出该内存为8,但是实际上:
打开内存调试窗口就会发现对象a里面多出了一个指针
这个指针指向了一块内存,该内存存储了类A里面所有包含的虚函数的地址,这个指我们也把他叫做虚表指针,指向的存储虚函数的地址的内存(也就是虚表指针指向的内存)叫做虚表。 而虚函数表指针一般分配在类的首四个字节
那么如果我们再加上类的继承,虚表指针里面会发生什么变化?
class A
{
public:
virtual void fun1() { cout << "A::fun1" << endl; }
virtual void fun2() { cout << "A::fun2" << endl; }
void fun3() { cout << "A::fun3" << endl; }
private:
char a;
int b;
};
class B :public A
{
public:
void fun1() { cout << "B::fun1()" << endl; } //只重写了基类的fun1函数
};
int main()
{
A a;
B b;
}
得出以下结论:
- 虚表指针也会被派生类继承下来,图中对象b中就从类A中将虚表指针和类A成员全部继承下来了。
- 其次我们发现fun3函数并没有出现在虚表上面说明只有虚函数地址才能存在虚表里面,但是要注意fun3也继承下来了,派生类中也可调用fun3
- 我们发现基类和派生类中的虚表中存的函数地址不一样,这是由于派生类B中将虚函数fun2重写,所以第一个虚函数表中第一个函数地址是不相同的,fun2继承下来是虚函数但是由于没有重写,所以放进了虚表中
- 不同类的虚表地址不同,同一个类中的虚表地址相同
- 虚函数表本质是一个函数指针数组,这个数组一般是以一个nullptr结尾(这个可以通过内存监视窗口查看)
易混淆
虚函数存在哪里?虚表存在哪里?
经典错误答案: 虚函数存在虚表里面,虚表存在对象里面(大错特错)
正确解答: 虚函数是代码,所以应该存在代码段,虚表里面注意存的是虚函数的地址。虚表不存在对象里面,对象里面存的是虚表的地址
虚函数调用时多态的原理
class A
{
public:
virtual void fun1() { cout << "A::fun1" << endl; }
virtual void fun2() { cout << "A::fun1" << endl; }
void fun3() { cout << "A::fun3" << endl; }
private:
char a;
int b;
};
class B :public A
{
public:
void fun1() { cout << "B::fun1()" << endl; }
};
void function(A& s) //虚函数多态的调用方法一
{
s.fun1();
}
void function(A* s) //方法二
{
s->fun1();
}
void function_false(A s) //方法三
{
s.fun1();
}
int main()
{
A a;
B b;
cout << "function(A& s)" << endl;
function(a);
function(b);
cout << endl << "function(A* s)" << endl;
function(&a);
function(&b);
cout << endl << "function_false(A s)" << endl;
function_false(a);
function_false(b);
}
多态的实现只能传入指针 和 引用 ,而不能传入对象。例如上面的方法三以对象作为参数,得到的结果就是不正确的,而方法一、二就实现了函数的多态。
以方法二为例
1:这个赋值的过程发生了切割,s实际上指向的是对象b中类A的成员和虚函数表
2:在虚函数表根据函数名找到函数的地址,并调用该函数
那为什么用对象作为参数就不行呢?
还是上面类之间的继承关系
int main()
{
A a;
B b;
printf("a对象的虚表地址:%p\n", *(int*)&a); //打印出对象a的虚表地址
printf("b对象的虚表地址:%p\n", *(int*)&b); //打印出对象b的虚表地址
A* p1 = &b;
A& p2 = b;
A p3 = b;
printf("p1对象的虚表地址:%p\n", *(int*)p1); //以指针形式传参
printf("p2对象的虚表地址:%p\n", *(int*)&p2); //以引用的形式传参
printf("p3对象的虚表地址:%p\n", *(int*)&p3); //以对象的形式传参
}
运行结果如下:
我们发现以指针和引用传参之后虚表地址依然是类B的虚表地址,但是用对象进行传参之后p3虚表的地址就变成了类A的虚表地址。这是因为p1、p2在赋值的过程中值发生了切片。而p3的赋值过程实际上是一种拷贝赋值构造,而虚表地址的初始化就是在构造函数的时候初始化,拷贝的过程并不拷贝虚表指针,所以只能使用接受类型的虚表指针
单继承的虚表
class A
{
public:
virtual void fun1() { cout << "A::fun1()" << endl; }
virtual void fun2() { cout << "A::fun2()" << endl; }
void fun3() { cout << "A::fun3" << endl; }
private:
char a;
int b;
};
class B :public A
{
public:
void fun1() { cout << "B::fun1()" << endl; }
virtual void fun4() { cout << "B::fun4()" << endl; }
virtual void fun5() { cout << "B::fun5()" << endl; }
};
int main()
{
A a;
B b;
printf("打印a对象的虚表:\n");
int* pa =(int *)*(int*)&a; //(int*)&a拿到的是对象a的前四个字节的地址,该地址解引用就是虚表的地址,最后将改地址强转成int*类型,这是为了下面pa++跳过四个字节
int i = 0;
while (*pa != NULL)
{
printf("a虚函数表vfptr[%d]:%p 函数运行结果:",i++,*pa);
((void(*)()) (*pa))(); //这里要把函数的地址强转成函数指针类型才能调用函数
pa++;
}
printf("\n");
printf("打印b对象的虚表:\n");
int* pb = (int*)*(int*)&b;
i = 0;
while (*pb != NULL)
{
printf("a虚函数表vfptr[%d]:%p 函数运行结果:", i++, *pb);
((void(*)()) (*pb))();
pb++;
}
}
当我们创建了类B的对象b的时候理论上虚表上应该有四个虚函数的地址,分别是:重写的fun1,继承下来的fun2、和自己定义的虚函数fun4、fun5,但是我们打开监视窗口发现虚表里面只有前两个函数的地址。这里实际上就是编译器的问题,编译器就是不显示后面两个函数😅,我们这里可以利用指针将虚表里面的内容全部打印出来(由于虚表一般以一个NULL指针结尾,所以我们就可以遍历整个函数指针数组),上面代码运行结果如下:
多继承的多态
class A
{
public:
virtual void fun1() { cout << "A::fun1()" << endl; }
virtual void fun2() { cout << "A::fun2()" << endl; }
};
class B /*:public A*/
{
public:
void fun1() { cout << "B::fun1()" << endl; }
virtual void fun3() { cout << "B::fun3()" << endl; }
virtual void fun4() { cout << "B::fun4()" << endl; }
};
class C :public A, public B
{
public:
virtual void fun5() { cout << "C::fun5()" << endl; }
};
int main()
{
C c;
printf("打印C对象的基类A虚表:\n");
int* pa = (int*)*(int*)&c;
int i = 0;
while (*pa != NULL)
{
printf("a虚函数表vfptr[%d]:%p 函数运行结果:", i++, *pa);
((void(*)()) (*pa))();
pa++;
}
printf("\n");
printf("打印C对象的基类B虚表:\n");
int* pb = (int*)*(int*)((B*)&c); //这里对对象c进行一个切片,这样(B*)&c 就指向了c中基类B部分的首地址
while (*pb != NULL)
{
printf("a虚函数表vfptr[%d]:%p 函数运行结果:", i++, *pb);
((void(*)()) (*pb))();
pb++;
}
}
上图如果还是搞不清楚指针为什么要强转成(B*)
,建议复习一下指针类型的意义
我们发现首先是多继承会出现两张虚函数表:分别是从类A和类B上继承下来的。其次派生类自己创建的虚函数是放在第一张虚函数表中,例如图中的fun5()
继承多态的问题
切片问题
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive:public Base1,public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
p1 p2 p3
之间的关系是怎么样的呢?答案是 p1==p3!=p2
首先解释一下答案,因为这里前两个发生了切片,Derive的内存分配如图:
那么p1和p3值相等区别在哪里?
区别在于指针的类型,也就是p3++跳过sizeof(Derive)
个字节,而p1++跳过sizeof(Base1)
,这两个的区别类似于函数名和函数地址的区别。
接口继承 vs 实现继承
class A
{
public:
virtual void func(int val =1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val=0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B *p = new B;
p->test();
return 0;
}
这里的结果是:
首先p->test()
调用的是类B从类A继承下来的test()函数这点毫无疑问,在test()函数中调用func()函数,这里的调用写成this->func()
会更好理解,注意这里的this指针的类型是A*
!,所以这里this->func()
this指针是基类的指针func又是重写了的虚函数,所以构成多态。this指针指向的对象类型是B所以会调用classB中的func,但是到这里很多同学会误以为结果是B->0
,但是实际上答案并非如此
接口继承
派生类继承的是基类的虚函数的接口,目的是为了重写达成多态,所以基类的接口(虚函数的返回值、函数名、参数)就是最终调用虚函数的接口,但是调用函数的实现是派生类重写实现的。这也从侧面解释了为什么只要基类中虚函数加virtual
,派生类重写时并不用加关键字virtual
这里就是使用了类A中func的接口,所以缺省参数使用的是1.🤗
友元函数、静态成员函数 vs 虚函数
- 友元函数
友元函数不是成员函数,所以不可能成为虚函数 - 静态成员函数
静态成员函数与对象无关,属于整个类,核心是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,没有this指针代表无法拿到虚表,所以也无法实现多态
虚表指针何时初始化
虚表指针是在构造函数初始化列表阶段之前才开始初始化的,所以这里也决定了构造函数不能是虚函数
class A
{
public:
A() :m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << " "; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A
{
public:
B() { test(); }
virtual void func()
{
++m_iVal;
std::cout << m_iVal << " ";
}
};
int main(int argc, char* argv[])
{
A* p = new B;
p->test();
return 0;
}
A* p = new B;
这一句代码中执行流程为
进入B的构造函数 -> 进入A的构造函数 -> 执行A构造函数参数列表(虚表指针被初始化,但此时func并没有被重写) -> 执行test()
函数 -> 执行this->func()
-> 执行A中的func -> 执行B的初始换列表(虚表指针被初始化,func被重写) -> 执行test()
函数 -> 执行this->func()
-> 返回new的B对象地址给指针p -> 执行p->test();
这里虚函数表指针何时被初始换很重要,执行结果是:
虚函数表 vs 虚基表
- 虚函数表
多态概念里存放虚函数地址的表 - 虚基表
菱形继承概念里存放公共元素地址的指针