多态

 

多态的定义

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

简单来说就是完成某个行为,不同的对象会产生不同的结果

 

多态的构成条件

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 

class A
{
public:
    virtual void fun1()
    {
        cout << "A" << endl;
    }
};

class B:public A
{
public:
    virtual void fun1()
    {
        cout << "B" << endl;
    }
};

void fun(A* p)//指针
{
    p->fun1();
}

void fun(A& p)//引用
{
    p.fun1();
}

int main()
{
    A a;
    B b;
    A* p = &a;
    A* p1 = &b;
    fun(a);
    fun(b);


    return 0;
}

 

 

虚函数

被virtual修饰的成员函数为虚函数

 

虚函数的重写

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

 

协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。但实际协变用的比较少

 

析构函数的重写

因为析构函数是完成清理工作,若没有重写时,析构会自动调用父类的析构函数,完成不必要的析构。那么就必须完成子类的析构函数清理子类的资源就好,不要再去调用父类的析构函数。

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。

 

override关键字

功能:检查子类是否重写了父类的的虚函数

class A
{
public:
    virtual void fun1()
    {
        cout << "A" << endl;
    }
};

class B:public A
{
public:
    virtual void fun1() override 
    {
        cout << "B" << endl;
    }
};

final关键字

功能:修饰虚函数,表示此虚函数不允许被重写

class A
{
public:
    virtual void fun1() final
    {
        cout << "A" << endl;
    }
};

class B:public A
{
public:
    //virtual void fun1() override 
    //{
    //    cout << "B" << endl;
    //}
};

 

 

重载、重写、重定义对比

 

 

有虚函数的类都有虚函数表 计算类的大小要对其 虚表大小为4/8 

计算带有虚函数的 都要加上4/8 虚表

 

class A
{
public:
    virtual void func(int val = 1)
    {
        cout << "A->" << val << endl;
    }
    virtual void test()
    {
        func();
    }
};

class B :public A
{
public:
    virtual void func(int val = 0)
    {
        cout << "B->" << val << endl;
    }
    //virtual void test()  这是继承下来了  这里为了方便看 
    //{
    //    func();
    //}
};

int main()
{
    B* p = new B;// B继承下来的test   去调用B类的func    
    //了解多态的特性  因为多态是继承父类的接口  而重写的是实现  所以这里的缺省用的是父类  实现是继续用子类
    p->test();
   
    return 0;
}

//运行为 B->1

多态是接口继承,调用子类时,接口用的依旧时父类 缺省也是如此 所以缺省都是用的父类  而子类时重写实现 所以调用时 实现调用的是子类。

总结:多态为接口继承,调用时,接口用父类,实现用子类重写后的。

 

接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

 

 

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

只要包含纯虚函数的类,就叫抽象类,抽象类表示,子类必须重写父类的虚函数,不然无法实例化对象

class A
{
public:
    virtual void fun1()=0
};

class B :public A
{
public:
    virtual void fun1()
    {
        cout << "重写" << endl;
    }
};

 

虚函数表

1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。

2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6. 虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中另外对象中存的不是虚表,存的是虚表指针。

 

 

静态绑定与动态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

 

 

相关问题

  1. nline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。

  4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。参考本节课件内容

  5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

  6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

  7. 虚函数表是存虚函数的指针,而虚基表是用来解决二义性和数据沉余问题。

 

 

这就是本篇的全部内容,若对您有所帮助,希望能获得您的赞,您的赞就是对我的最大支持