前言
大家好,我是jiantaoyab,本篇文章给大家介绍多态。
先来认清几个概念隐藏、覆盖、重载。
重载
重载必须是发生在同一个类域中, 满足函数名相同,参数个数、顺序、类型不同。如果一个在父类域一个在子类域,是不会存在重载的。
隐藏
在继承中的学习可以知道,每一个类都有独立的成员变量和成员函数。当子类继承父类的时候,会将父类的全部成员复制一份作为子类的成员。
当子类和父类具有相同名字的成员时,子类对象去访问成员会先访问子类的成员,如果想要访问父类的同名成员要指明类域,这叫现象就叫做隐藏。
隐藏是对子类对象而言的。
覆盖/重写
当子类中有一个跟基类完全相同的虚函数,返回值类型、函数名字、参数列表完全相同,子类对象调用的时候,会直接调用子类的成员,称子类的虚函数重写了父类的虚函数,重写只是重写函数体内的内容。
多态的概念
多态是父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。想要构成多态必须满足2个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写
先来认识什么是虚函数
被virtual修饰的函数就称为虚函数,这里的virtual和继承解决菱形继承用的virtual含义是不一样,只是用了同一个关键字。一会在多态的原理会更详细的介绍。
class A
{
public:
virtual void fun() {}
}
class B : public A
{
public:
void fun(){}
}
//这里的派生类成员函数fun()并没有virtual关键字,但是也构成重写,因为父类A中的fun()
//带上了virtual关键字而子类在继承之后也会保持着这个属性,但是不建议这样使用。
虚函数重写有2个例外:
-
协变:基类与派生类虚函数返回值类型不同。
-
虚构函数的重写:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字看起来不同,但是编译器会统一将虚构函数名字改为destructor就构成了重写。
class A { public: virtual ~A() {} } class B : public A { public: virtual ~B() {} }
一定要对析构函数重写,这样当父类指针或者引用指向子类的对象时候,析构的时候就能调用正确的析构函数去析构。
抽象类
如果在虚函数的后面加上=0,这个函数就称为纯虚函数,只要包含了纯虚函数的类就叫做抽象类/接口类,抽象类不能实例化出对象,派生类继承了之后也不能实例化对象,除非重写纯虚函数,派生类也必须对纯虚函数进行重写不然编译不通过。
class A
{
public:
virtual void fun() = 0 {}//纯虚函数
}
在基类中的纯虚函数一般我们只声明并不实现,如果实现了也没有意义。因为,抽象类不能实例化出对象,一般没有对象也调用不了这个函数。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
虚函数表
虚函数是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,它就像一个地图一样,指明了实际所应该调用的函数。
C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
举个例子
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class A :public Base
{
public :
virtual void f() { cout << "A::f" << endl; }
virtual void g() { cout << "A::g" << endl; }
virtual void h() { cout << "A::h" << endl; }
};
多重继承下的虚函数表
如果想要打印出虚函数表和虚函数的地址要怎么做呢?
在32位下,先取对象b的地址强转为一个int*的指针,再解引用取出头4个字节的值,这个值就是指向虚表的指针,再将这个指针强转为VFPTR *类型,通过对这个VFPTR *类型就能访问虚函数指针数组里面的值。
typedef void(*VFPTR) (); // VFPTR虚表指针的类型
void PrintVTable(VFPTR vTable[])
{
cout << v_table << endl; //虚表地址
for(int i = 0; vTable[] != nullptr; i++) //vs下以null结尾
{
printf("%d:%p", i, vTable[i]); //虚函数地址
}
}
int main()
{
Base b;
A a;
VFPTR* v_table = (VFPTR*)(*(int *) & b );
PrintVable(v_table);
}
再回头看看要满足多态的2个条件,假如我们用的是父类的对象而不是引用/指针,子类在切片的时候是不会将虚表指针切过去的,不然等到父类对象自己调用的时候就不知道调用的父类的还是子类函数。
必须对虚函数进行重写是因为在对虚函数重写之后,子类的对象会多出一个虚函数指针(虚表指针是在构造函数的时候自动初始化的),
满足多态的话,函数调用不是在编译的时候确定的,是运行起来在对象中的虚函数表中去取的,虚函数里面存了什么决定调用谁。
虚表虚函数是放到哪里的呢?
虚函数和普通函数一样是放到代码段的,只是它的指针放到虚表中,对象存的是虚表的指针,同样的虚表也是存在代码段的。
c++11 final/override
final
如果在之前c++98想要设计一个不能被继承的类我们可以通过把父类构造函数弄成私有的,如果父类想自己创建对象可以写一个公有的成员函数来进行创建。在c++11后,可以使用final关键字,在类后面加上final 表示不能被继承,如果加在一个函数后可以限制不能给重写。
class A final {}; //不能被继承
class B
{
public:
virtual void f() final {} // f() 不能被重写
}
override
在语法上检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class A
{
public:
virtual void f(){};
}
class B : public A
{
public:
virtual void f() override{}; //如果没有对 f() 重写会报错
}
多态常见问题
inline函数可以是虚函数吗?
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。理由如下:内联是在发生在编译期间,编译器会自主选择内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
静态成员可以是虚函数吗?
不能,这是因为静态成员函数属于类本身,而不是属于特定的对象实例。静态成员函数在内存中只有一份,不依赖于特定对象的调用,因此它们没有this指针。而虚函数是通过动态绑定实现的,它依赖于运行时对象的实际类型来确定调用哪个函数。由于静态成员函数不通过this指针访问,无法实现动态绑定,因此它们不能是虚函数。
构造函数可以是虚函数吗?析构函数呢?
构造函数不可以是虚函数,这是因为虚函数依赖于虚函数表而虚函数表指针是在构造函数中初始化的。在对象实例化之前,即虚函数表指针被正确初始化之前,是无法通过虚函数表指针调用虚函数的。此外,构造函数的主要作用是初始化对象,而虚函数主要用于在运行时通过父类的指针或引用调用子类的成员函数,实现动态绑定和多态。由于构造函数是在创建对象时自动调用的,不可能通过父类的指针或引用去调用,因此规定构造函数不能是虚函数。
析构函数的话最好定义成虚函数,具体上面有介绍。
对象访问普通函数快还是虚函数更快?
访问普通成员函数更快,因为普通成员函数的地址在编译阶段就已确定,因此在访问时直接调用对应地址的函数,而虚函数在调用时,需要首先在虚函数表中寻找虚函数所在地址,因此相比普通成员函数速度要慢一些。
虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况下存在代码段
假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( D)
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
关于虚表说法正确的是(D )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
下面说法正确的是( C)
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;
}
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
下面程序输出结果是什么? (A)
#include<iostream>
using namespace std;
class A{
public:
A(char *s) { cout<<s<<endl; }
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:
C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
{ cout<<s4<<endl;}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class AD:class A class C class B class D
在对象的创建和销毁过程中,构造函数和析构函数的调用顺序如下:
- 首先,创建类D的对象时,会先调用类A的构造函数,然后调用类B和类C的构造函数。因为类B和类C都是虚继承自类A的,所以只会调用一次类A的构造函数。所以输出结果为"class A"。
- 接着,调用类B的构造函数,输出结果为"class B"。
- 然后,调用类C的构造函数,输出结果为"class C"。
- 最后,调用类D的构造函数,输出结果为"class D"。
- 当对象被销毁时,析构函数的调用顺序与构造函数相反。首先调用类D的析构函数,然后调用类C和类B的析构函数,最后调用类A的析构函数。所以输出结果为"class D"、“class C”、“class B”、“class A”。
以下程序输出结果是什么(B)
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
因为B中没有对test进行重写,那么调用父类的test(),test中调用了func,这里的this指针是B类型的,B并且对func进行了重写,重写是一种接口继承,重写只要满足参数,函数名,返回值相同就可以,而缺省参数会用父类的,所以最终打印出来就是B->1。