【C++进阶2--多态】面向对象三大特性之一,多种形态像魔法?

今天,带来C++多态的讲解。

多态和继承并用,能产生“魔法般的效果”。

*文中不足错漏之处望请斧正!


见见多态

是什么

使得父类指针或引用有多种形态。

怎么使它有多种形态呢?咱们先见见猪跑。

见见猪跑

class Base
{
public:
    virtual void print() { cout << "Base" << endl;}
};

class Derive1 : public Base
{
public:
    virtual void print() { cout << "Derive1" << endl;}
};

class Derive2 : public Base
{
public:
    virtual void print() { cout << "Derive2" << endl;}
};

int main()
{
    Base b;
    Derive1 d1;
    Derive2 d2;
    
    Base* ptr;
    ptr = &b;
    ptr->print();
    ptr = &d1;
    ptr->print();
    ptr = &d2;
    ptr->print();
    return 0;
}
Base
Derive1
Derive2

父类指针,存父类对象地址,就能调用父类中的print,存子类对象地址,就能调用子类中的print,拥有了多种形态。


多态的实现

满足多态的前提:子类对父类的虚函数完成重写。

啥是虚函数,啥是重写?

虚函数

虚函数是被virtual修饰的成员函数。可以理解为一种对函数体的泛化。

在声明虚函数的域,默认有一份实例;在此域之外,你可以对虚函数“实例化”,得到新的一份实例。

其作用是实现多态性。

重写/覆盖

重写就是“实例化”虚函数,可以产生新的“虚函数实例”,会把原来的实例覆盖掉。

  • 对虚函数重写的条件:[函数名、返回值、参数]和父类的虚函数相同
  • *重写的仅仅是函数体,重写前后接口是一样的
  • 例外
    • 子类的虚函数可以不写virtual
    • 协变:返回值可以是任意父子类关系的指针/引用
class Base
{
public:
    virtual void print() { cout << "Base" << endl;} 
};

class Derive1 : public Base
{
public:
    virtual void print() { cout << "Derive1" << endl;}
};

class Derive2 : public Base
{
public:
    virtual void print() { cout << "Derive2" << endl;}
};
int main()
{
    Base b;
    Derive1 d1;
    Derive2 d2;
    
    Base* ptr;
    ptr = &b;
    ptr->print();
    ptr = &d1;
    ptr->print();
    ptr = &d2;
    ptr->print();
    return 0;
}
  • print这个虚函数默认有一份实例,它的函数体功能是打印"Base"的
  • print这个虚函数在Derive1中被重写了,这份新的实例把原来默认的覆盖了,它的函数体功能是打印”Derive1”的
  • print这个虚函数在Derive2中被重写了,这份新的实例把原来默认的覆盖了,它的函数体功能是打印”Derive2”的

如果不重写呢?

class Base
{
public:
    virtual void print() { cout << "Base" << endl;}
};

class Derive1 : public Base
{
public:
//    virtual void print() { cout << "Derive1" << endl;}
};

class Derive2 : public Base
{
public:
//    virtual void print() { cout << "Derive2" << endl;}
};

main函数不变,结果如下:

Base
Base
Base

看上面这个调用的手法,有个疑问:它是怎么知道调用哪个虚函数的实例的?

这就要说到虚函数表了。

虚函数表

有虚函数的类,其对象都会存一个虚函数表指针vptr,虚函数表vtable是干嘛的?

虚函数表是一个类的虚函数地址表,存放了这个类对某个虚函数的所有实例(对虚函数进行重写的到的真实函数)的地址。说白了,某个类对一个虚函数的实例,是用虚函数表来描述和组织起来的。

  • 为提高效率,这个可能高频访问的虚表(虚函数表)指针一般放在对象的头4/8个字节
  • 按我们的说法,虚函数表描述的是整个类对某个虚函数的实例,因此它就像类的static成员一样,属于整个类,所以存放在代码段

大概过程:

  1. 创建类对象(对象的头4/8个字节存了一个虚表指针)
  2. 调用对象的某个虚函数实例
  3. 根据虚表来找到当前类对这个虚函数的实例

普通调用和多态调用

普通调用也叫静态绑定,多态调用也叫动态绑定。

静态绑定:编译时通过调用方类型确定调用的函数
动态绑定:运行时通过父类指针指向的对象类型确定调用的函数

  • 用父类指针或引用调用被重写的虚函数是动态绑定
  • 其他都是静态绑定

在这里插入图片描述


多态中的析构函数

若在继承中出现这样的情况:

  1. 动态申请对象
  2. 子类没有重写析构函数(静态绑定)

则delete动态对象的空间时,析构调用不完全——只会根据指针类型静态绑定,只调用父类的析构。

class Base
{
public:
    ~Base()
    {
        cout << "~Base()" << endl;
        delete[] _pb;
    }
private:
    int* _pb = new int[10];
};

class Derive : public Base
{
public:
    ~Derive()
    {
        cout << "~Derive()" << endl;
        delete[] _pd;
    }
private:
    int* _pd = new int[20];
};

int main()
{
    Base* ptr = new Derive;
    delete ptr;
}
~Base()

可以看到,只会调用父类析构。

静态绑定不行,我们来动态绑定,多态上场。

class Base
{
public:
    virtual ~Base()
    {
        cout << "~Base()" << endl;
        delete[] _pb;
    }
private:
    int* _pb = new int[10];
};

class Derive : public Base
{
public:
    ~Derive() //此处可以不用写virtual,这是子类可以不用写virtual的一种使用场景
    {
        cout << "~Derive()" << endl;
        delete[] _pd;
    }
private:
    int* _pd = new int[20];
};

int main()
{
    Base* ptr = new Derive;
    delete ptr;
}
~Derive()
~Base()

满足了多态,调用完父类析构之后就会自动调用子类析构,成功解决。

Destructor

我们之前提到继承中的析构函数名都会被处理成Destructor,为了能够满足重写的条件。

在这里插入图片描述


继承中的对象模型

  1. 单继承(无重写)的虚函数表:
    1. 虚函数按照其声明顺序放于表中
    2. 父类的虚函数在子类的虚函数前面
  2. 单继承(有重写)的虚函数表
    1. 被重写的虚函数被放到了虚表中原来父类虚函数的位置(所以调用的时候不会跑去调用父类的,而是调用自己的)
    2. 没有被覆盖的函数依旧
  3. 多继承(无重写)的虚函数表
    1. 每个父类都有自己的虚表
    2. 子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是按照声明顺序来判断的)

final和override

final

表明类不能被继承。

设计一个不能被继承的类

  1. final修饰类
class A final {};

class B : public A {};

int main()
{
    B b; //A被final修饰,无法被继承

    return 0;
}
  1. 构造私有化——子类想实例化必须调用父类构造。
class A
{
private:
    A() {}
};

class B : public A {};

int main()
{
    B b; //A的构造是私有,实例B需要调用A的析构,调不动,所以A无法被继承

    return 0;
}

override

override可以检查子类虚函数是否和父类的某个虚函数构成重写,或者强制要求某个函数被重写(需要重写的就都带上)。

//override:检查子类的虚函数是否完成重写
class Car
{
public:
    void Drive() {}
};

class Benz : public Car
{
public:
    //err:'Drive' marked 'override' but does not override any member functions
    virtual void Drive() override { cout << "Benz->comfortable" << endl; }
};

int main()
{
    Benz mycar;
    mycar.Drive();

    return 0;
}

抽象类

先导:纯虚函数

是什么:虚函数后写上0,不需要函数体的虚函数。(可以有函数体,但不会被执行)

virtual void func() = 0;

是什么

有纯虚函数的类。

为什么

用于接口继承。

  • 接口继承:主要是为了让子类重写,形成多态
  • 实现继承:主要是为了复用函数体
class Car
{
public:
    virtual void Drive() = 0;
};

class BMW : public Car
{
public:
    virtual void Drive()
    {
        cout << "驾驶乐趣+1" << endl;
    }
};

int main()
{
    BMW mycar;
    mycar.Drive();

    return 0;
}

特性

  • 抽象类不能实例化出对象
  • 若想实例化对象,必须重写纯虚函数(这样就不是抽象类了)
  • 子类继承了抽象类也还是抽象类

今天的分享就到这里了,感谢您能看到这里。

这里是培根的blog,期待与你共同进步!

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值