必看!用示例代码学 C++ 多态,快速掌握基础知识,高效提升编程能力

59 阅读9分钟

image.png

C++多态


目录

C++多态

●1.多态的概念

●2.多态的定义及实现

●3.抽象类

●4.多态的原理

●5.继承和多态常见的面试问题


1.多态的概念

多态的概念,就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。例如:父类和子类去完成同一个函数功能时,基类与派生类的行为是有区别的,这样的功能区别就是多态。

2.多态的定义及实现

  1. 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
  2. 在继承中构成多态,需要两个条件:1.必须通过基类的指针或引用调用虚函数;2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
  3. 虚函数:即被virtual修饰的类成员函数称为虚函数。
  4. 虚函数的重写:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

示例:

class person{
public:
    //2.被调用的函数必须是虚函数
    virtual void buyticket()
    {
        cout<<"全价买票"<<endl;
    }
};
class student:public person{
public:
    //2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    virtual void buyticket()
    {
        cout<<"半价买票"<<endl;
    }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
};
void func(person &p)
{
    //1.通过基类的指针或引用调用虚函数
    p.buyticket();
}
 
void test1()
{
    perosn p;
    func(p);
 
    student s;
    func(s);
}

虚函数重写的两个例外:

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

示例:

class person{
public:
    virtual ~person()
    {
        cout<<"~person()"<<endl;
    }
};
class student:public person{
public:
    virtual ~student()
    {
        cout<<"~student()"<<endl;
    }
};
void test2()
{
    // 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
    person *p1=new perosn;
    delete p1;
    //~person()
    person *p2=new student;
    delete p2;
    //~student()
}

从上面可以看出,C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  • final:修饰虚函数,表示该虚函数不能再被重写;
class person{
public:
    virtual void buyticket() final{}
};
class student:public person{
public:
    virtual void buyticket(){}    //报错,因为基类中该虚函数被final修饰,已经不可以再被重写;
};
  • override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错;
class person{
public:
    virtual void buyticket(){}
};
class student:public person{
public:
    virtual void buyticket() override{}    //重写后编译即可通过,否则编译报错;
};

重载、重写、重定义的对比:

  1. 重载,两个函数在同一作用域,函数名/参数相同;
  2. 重写,两个函数分别在基类和派生类的作用域,函数名/参数/返回值都必须相同;
  3. 重定义,两个函数分别在基类和派生类的作用域,函数名相同,两个基类和派生类的同名函数不构成重写就是重定义;

3.抽象类

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

示例:

class person{
public:
    virtual void buyticket()=0;    //纯虚函数
};    //抽象类|接口类
class student:public person{
public:
    virtual void buyticket()
    {
        cout<<"void buyticket()"<<endl;
    }
};
void test3()
{
    person *p=new student;
    p->buyticket();
}

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

4.多态的原理

示例1:认识虚表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
    int _b = 1; //4B
};

通过观察测试我们发现_b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(虚函数指针指向一个该类虚函数表,表中存放着虚函数的调用地址)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

示例2:虚表创建情况

class base{
public:
    virtual void func1()    //虚函数
    {
        cout<<"base virtual void func1()"<<endl;
    }
    virtual void func2()   //虚函数
    {
        cout<<"virtual void func2()"<<endl;
    }
    void func3()    普通函数
    {
        cout<<"void func3()"<<endl;
    }
 
protected:
    int _b;
};    
class derive: public base{
public:
    virtual void func1() //子函数重写func1()形成多态
    {
        cout<<"derive virtual void func1()"<<endl;
    }
 
private:
    int _d;
};
void test4()
{
    base b;
    derive d;
    //断点
}

image.png

运行调试程序:

  1. 派生类对象d中也有一个虚指针,d由两部分组成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员;
  2. 基类b与派生类d的虚表内容是不一样的,因为派生类对基类func1进行了重写形成了多态,所以_vfptr[0]的内容不同(指针指向不同);而func2是继承下来的虚函数,所以_vfptr[1]虚函数指针的内容父类和子类相同;
  3. 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放一个nullptr;

示例3:多态虚表调用情况

class Person {
public:
    virtual void BuyTicket() //1
    { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
    virtual void BuyTicket() //2
    { cout << "买票-半价" << endl; }
};
void Func(Person* p)
{
    p->BuyTicket();    //先1后2
}
void test5()
{
    Person Mike;
    Func(&Mike);
 
    Student Johnson;
    Func(&Johnson);
}

image.png

5.继承和多态常见的面试问题

  1. 继承这种面向对象的方法可以让用户变富有;

  2. 动态绑定(多态)是面向对象程序设计语言的一种机制,这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象;

  3. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用;

  4. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用;

  5. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现;

  6. 声明纯虚函数的类不可以进行实例化对象;

  7. 内联函数不可以是虚函数;

  8. 一个类的不同对象共享该类的虚函数表;

  9. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表;

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

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

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

  13. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。

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

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


<您的三连和关注是我最大的动力>

🚀 文章作者:张同学的IT技术日记

分类专栏:C++系列