首先,什么是多态呢?很多概念起初我们都是不理解的,就像我们刚接触继承一样,当学完后发现其实也没那么难,也挺容易理解的。
多态详细点就是多种状态,例如游戏中的抽宝箱,每个人难道都是一样的概率吗,如果你是游戏的老板,游戏玩家有三种,氪金玩家,回归老玩家,和零充平民,你会让这三种人做同样事的时候概率一样吗?不会的,要想让不同的人做同样的事呈现的结果不同,就是我们多态要实现的。
1 子类的虚函数和父类的虚函数构成重写
2 调用的是重写虚函数
3 是用父类的指针或者引用调用的这个重写虚函数
这些条件后面会在原理部分一个个讲解,在讲原理前我们还要补充一些概念。
子类和父类的虚函数函数名相同,参数类型相同,返回值类型相同,三同后当子类继承了父类的虚函数,会对父类的虚函数进行重写。还有值得一提的是重写的仅仅是实现(这里后面会举例提及)。
下面代码中子类的Print函数没有加virtual,那是不是说明Print不是虚函数,然后就不会进入虚函数表,更不会发生多态了呢?实际上还是会发生多态,其实这是一种特殊情况,语法规定当子类有个函数和父类构成三同后,并且父类的函数加了virtual关键字,即便子类的函数像下面Print函数一样不加virtual关键字,编译器也会将这两个函数推入重写流程,使得子类的函数的框架用的是父类的,也就继承了虚函数属性,进入了虚函数表,所以可以实现多态。
class A
{
public:
virtual void Print()
{
cout << "A::Print()" << endl;
}
int _a = 1;
};
class B:public A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
int _b = 2;
};
还有两个也是构成重写的例子
(1)协变(基类和派生类的返回值类型可不同)
但是这个类型也有些限制,基类必须返回基类指针或引用,派生类必须返回派生类指针或引用,而且必须同为指针或引用。
(2)析构函数的重写
我在继承中曾提到,父子类的析构函数会触发隐藏,因为编译器将所有的析构函数名重命名了,那为什么要重命名呢?就是为了使父子类析构函数涉及重写,那为什么要重写,因为要触发多态,那为什么我们要触发多态,看下面代码。
class A
{
public:
~A()
{
cout<<"~A()"<
就是在原来的成员函数前加一个virtual关键字,我们知道先前的虚继承会在子类中增加一个虚基表指针,但那是用来找父类成员的,而现在子类有了虚函数,对子类对象会有什么变化呢?当我们的子类对象还没继承父类对象时,看看我们子类对象内部的变化。
class B
{
public:
virtual void Print()
{
cout << "A::Print()" << endl;
}
int _b = 2;
};
可以看到的除了一个成员变量_b,貌似还多一个地址存在B类对象中,这个地址其实就是虚函数表的地址,也就是说类中如果有虚函数,就会有一张虚函数表,该表存的是能找到函数的方法(可以简单的理解为函数地址,但实际上存的只是一句jump指令的地址,该地址可以帮助我们找到函数)。
在这里,我们就可以再进一步理解子类的虚函数被父类重写究竟意味着什么。
class A
{
public:
virtual void Print()
{
cout << "A::Print()" << endl;
}
int _a = 1;
};
class B:public A
{
public:
virtual void Print()
{
cout << "A::Print()" << endl;
}
int _b = 2;
};
void test1()
{
B b1;
}
B类公有继承A类时,我们发现子类对象b1虚函数表指针也还在,就是增加了一个父类成员变量,那重写究竟发生在哪呢?目前只剩下虚函数表还未查看。
父类对象内存图以及虚函数表
子类对象内存图以及虚函数表
我们知道有虚函数就会有虚表,那虚表应该存的不仅仅是子类的虚函数,也应该有父类的虚函数,可是我们看见子类对象的虚函数表却只有一个地址。明明自己也有一个虚函数,加上继承而来的虚函数,按理说应该是有两个的。其实重写就发生在这里。父类和子类各有一张虚表,当子类继承了父类时,子类会拷贝一份父类的虚表加入到自己的虚表中,但是当子类的虚函数和父类的虚函数构成重写,那体现在虚函数表就是用子类的虚函数地址替换父类的。
要注意的是是否是虚拟继承不影响子类对象是否有虚函数表,虚函数表和虚基表是两套概念,不要混为一谈。
其实在讲虚函数的时候我就已经把多态原理讲得七七八八了,现在结合多态例子再来体会体会。
class A
{
public:
virtual void Print()
{
cout << "A::Print()" << endl;
}
int _a = 1;
};
class B:public A
{
public:
virtual void Print()
{
cout << "A::Print()" << endl;
}
int _b = 2;
};
void Print(A& a2) a2是父类的引用,既可以接收父类对象,也可以接受子类对象(继承知识)
{
a2.Print();
}
void test1()
{
B b1;
A a1;
Print(b1);
Print(a1);
}
这就是多态的体现。实现步骤如下: 现在子类b1,父类对象a1,都传对象给父类的引用a2,让父类的引用去调用函数,以前调用普通函数是在编译的时候确定地址,而现在调用虚函数是运行阶段去虚函数表找调用方法,找虚函数表时:如果a2这个父类的引用指向子类对象,那用的就是子类对象的虚函数指针,用这个指针找的虚函数表,所以调用的是子类的虚函数,而当它指向父类对象时,用的是父类对象的虚函数指针,所以也就找到了父类的虚函数表,所以调用的是父类的虚函数。
所以才会有如下的结果,虽然都是a1去调用的Print函数,但是却能根据指向对象去调用不同的函数,妙啊,这就是多态啊。
这么说来,我们重新定义了虚函数的调用方法——是用虚函数表,这样父类引用调用子类还是父类的虚函数关键看引用指向的对象是谁。那这时候我们再回去理解理解多态条件为什么是那几个?
1 为什么是虚函数: 我认为是要和普通函数区分开,要用虚函数表这种方法才能做到指向父类调用父类的虚函数,指向子类调用子类的虚函数。
2 为什么要调用的虚函数要构成重写,第一 因为我们想让调用父类的虚函数和子类的虚函数用相同的格式,也就是函数名,参数,返回值都一样,如果子类虚函数不要参数,父类虚函数要参数,那我们想一想,给不给函数传参是写代码时就决定的,而这个虚函数要不要参数却要运行才知道,这不就出问题了吗,而且虚函数构成重写后,子类的虚函数地址覆盖父类的虚函数地址,不然两个地址,用谁的。
3 我本来好奇为什么要统一传参给Print,然后在这个函数里调用Print成员函数。
现在我才发现这种方式有多妙,它提供了一种统一的调用方法,而且参数是父类的指针或引用,既可以接收子类对象传参,还可以接收父类对象传参。
void Print(A& a2)
{
a2.Print();
}
但如果参数a2是父类对象:即便是子类对象赋值给父类对象a2,父类对象a2的虚函数指针仍然指向父类的虚函数表,虚函数表指针不参与复制的过程,所以父类对象也就无法找到子类的虚函数表了。
子类对象就更不行了。如果用子类的引用或者指针,那它就只能接收子类对象,父类对象怎么办,只有父类指针和引用才能既可以指向子类对象又可以指向父类对象。
class A
{
public:
virtual void fun1(int a=1)
{
cout << a << "A::fun1()" << endl;
}
int _a = 1;
};
class B:public A
{
public:
virtual void fun1(int a=2)
{
cout <
结果如下:a1是子类对象的引用,按理说是指向子类的,为什么用的缺省参数却是父类的呢?当然,后面打印的B::fun1()可以证实调用的是子类的函数,a1和b1都是调用子类函数,参数为什么不一样呢?这就牵扯到一个冷门知识了,多态中子类的虚函数会被重写,之前是说重写子类虚函数地址会覆盖父类的虚函数表中对应的虚函数地址,但是实际上父类的函数结构也会对子类的函数结构进行覆盖,但不是说把函数改了(因为当我们用子类对象普通调用的时候,用的缺省参数还是子类的),而是说a1通过虚函数表找到的子类虚函数中如果要用缺省参数,编译器会去找父类的。
所以我认为重写的仅仅是实现这句话应该只是方便理解的,本来没有多态的时候,那a1调用fun1肯定是父类的函数,但是当有了多态,上面a1.fun1()就变成调用子类的了,就可以简单地理解为把父类的函数体覆盖了,重写了,但函数结构,例如缺省参数用的是父类的。实际上成员函数就一份,怎么会被改呢,只是我们把调用的函数地址改成了子类的,当要用缺省参数时,编译器从父类那里拿呗。
之前子类虚函数表都只是单继承下的,如果是多继承,那情况则会更复杂。
我们已经知道如果一个类有虚函数,那该类的对象就会有一张虚函数指针,继承给子类的时候虚函数指针也会被当成父类成员被继承,所以多继承中如果A,C父类都有虚函数,那在子类B中就一定会有两张虚表。
多继承随之而来还有个问题,如果B类有一个虚函数fun1,两个父类都有个虚函数fun1,重写的时候会不会把子类的fun1的地址在两张虚表各放一份呢?虽然虚表里显示的地址不一样,但其实都是调用一个子类的函数(这个要看汇编,并且一步步调试,真加入到博客进来阅读量会很大,有兴趣的可以私信)。
class A
{
public:
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
int _a = 1;
};
class C
{
public:
virtual void fun1()
{
cout << "C::fun1()" << endl;
}
int _c = 3;
};
class B:public A,public C
{
public:
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
int _b = 2;
};
再提一下,如果子类还有多的虚函数也会放入第一个虚表,当然如果子类有和C中的虚函数构成重写,就放到第二张表格去了。(vs下浅浅测试过)。
到这就轻松多了,也就一些概念要补充。
其实我们早就接触了多态,函数重载就是静态多态的形式之一,静态多态是指函数在编译时确定地址,而动态多态是在运行时才到虚函数表找的地址。
纯虚函数就是在虚函数括号后面加个=0,如果纯虚函数没有函数体,那就得再加个分号,有就不用了。而包含了纯虚函数的就叫抽象类,抽象类是无法实例化出对象的。
class A
{
public:
virtual void fun1()=0
{
cout << "A::fun1()" << endl;
}
int _a = 1;
};
class B:public A, B类继承了A类的纯虚函数,且没有对其重写,那B类也变成抽象类
{
public:
};
C类再继承也得重写了纯虚函数才可以实例化出对象,这是一种强制重写父类虚函数的方式
class C:public B
{
public:
virtual void fun1()
{
cout << "C::fun1()" << endl;
}
int _c = 3;
};
间接强制重写父类虚函数用的是override关键字。