C++多态及其相关应用

目录

1.函数重写(隐藏)

2.函数隐藏

3.多态的基本概念

4.多态的实现

5.纯虚函数和抽象类

6.虚析构和纯虚析构


1.函数重写(隐藏)

定义:子类重新定义父类中有相同名称、返回值类型和参数的虚函数,主要在继承关系中出现。

基本条件:

        1.重写的函数和被重写的函数必须为虚函数(virtual),并且分别位于基类和派生类中。

        2.重写的函数和被重写的函数,返回值、函数名和函数参数必须完全一致。

#include <iostream>
using namespace std;
class Animal {
public:
    virtual void speak()
    { 
        cout << "动物会说话" << endl; 
    }
};
class Cat : public Animal {
public:
    virtual void speak() { // 函数重写,这个virtual可写可不写
        cout << "小猫会喵喵叫" << endl;
    }
};
int main() {
    Cat cat;
    cat.speak();
    return 0;
}

2.函数隐藏

当父类和子类中有同名函数,子类中的函数会将父类中的同名函数隐藏

注意:在子类中和父类函数名字相同不是重写就是函数隐藏,注意这里指的是函数的名字,不包括函数的参数和返回值。

#include <iostream>
using namespace std;
class Animal {
public:
    virtual void speak() 
    { 
        cout << "动物会说话" << endl;
    }
    void work() 
    { 
        cout << "动物会工作" << endl;
    }
};
class Cat : public Animal {
public:
    virtual void speak()// 函数重写,这个virtual可写可不写
    { 
        cout << "小猫会喵喵叫" << endl;
    }
    void work() // 函数隐藏,因为不是虚函数
    { 
        cout << "小猫会工作" << endl;
    }
    void work(int a) // 函数隐藏(参数不同)
    { 
        cout << "小猫会工作" << endl;
    }
    //int work() //函数隐藏(返回值类型不同)
    //{
    //    cout << "小猫会算数" << endl;
    //    return 6;
    //}
};
int main() {
    Cat cat;
    cat.work();
    return 0;
}

3.多态的基本概念

定义:通过基类指针或引用调用派生类的函数实现不同的行为

分类:

        静态多态:函数重载和运算符重载属于静态多态,复用函数名

        动态多态:派生类和虚函数实现运行时多态

区别:

        1.静态多态的函数地址绑定是早绑定——编译节点确定函数地址

        2.动态多态的函数地址是晚绑定——运行时阶段确定函数地址

原因:静态多态基于编译时就能确定的信息(如函数重载的参数特征、模板的参数类型等)来绑定函数地址,而动态多态由于要依据对象的实际类型,而这个类型在编译时往往不确定,需要在运行时通过虚函数表等机制去动态查找并绑定函数地址,所以分别呈现出早绑定和晚绑定的特性。

多态满足的条件

        1.有继承关系

        2.子类重写父类中的虚函数

多态使用的条件

        父类指针或引用指向子类对象

#include <iostream>
using namespace std;
class animal {
public:
    virtual void speak() { cout << "动物会说话" << endl; }
    void work() { cout << "动物会上班" << endl; }
};

class cat : public animal {
public:
    virtual void speak() { cout << "小猫会喵喵叫" << endl; }
    void work() { cout << "猫会上班" << endl; }
};
class dog : public animal {
public:
    virtual void speak() { cout << "小狗会旺旺叫" << endl; }
    void work() { cout << "小狗会上班" << endl; }
};
void Speak(animal& animal) { // 通过引用指向子类对象
    animal.speak();
}
int main() {
    // 动态多态:父类的指针或者引用指向子类对象,并且通过该指针或者引用调用子类重写的虚函数
    animal* a = new dog;
    a->speak();
    a->work(); // 调用的函数是隐藏函数时,调用函数的指针或对象是什么类型调用哪里的函数
    return 0;
}

        例如在上述代码中,两个子类Cat和Dog都继承父类animal,然后在子类中都对speak虚函数进行了重写,而work函数则是实现的是函数隐藏。在主函数中定义了一个父类函数的指针a,然后它指向的是子类对象dog,然后调用的是子类重写的虚函数speak,所以就会执行子类的speak函数。而work函数由于是函数隐藏,所以调用函数的指针是什么类型就调用哪儿的函数,运行结果如下。

4.多态的实现

为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心就是虚函数表。

类的虚函数表:

        1.每个包含了虚函数的类都包含一个虚表(存放虚函数指针的数组)

        2.当一个类(B)继承了另外一个类(A)时,类B就会继承A的函数的调用权

所以如果一个基类包含了虚函数,那么其继承类也可以调用这个虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();

private:
    int m_data1, m_data2;
};
class B : public A { // 此时类B也拥有自己的虚表
};

类A的虚函数表如下:

1.虚表是一个存放指针的数组,其内元素是虚函数的指针,每个元素对应一个虚函数的函数指针

2.虚表的头目,即虚函数指针的赋值是发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构建出来了。

虚表指针:

        1.虚表指针属于类,而不是某个具体的对象,一个类只需要有一个虚表即可。同一个类的所有对象都是使用的同一个虚表。

        2.为了指定对象的虚表,对象内部包含了一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*_vpr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

        3.验证_vptr的方法(_vptr指针不可被访问),就是求先求一个普通类的字节数大小sizeof(),然后将类中的,某个函数前加上virtual让其变成一个虚函数,再求该类所占字节数的大小,会发现多了4个字节(或者8个字节,取决当前的编译器是64位还是32位),这就验证了_vptr的存在。

#include <iostream>
using namespace std;
class A {
public:
    void vfunc1() {

    }
    void vfunc2() {

    }
    void func1() {

    }
    void func2()
    {}

private:
    int m_data1, m_data2;
};
int main() {
    cout << "类A的大小为:" << sizeof(A)<<endl;
}

因为此时类A中没有虚函数 ,只有两个整型的成员变量,所以输出的大小为8

然后将vfun1函数前面加上virtual关键字,让其变成一个虚函数,此时输出的结果变成了16,因为此时编译器为64位,所以指针的大小为8(32位指针对应的大小为4,64位对应的大小为8)

再将vfun2也变成虚函数,输出的大小仍然是16位,说明所以的虚函数都在一个虚函数表中,并不是一个虚函数对应一个虚函数。

5.纯虚函数和抽象类

定义:在基类中声明的虚函数,它在基类中没有函数体的定义,只是通过在函数声明的结尾添加 = 0 来表明其纯虚函数的身份。

语法:virtual 返回值类型 函数名 (参数列表)=0;

当一个类中有了纯虚函数,这个类也被称为抽象类。

抽象类的特点:

        1.无法实例化对象

        2.子类必须重写抽象类中的纯虚函数,否则也是抽象类。

#include <iostream>
using namespace std;
/*
        纯虚函数在虚表中存放的是0
    */
class Animal {
    virtual void speak() = 0; // 纯虚函数
    void work() { cout << "animal work" << endl; }
};
class Cat : public Animal {
    void speak() { cout << "cat speak" << endl; }
    void work() { cout << "cat work" << endl; }
};
int main() {
    // Animal a;//不能实例化抽象类的对象
    Cat c;
    return 0;
}

6.虚析构和纯虚析构

虚析构函数:把一个析构函数声明成虚函数,即在析构函数前面加上virtual关键字

纯虚析构:与纯虚函数相似,只不过要有定义(一般在类外定义)

多态在使用使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数。

解决方案:将父类中的虚构函数改为虚析构或纯虚析构,因为改成了虚函数,此时会根据对象的实际类型,去调用该对象的析构函数。

虚析构和纯析构的共性:

        1.可以解决父类指针释放子类的对象

        2.都需要有具体的函数实现

虚析构和纯虚析构的区别:

        如果是纯虚析构,该类属于抽象类,无法实例化

语法:

        虚析构:virtual ~类名{}

        纯虚析构:virtual ~类名=0;

#include <iostream>
using namespace std;
class animal {
public:
    animal() { cout << "animal构造" << endl; }
    virtual ~animal() { cout << "animal析构" << endl; }
};
class cat : public animal {
    int* p;
public:
    cat():p(new int(10)) { cout << "cat构造" << endl; }
    ~cat() {
        delete p;
        cout << "cat析构" << endl;
    }
};
int main() {
    animal* a = new cat;
    delete a;
    return 0;
}

总结:

1.虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

2.如果子类没有堆区数据,可以不写虚析构或纯虚析构

3.拥有纯虚析构函数的类也是抽象类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值