0


[ C++ ] 抽象类 虚函数 虚函数表 -- C++多态(1)

问题引入:

本篇文章主要介绍C++中的多态特性。我们在继承的时候知道了派生类对象使用基类的方法,而未做任何的修改,是直接使用父类的方法。然后,可能会有这种情况,即希望同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用该方法的对象。这种较为复杂的行为称作--多态。所谓多态就是具有多种形态,即同一个方法的行为随上下文而异。现在我们一起来进入多态!

1.多态的概念

1.1 概念

在上面我们提到,所谓多态通俗来说就是多种形态。具体点就是去完成某个行为,当不同的对象去完成时就会产生出不同的状态。

举个例子:

车站售票处推出根据购票身份不同从而出售与身份相对应的车票。

推出三种方案:

  1. 普通成人:排队购票,每人100¥

  2. 学生 :排队购票,每人 50 ¥

  3. 军人 :购买预留票,无需排队,每人100¥

我们发现这个案例正是使用了多态,不同的身份对应购买与之身份对应的车票。我们使用代码加以实现。

2.多态的定义及实现

2.1 多态的构成条件

有两个重要的机制可用于实现多态:

  • 在派生类中重新定义基类的方法,也叫做方法的重写(要求返回值 函数名 参数列表相同)
  • 使用虚方法(虚函数)

根据这个原则我们实现一下刚刚的代码:

class Person
{
public:
    Person(const char* name = "张三")
        :_name(name)
    {}
    
    virtual void BuyTicket()
    {
        cout << _name << "购票,需要排队,每人 100 ¥" << endl;
    }
protected:
    string _name;
};

class Student : public Person
{
public:
    Student(const char* name)
        :_name(name)
    {}

    virtual void BuyTicket()
    {
        cout << _name << "购票,需要排队,每人 50 ¥" << endl;
    }
private:
    string _name;
};

class Soldier : public Person
{
public:
    Soldier(const char* name)
        :_name(name)
    {}

    virtual void BuyTicket()
    {
        cout << _name << "购买预留票,不需要排队,每人 100 ¥" << endl;
    }
private:
    string _name;
};

void Buy(Person* p)
{
    p->BuyTicket();
}

int main()
{
    Person p("张三");
    Buy(&p);

    Student st("张同学");
    Buy(&st);

    Soldier so("报国");
    Buy(&so);

    return 0;
}

2.2 虚函数

虚函数:即被virtual修饰的类成员函数

    virtual void BuyTicket()
    {
        cout << _name << "购票,需要排队,每人 100 ¥" << endl;
    }

2.3 虚函数的重写

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

class Person
{
public:
    virtual void BuyTicket(){}
};

class Student : public Person
{
public:
    //派生类也可以不写virtual 但是不推荐
    virtual void BuyTicket(){}
};

class Soldier : public Person
{
public:
    //派生类也可以不写virtual 但是不推荐
    virtual void BuyTicket(){}
};

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是这种写法不是很规范。建议如果是虚函数就加上virtual更加醒目。

2.4 虚函数重写的两个例外

2.4.1 协变(基类与派生类虚函数返回值类型不同)

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

class A 
{};

class B : public A 
{};

class Person {
public:
    virtual A* f() 
    { 
        cout << "A* f() " << endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    virtual B* f() 
    { 
        cout << "B* f() " << endl;
        return nullptr;
    }
    
};

int main()
{
    Person p;
    Student s;
    
    Person* ptr = &p;
    ptr->f();

    ptr = &s;
    ptr->f();

    return 0;
}

依旧是构成多态的

2.4.2.析构函数的重写 (基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构只要定义,无论是否加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;
    Person* st = new Student;

    delete p;
    delete st;

    return 0;
}

注:只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

2.4.3 选择题测试

下面的程序输出结果是什么?

A: A->0 B: B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确

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()
{
    B* p = new B;
    p->test();
    return 0;
}

我们一起来分析一下这道题:

1.进入main函数,我们B创造一个B的对象,这是普通对象的创建,由于B公有继承了A,因此A中的公有方法B对象也可以调用,因此B的指针对象p可以调用test()方法,这里使用的是继承的属性。

2.进入test方法是我们的this指针是A的this指针,因此函数的实现在A类内,this指针调用func()方法时,由于父类中func是虚函数,即使子类B不加virtual也是虚函数,同时func函数的返回值,函数名,参数列表都相同(这里缺省值不影响,只要参数列表的数据类型和参数顺序相同即可)。

3.因此A和B的func构成函数重写,此时会调用func()的重写方法。只不过这里需要注意的是,缺省值使用的是父类的缺失值。因此打印 B->1,而不是B->0.因此本题选择B。你选对了吗?

2.5 C++11 final 和 override

2.5.1 final

final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
    virtual void Drive() final 
    {}
};
class Benz :public Car
{
public:
    virtual void Drive() 
    { cout << "Benz-舒适" << endl; }
};

2.5.2 override

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写就要报错

class Car
{
public:
    virtual void Drive(){}
};

class Benz : public Car
{
public:
    virtual void Drive() override
    {
        cout << "Benz-舒适" << endl;
    }
};

加入我们一旦去掉基类的virtual,override就会提示不能进行重写。

2.6 重载 重写(覆盖) 重定义(隐藏)的对比

3. 抽象类

3.1 什么是抽象类

在虚函数后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数得类叫做抽象类。

抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

//抽象类 -- 不能实例化出对象
//在现实生活一般没有具体对应的实体
//间接功能:子类必须进行重写才能实例化出对象
class Car
{
public:
    //纯虚函数  --  抽象  
    //没有对象可以调用 子类必须重写才能调用
    virtual void Drive() = 0;

    //实现没有价值,因为没有对象会调用他
    virtual void Drive() = 0
    {
        cout << " Drive()" << endl;
    }
};

我们发现抽象类是不能实例化出对象的

class Car
{
public:
    void f()
    {
        cout << "f()" << endl;
    }
    virtual void Drive() = 0;

};
class BMW : public Car
{
public:
    //进行了重写
    virtual void Drive()
    {
        cout << "BMW-操控" << endl;
    }
};

int main()
{
    BMW b;
    b.Drive();
    b.f();
    return 0;
}

3.2 接口继承和实现继承

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

4. 虚函数表

首先这里有一道常考的笔试题:

//32位下 sizeof(Base)是多少?
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
    int _b = 1;
};

int main()
{
    cout << sizeof(Base) << endl;
    Base b;

    return 0;
}

通过测试我们发现b对象是8bytes,而不是4bytes,通过调试我们发现,除了_b成员,还多了一个_vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

如果我们多添加几个虚函数,那又会是怎样呢?

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2()" << endl;
    }
    virtual void Func3()
    {
        cout << "Func3()" << endl;
    }
private:
    int _b = 1;
};

那我们在多天几个普通函数呢?

因此我们通过观察和测试,发现了下面几个问题:

1.虚函数存在虚表,虚表存在对象中。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

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

针对上面的代码我们如果增加一个派生类去继承Base(),并且重写Func1()

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2()" << endl;
    }
    virtual void Func3()
    {
        cout << "Func3()" << endl;
    }
    void Func4()
    {
        cout << "Func3()" << endl;
    }
private:
    int _b = 1;
};

class Derive :public Base
{
public:
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

此时通过调试我们又发现了新的问题:

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

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

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

总结一下派生类的虚表生成:

    a.先将基类中的虚表内容拷贝一份到派生类虚表中

    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 

    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

(本篇完)

标签: c++ 开发语言

本文转载自: https://blog.csdn.net/qq_58325487/article/details/126774580
版权归原作者 小白又菜 所有, 如有侵权,请联系我们删除。

“[ C++ ] 抽象类 虚函数 虚函数表 -- C++多态(1)”的评论:

还没有评论