【C++】多态

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6


目录

一、多态的概念

二、静态的多态

三、动态的多态

1、虚函数

2、虚函数的重写覆盖

3、利用虚函数的重写实现多态

4、虚函数重写的例外

4.1协变返回值分别为构成父子关系的指针或引用也构成重写

4.2析构函数是虚函数的场景动态申请的子类对象交给父类指针管理需要virtual修饰析构函数

四、C++11中的final和override

1、C++98防止一个类被继承的方法

2、C++11的final关键字

2.1final修饰类防止该类被继承

2.2final修饰虚函数防止该虚函数被重写

3、C++11的override关键字

override修饰子类重写的虚函数检查是否完成重写

五、重载、重写、隐藏的区别

六、抽象类接口类

1、抽象类的概念

2、实现继承和接口继承

七、多态的实现原理

1、虚函数表

2、多态的原理

2.1形成多态的原因

2.2为什么一定要传入从子类对象切片而来的父类对象的指针/引用才能引发多态为什么不能直接传入切片的父类对象

2.3多态与非多态的成员函数调用区别

八、单继承和多继承中子类虚函数表


一、多态的概念

多态指多种形态。不同的对象完成同一件事情但是结果不同。例如公交刷卡行为成人刷卡全价学生刷卡半价。亦或是不同的客户来消费金卡会员8折银卡会员9折普通会员无折扣。

二、静态的多态

静态的多态是在编译时产生不同。例如函数重载就是一种静态的多态行为。看上去是在调用同一个函数但是会产生不同的行为。

int main()
{
    int a=1;
    double b=2.3;
    std::cout<<a<<std::endl;
    std::cout<<b<<std::endl;
    return 0;
}

三、动态的多态

动态的多态是在运行时产生不同。

构成多态的条件缺一不可否则就不构成多态。

1、必须通过父类对象的引用或指针当做形参调用虚函数。仅限引用和指针是原因见下文

2、子类必须完成对父类虚函数的重写且被调用的函数是虚函数。

1、虚函数

class Person 
{
public:
    virtual void BuyTicket() 
    { 
        cout << "买票-全价" << endl;
    }
};
class Student : public Person
{
public:
virtual void BuyTicket()
    {
        cout<<"买票-半价"<<endl;
    }
};

被virtual关键字修饰的类的非静态成员函数称为虚函数。

注意虚函数和虚继承虽然都使用了virtual关键字但是它们没有关系。

1、子类重写父类虚函数时子类“三同”函数不写virtual也构成重写但是不规范。C++设计者的初衷是父类写了virtual即使子类不写也构成多态那就不会出现内存泄漏的情况了。设计项目时可能父类和子类并不是同一个人写的那么子类程序员没写virtual的概率极大

2、可以这样理解虽然子类对应函数没写virtual但他先继承了父类中虚函数的“虚”属性再完成重写。注意类作用限定符也会被子类继承如果父类虚函数是public即使子类重写函数是private也会变成public

2、虚函数的重写覆盖

如果父类中存在虚函数并且子类拥有“三同”成员函数返回值类型函数名称参数列表均相同。那么子类的虚函数就是对父类虚函数的重写。例如上面例子中Student::BuyTicket()是Person::BuyTicket() 它们构成重写而不是构成隐藏。

重写还有一个叫法是覆盖。子类重写父类的虚函数意为子类重写的函数会覆盖父类的这个虚函数。下文会细讲

3、利用虚函数的重写实现多态

class Person 
{
public:
    virtual void BuyTicket() 
    { 
        cout << "买票-全价" << endl;
    }
};
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"买票-半价"<<endl;
    }
};
void Func(Person& p)//子类传入会被切片所以可以不用const/构成多态跟p类型无关传子调子传父调父
{
    p.BuyTicket();
}
int main()
{
    Person p;
    Student s;
    //传入父类对象调用父类的BuyTicket();传入子类对象将调用子类的BuyTicket();
    Func(p);
    Func(s);//这里的s会被切片传参
}

先看构不构成多态构成多态那么和对象类型无关传入哪个对象的引用/指针就使用谁的虚函数。

如果不构成多态均调用p类型的函数。

4、虚函数重写的例外

4.1协变返回值分别为构成父子关系的指针或引用也构成重写

构成重写需要成员函数返回值相同但是存在例外当返回值是构成父子关系的指针或引用时它们也构成重写。这种重写叫做协变。不过父类返回值一定要用父指针/父引用子必须用子指针/子引用不能颠倒。

4.2析构函数是虚函数的场景动态申请的子类对象交给父类指针管理需要virtual修饰析构函数

class Person 
{
public:
    virtual ~Person()
    {}
};
class Student : public Person
{
public:
    virtual ~Student()
    {}
};

析构函数也可重写虽然父子析构函数的函数名表面上不一样但其实所有析构函数的函数名都会被处理成destructor。

下面看个场景

各new一个父类/子类的对象交给父类指针管理这没问题。但是在析构的时候因为是父类指针所以p1p2都会去调用父类的析构函数但别忘了起初我们可是new了一个子类对象如果子类对象中存在资源那么就会导致内存泄漏

为了避免这种情况就需要使用virtual关键字修饰析构函数

父子构造函数构成多态那就不看p1p2的类型了p1p2指向哪个对象就调用哪个对象的析构函数。

四、C++11中的final和override

1、C++98防止一个类被继承的方法

class Person
{
public:
    static Person CreateObj()
    {
        //new Person;
        return Person();
    }
private:
    Person()
    {}
};
class Student : public Person
{
public: 
};
int main()
{
	Person p=Person::CreateObj();
    return 0;
}

C++98通过把构造函数变为私有的方式让子类继承后根本构造不出父类对象。

但是父类却可以通过静态的“偷家”函数构造对象。这里必须静态静态成员函数调用无需借助对象。如果是非静态成员函数则需要对象才能调用但是生成对象必须通过这个函数······无限循环了

2、C++11的final关键字

2.1final修饰类防止该类被继承

class Person final
{
public:
};

Person被final修饰后将不能被继承

2.2final修饰虚函数防止该虚函数被重写

3、C++11的override关键字

override修饰子类重写的虚函数检查是否完成重写

为了防止程序员在子类进行重写时函数名拼写出现错误这就造成了重写的函数和父类被重写的函数对不上。这是个很严重的问题因为这种情况并不违反语法规则编译期间编译器是不会报错的只有在程序运行时发现结果不对回去debug时才能发现问题。

C++很贴心的增加了override关键字成员函数被修饰后编译器会帮忙检查是否重写成功。

五、重载、重写、隐藏的区别

六、抽象类接口类

1、抽象类的概念

在虚函数的后面加上=0则这个函数被称为纯虚函数包含纯虚函数的类被称为抽象类接口类。一个类型在现实世界中没有对应的实物就可以定义为抽象类。例如职能类、Person类等。

抽象类不能实例化出对象

class Person 
{
public:
    virtual void Func()=0
    {
        //纯虚函数一般只声明不实现。因为没有对象
    }
};

子类继承了父类的纯虚函数子类也变成了抽象类同样不能实例化出对象。

除非子类重写纯虚函数子类才能实例化出对象。

抽象类的作用是强制子类进行重写

2、实现继承和接口继承

子类继承父类的普通函数是为了使用该函数的具体实现这是实现继承。

而虚函数是为了让子类进行重写实现多态子类只继承了函数名并不继承具体实现这是接口继承接口继承会继承父类的类作用限定符

所以不准备实现多态的话就不要用virtual去修饰成员函数了。

七、多态的实现原理

1、虚函数表

注意虚函数表指针并不一定放在所有成员变量的最前面有的编译器会放在最后面。虚函数表指针指向的虚函数表本质是存放虚函数指针的函数指针数组vs下会在这个数组最后放一个nullptr而Linux不会。

虚函数表存放于常量区代码段

vs中虚函数表中存放的并不是虚函数的地址而是一句jump指令的地址通过该jump指令找到对应的虚函数。

再看一段代码

p能调用Func1是因为Func1并不存放在类中而是在代码区所以p->Func1并不是解引用而是将p当做形参传递给this。

p调不了Func2是因为虚函数需要通过类对象中的虚函数表指针找到对应的虚函数进行调用所以它是一个解引用行为p是nullptr对空指针的解引用行为引发程序崩溃。

2、多态的原理

2.1形成多态的原因

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "买票-全价" << endl;
    }
private:
    int _a;
};
class Student : public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "买票-半价" << endl;
    }
private:
    int _b;
};
void Func(Person& p)//子类传入会被切片所以可以不用const/构成多态跟p类型无关传子调子传父调父
{
    p.BuyTicket();
}
int main()
{
    Person p;
    Student s;
    //传入父类对象调用父类的BuyTicket();传入子类对象将调用子类的BuyTicket();
    Func(p);
    Func(s);//这里的s会被切片传参
}

从监视窗口可以看到子类中的虚函数表指针存放于继承于父类的那部分但是父子对象中虚函数表指针及指向的虚函数并不是同一个。切片后得到的父类对象的虚函数表指向重写的虚函数这就解释了传入一个父类对象的指针/引用就调用父类对象的虚函数传入一个从子类切片而来的父类对象的指针/引用就会去调用重写的虚函数。

同类型的对象虚表指针是相同的均指向同一张虚函数表

2.2为什么一定要传入从子类对象切片而来的父类对象的指针/引用才能引发多态为什么不能直接传入切片的父类对象

因为指针/引用切片出来的父类对象能获得重写的虚函数值切片出来的父类对象中的虚函数必须源自于父类。想一想如果值切片的父类对象中的虚函数拷贝自子类重写的虚函数那不是乱套了。例如虚函数是析构函数值切片后的父类中的析构函数如果是子类的析构函数那么父类对象在析构的时候要出大问题

2.3多态与非多态的成员函数调用区别

函数调用满足多态需要在程序运行时去对象中的虚函数表中找虚函数进行调用不满足多态的成员函数编译器在编译时确定调用的地址。

八、单继承和多继承中子类虚函数表

1、单继承中子类中非重写虚函数将放在同一张虚函数表中。

当然对子类进行切片时切片得到的父类对象是不会得到func3和func4的。

2、多继承中子类中非重写的虚函数将被存放于第一个继承的父类部分的虚函数表中。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++