C++ 多态相关

Posted dengxinlong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 多态相关相关的知识,希望对你有一定的参考价值。

什么是多态?虚函数的实现原理是什么?

多态分为静态多态和动态多态

  • 静态多态:发生在编译时,主要有函数重载,运算符重载
  • 动态多态:发生在运行时,主要通过虚函数的形式实现
    多态性可以概括为"一个接口,多种方法",程序在运行时才决定调用的函数,多态性是oop编程的核心,C++的多态性通过虚函数实现,基类中定义虚函数,允许子类继承并且可以重新定义该函数的实现,这也就是覆盖。多态的目的就是为了接口的重用。也就是说,无论传递过来的是哪个类的对象,函数都可以通过接口调用到适合各自对象的实现方法。
    最常见的用法就是声明一个基类指针或者引用,然后该指针或者引用绑定一个派生类的对象,调用相应对的虚函数,可以根据指向的派生类的不同而实现不同的方法。

虚函数实现原理:当在类中定义一个虚函数,则该类所实例化的对象的存储位置将会多一个vfptr指针,该指针指向一个虚函数表,虚函数表中存放的就是虚函数的入口地址


构造函数能设置为虚函数吗?为什么?

构造函数不能设置为虚函数,虚函数就是为实现C++的OOP编程中的多态性而设定的,而动态多态性的激活条件是:

  • 基类中有定义虚函数,同时其派生类中有定义该函数的覆盖函数;
  • 创建派生类对象
  • 定义一个基类的指针或者引用,同时绑定到其派生类的对象中; 通过已经定义的基类指针或者引用调用相应的虚函数

==由上面动态多态性的激活条件可以知道,要使用虚合理函数,则必须先创建派生类的对象,而在创建派生类对象的过程中必将调用其基类的构造函数,而如果基类中的构造函数是虚函数,而此时派生类对象又没有创建,陷入了矛盾的境地,所以构造函数不能设置为虚函数,同时也是没有任何意义的一件事==


在什么情况下析构函数要设置成虚函数?为什么?

当基类中的数据成员申请了堆空间,同时该基类中也定义了析构函数用于销毁该空间和虚函数。如果该基类有一个派生类,该派生类中也申请了一块堆空间。这时我们如果new一个派生类中空间,把new表达式返回的派生类指针赋给一个基类指针。如果我们使用delete表达式来释放刚才申请的派生类空间,这时只会调用基类的析构函数,不会调用派生类的析构函数,因为已经把派生类的指针赋给了基类指针。这时我们应该把析构函数设置为虚函数,在调用派生类的析构函数的同时,也将会调用基类的析构函数,这时所有申请的资源都得到了释放。代码如下:

#include <string.h>

#include <iostream>

using std::cout;
using std::endl;

class Base

public:
    Base(const char * base)
    : _base(new char[strlen(base) + 1]())
    
        strcpy(_base, base);
        cout << "Base(const char * base)" << endl;
    

    virtual void print(void) const
    
        cout << "base = " << _base << endl;
    

    ~Base()
    
        if (_base)
            delete [] _base;
        cout << "~Base()" << endl;
    

private:
    char * _base;
;

class Derived
: public Base

public:
    Derived(const char * base, const char * derived)
    : Base(base)
    , _derived(new char[strlen(base) + 1]())
    
        strcpy(_derived, derived);
        cout << "Derived(const char * base, const char * derived)" << endl;
    
    
    void print(void) const 
    
        cout << "derived = " << _derived << endl;
    

    ~Derived()
    
        if (_derived)
            delete [] _derived;
        cout << "~Derived()" << endl;
    
private:
    char * _derived;

;

int main(void)

    Base * base = new Derived("hello", "world");
    base->print();

    delete base;

    return 0;

运行结果

Base(const char * base)
Derived(const char * base, const char * derived)
derived = world
~Base()

分析:对于虚函数的调用这里就不再分析,就是派生类将基类的虚函数给覆盖了,这时从基类对象调用基类中的虚函数,将会调用派生类的虚函数。这里分析析构函数,通过运行结果可知,在delete base时,基类的析构函数被调用了,但是派生类的析构函数没有被调用,也就是说,基类中申请的堆资源得到了释放,而派生类中的资源没有得到释放,这不是我们想要的结果,原因上面已经分析了。要解决这个问题,应该想到,在调用派生了析构函数时,基类的析构函数也会被自动调用,我们可以尝试调用派生类的析构函数,那么怎么调用派生类的析构函数呢?这里可以利用虚函数的覆盖特性,我们把基类的析构函数设置为virtual,这时delete base时将调用派生类的析构函数,达到了我们的目的,代码修改如下:

virtual
    ~Base()
    
        if (_base)
            delete [] _base;
        cout << "~Base()" << endl;
    

运行结果:

Base(const char * base)
Derived(const char * base, const char * derived)
derived = world
~Derived()
~Base()

当基类析构函数设置为虚函数后,其派生类析构函数会自动成为虚析构函数,即使没有加上virtual关键字,如果接下来要调用析构函数,就会走虚函数的调用机制


在基类的普通成员函数内部调用虚函数时的情况

#include <iostream>

using std::cout;
using std::endl;

class Base 

public:
    Base() = default;

    Base(int base_val)
    : _base_val(base_val)
    
        cout << "Base(int base_val)" << endl;
    

    void func1(void)
    
        //this->display();
        display();
    

    void func2(void)
    
        Base::display();
    

    virtual void display(void) const
    
        cout << "base_val = " << _base_val << endl;
    

private:
    int _base_val;

;

class Derived
: public Base

public:
    Derived(int base_val, int derived_val)
    : Base(base_val)
    , _derived_val(derived_val)
    
        cout << "Derived(int base_val, int derived_val)" << endl;
    

    void display(void) const
    
        cout << "derived_val = " << _derived_val << endl;
    
private:
    int _derived_val;
;

int main(void)

    Base base(12);
    base.func1();

    Derived derived(34, 56);
    derived.func1();

    cout << "-----------" << endl;
    base.func2();
    derived.func2();

    return 0;

运行结果:

Base(int base_val)
base_val = 12
Base(int base_val)
Derived(int base_val, int derived_val)
derived_val = 56
-----------
base_val = 12 base_val = 34

分析:由上面的代码可知,display()函数定义为了虚函数,同时在基类中的func1 和 func2 函数中通过两种方式来访问该虚函数; Base base(12); base.func1();对于这段代码很好理解,就是基本的创建对象,然后访问对象中的成员,只不过在该成员中访问了该对象的虚函数,但由于没有继承关系,所以和普通的访问没有区别;

Derived derived(34, 56);
derived.func1();

对于这段代码,由于 Derived类继承自Base类,在继承了基类中的 func1、func2和虚函数后,由于Derived类中重新定义了该虚函数,这时子类的同名函数会覆盖基类中的虚函数,所以这时func1调用的就是子类中重定义的虚函数;

base.func2();
derived.func2();

上面的代码通过运行结果可知,虽然是derived对象调用的func2函数,但是该函数内部通过作用域运算符调用了虚函数,这时的调用将会直接调用基类中的虚函数,而不会调用派生类中的虚函数


在基类的构造函数和析构函数内部调用虚函数时的情况

#include <iostream>

using std::cout;
using std::endl;

class Grandpa 

public:
    Grandpa(int grandpa_val)
    : _grandpa_val(grandpa_val)
    
        cout << "Grandpa(int grandpa_val)" << endl;
    

    virtual
    void func1(void)
    
        cout << "Grandpa: func1()" << endl;
    

    virtual
    void func2(void)
    
        cout << "Grandpa: func2()" << endl;
    

    ~Grandpa()
    
        cout << "~Grandpa()" << endl;
    
private:
    int _grandpa_val;

;

class Son
: public Grandpa

public:
    Son(int grandpa_val, int son_val)
    : Grandpa(grandpa_val)
    , _son_val(son_val)
    
        cout << "Son(int grandpa_val, int son_val)" << endl;
        func1();
    

#if 1
    void func1(void)
    
        cout << "Son: func1()" << endl;
    

    void func2(void)
    
        cout << "Son: func2()" << endl;
    

#endif 

    ~Son()
    
        cout << "~Son()" << endl;
        func2();
    
private:
    int _son_val;
;

class Grandson
: public Son

public:
    Grandson(int grandpa_val, int son_val, int grandson_val)
    : Son(grandpa_val, son_val)
    , _grandson_val(grandpa_val)
    
        cout << "Grandson(int grandpa_val, int son_val, int grandson_val)" << endl;
    

    void func1(void)
    
        cout << "Grandson: func1()" << endl;
    

    void func2(void)
    
        cout << "Grandson: func2()" << endl;
    

    ~Grandson()
    
        cout << "~Grandson()" << endl;
    

private:
    int _grandson_val;
;

int main(void)

    Grandson test(1, 2, 3);
    //cout << "-------------/\n";
    Grandpa  & tt = test;
    cout << "-------------/\n";
    tt.func1();
    return 0;

运行结果

Grandpa(int grandpa_val)
Son(int grandpa_val, int son_val)
Son: func1()
Grandson(int grandpa_val, int son_val, int grandson_val)
-------------/
Grandson: func1()
~Grandson()
~Son()
Son: func2()
~Grandpa()

由代码和运行结果可知,当我们在构造函数或者析构函数内部调用虚函数,虽然表面上看和动态联编没说区别,但是,在这两个函数体内调用虚函数,其实本质上调用的是自身类中的函数(不是虚函数),或者说没有从虚函数的机制进行调用,而是按照普通函数的机制进行调用(静态联编)


验证虚函数表的存在

我们知道,如果一个基类中定义了虚函数,同时派生类中覆盖了该虚函数,这时实例化派生类时创建的派生类对象的存储空间当中将会多一个指向虚函数表的指针,使得能够表现为动态多态的性质,这里就对虚函数表的存在进行验证,代码如下:

#include <iostream>

using std::cout;
using std::endl;

class Base

public:
    Base(long base_val)
    : _base_val(base_val)
    
        cout << "Base(int base_val)" << endl;
    

    virtual 
    void func1(void)       cout << "Base::func1()" << endl;    

    virtual 
    void func2(void)       cout << "Base::func2()" << endl;    

    virtual 
    void func3(void)       cout << "Base::func3()" << endl;    

private:
    long _base_val;

;

class Derived
: public Base

public:
    Derived(long base_val, long derived_val)
    : Base(base_val)
    , _derived_val(derived_val)
    
        cout << "Derived(long base_val, long derived_val)" << endl;
    

    virtual 
    void func1(void)       cout << "Derived::func1()" << endl;    

    virtual 
    void func2(void)       cout << "Derived::func2()" << endl;    

    virtual 
    void func3(void)       cout << "Derived::func3()" << endl;    

private:
    long _derived_val;
    
;

int main(void)

    Base base(10);
    cout << "sizeof(Base) = " << sizeof(Base) << endl;  

    long * p = (long *)&base;
    cout << p[0] << endl;
    cout << p[1] << endl; 

    typedef void (*Function)(void);

    long * p2 = (long *)p[0];
    Function f = (Function)p2[0];
    //Function f = (Function)p[0][0];
    f();

    f = (Function)p2[1];
    f();

    f = (Function)p2[2];
    f();

    return 0;

运行结果

Base(int base_val)
sizeof(Base) = 16
94272637283664
10
Base::func1()
Base::func2()
Base::func3()

编译和运行环境是gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1) ,所以系统的是64位的,所以在指针的大小是8个字节(64位),所以为了方便验证,这里用long类型的来存储数据。我们知道,对象base的在栈中的大小是16个字节,其中8个字节的long类型数据,8个字节的指针,指针也就是vfptr指针,指向虚函数表的,这里就是验证虚函数表是否真的存在。对对象base取地址,强制转换为long类型,看 p[1]数据,就是我们一开始初始化base对象时赋的值, p[0] 则是一个指针,指向虚函数表,因为指针的大小是8个字节,所以我们再一次把 p[0]强制转换为long* 类型,从程序中我们可以看出来,p[0] 中共有3个指针,也就是 p2中的数据,因为 p2中的数据是8个字节的指针,但编译器无法识别这个地址数据,所以我们把该数据强制转换为指针函数,这样就可以调用虚函数了。从而验证了虚函数表哦的存在性


什么是纯虚函数?什么是抽象类?抽象类的作用是什么?

  • 纯虚函数指的在基类中的是一个虚函数没有定义,而是在声明该纯虚函数的时候在后面添加 = 0;即可,这时基类中的虚函数就是纯虚函数
  • 如果在基类中定义纯虚函数,则该基类就是抽象基类,抽象基类只向外提供接口,而不能实例化对象,当该基类的派生类可以实例化对象,不过前提是这些派生类定义了覆盖这些纯虚函数的函数,通过这里我们就一目了然了,抽象基类只负责提供接口的定义,不负责接口的实现,接口的实现由该基类的派生类来完成。代码如下:
#include <math.h>

#include <iostream>

using std::cout;
using std::endl;

class Figure            //定义一个抽象基类

public: 
    virtual void display(void) const = 0;   //定义纯虚函数
    virtual double area(void) const = 0;
;

void display(Figure & figure)

    figure.display(); 
    cout << ", the area is " << figure.area() << endl;


class Circle
: public Figure

public:
    Circle(double radius)
    : _radius(radius)
    

    void display(void) const
    
        cout << "I am Circle";
    

    double area(void) const
    
        return 3.14 * _radius * _radius;
    

private:
    double _radius;
;

class Rectangle
: public Figure

public:
    Rectangle(double length, double width)
    : _length(length)
    , _width(width)
    

    void display(void) const
    
        cout << "I am Rectangle";
    

    double area(void) const
    
        return _length * _width;
    

private:
    double _length;
    double _width;
;

class Triangle
: public Figure

public:
    Triangle(double a, double b, double c)
    : _a(a)
    , _b(b)
    , _c(c)
    

    void display(void) const
    
        cout << "I am Triangle";
    

    double area(void) const
    
        double p = (_a + _b + _c)/2;
        return sqrt(p * (p - _a) * (p - _b) * (p - _c));
    

private:
    double _a;
    double _b;
    double _c;
;

int main(void)

    Circle circle(10);
    Rectangle rec(10, 12);
    Triangle triangle(3, 4, 5);

    display(circle);
    display(rec);
    display(triangle);


    return 0;

运行结果:

I am Circle, the area is 314
I am Rectangle, the area is 120
I am Triangle, the area is 6

分析:从上面的代码可以知道,Figure为抽象基类,只定义向外提供的各种接口,而实现则由该基类的派生类来完成,在main函数中也可以看出来,调用同一个函数,但结果却是不一样的。这种抽象基类遵循的原则是:对修改关闭(不需要修改原来的代码),对扩展开放。
另:对于这种抽象基类应该注意的是,只要基类中有定义的纯虚函数,则其派生类中必须对基类中的所有纯虚函数给予实现,只要有一个纯虚函数没有实现,则该派生类也不能创建对象。
上面是按照定义纯虚函数来实现抽象类,这里还有另外一种抽象类,就是将基类的构造函数设置为protected,这时的基类是不能实例化对象的,要调用基类的构造函数,通过该基类派生出子类,实例化该子类,通过该对象来调用基类中可以访问的成员,代码如下:

#include <iostream>

using std::cout;
using std::endl;

class Base

public:
    virtual void print(void)
    
        cout << "I am Base_class" << endl;
        cout << "base_val = " << _base_val << endl;
    

protected:              //把基类的构造函数定义为protected性质,这时外部非派生类将不能对该类进行实例化,只能通过该类的派生来调用该基类的构造函数
    Base(int base_val)
    : _base_val(base_val)
    
        
        cout << "Base(int base_val)" << endl;
    

private:
    int _base_val;
;

class Inherit_cls
: public Base

public:
    Inherit_cls(int base, int inherit_val)      //通过派生类中的构造函数来调用基类中的构造函数
    : Base(base)
    , _inherit_val(inherit_val)
    
        cout << "Inherit_cls(int base, int inherit_val) " << endl;
    

    void print(void)
    
        cout << "I am Inherit_cls" << endl;
        cout << "inherit_val = " << _inherit_val << endl;
    

private:
    int _inherit_val;

;

int main(void)

    Inherit_cls test(12, 34);
    Base & base = test;
    base.print();

    return 0;

分析:上面的代码中是C++语言的动态多态性,但是把基类中的构造函数设置为了protected,这时是无法通过该基类来创建基类对象,只能通过基类的派生类创建对象,这种抽象类的功能相比较前一种而言弱了很多,对于抽象类,应该使用纯虚函数来实现。

什么是重载?什么是隐藏?什么是覆盖?他们之前的区别是?

  • 重载:重载可以在同一个类中,也可以是普通函数,只要函数的名称相同但是函数的参数个数,参数类型,返回值不同,就可以实现重载
  • 隐藏:隐藏指的有继承关系的父子类中的同名函数,如果基类中有一个函数function,而子类中也有一个函数function,这时将会隐藏基类中的function函数,想要调用基类中的function函数,只需要加一个作用域限定符即可
  • 覆盖:指的是虚函数,如果基类中定义了一个虚函数,同时该基类的派生类也定义了与基类中同名、同返回值、同参数列表的函数(可以没有virtual修饰),这时实例化该派生类对象,这时派生类中的虚函数将会覆盖掉从基类中继承过来的虚函数

多重继承虚函数的情况

#include <iostream>

using std::cout;
using std::endl;

class A

public: 
    virtual
    void f(void)       cout << "A::f()" << endl;   

    virtual 
    void g(void)       cout << "A::g()" << endl;   

    virtual 
    void h(void)       cout << "A::h()" << endl;   
;

class B

public: 
    virtual
    void f(void)       cout << "B::f()" << endl;   

    virtual 
    void g(void)       cout << "B::g()" << endl;   
 
    void h(void)       cout << "B::h()" << endl;   

    void j(void)       cout << "B::j()" << endl;   
;

class C 
: public A
, public B

public:
    virtual
    void f(void)       cout << "C::f()" << endl;   

    void h(void)       cout << "C::g()" << endl;   

    void j(void)       cout << "C::j()" << endl;   
;


int main(void)

    C c;
    cout << "sizeof(A) = " << sizeof(A) << endl;

    A & a = c;
    a.f();
    a.g();
    a.h();
    cout << "-----------------" << endl;

    B & b = c;
    b.f();
    b.g();
    b.h();
    b.j();

    c.f();
    //c.g();
    c.h();
    c.j();

    return 0;

运行结果

sizeof(A) = 8
C::f()
A::g()
C::g()
/-----------------
C::f()
B::g()
B::h()
B::j()
/---------------
C::f()
C::g()
C::j()

由上面的代码可知,这里的多重继承虚函数指的就是C多重继承自A,B两个类;同时C也继承了A,B的虚函数表,如果C类中有定义覆盖A,B中的虚函数,则虚函数表中的虚函数将发生变化,将原来基类中的虚函数覆盖为派生类中的虚函数,这就表现了动态多态的性质。对于此类情况,把各种类的虚函数继承关系画出来,就一目了然。

以上是关于C++ 多态相关的主要内容,如果未能解决你的问题,请参考以下文章

C++:多态(重写,多态原理单继承和多继承的虚函数表)

C++:多态(重写,多态原理单继承和多继承的虚函数表)

C++ template —— 动多态与静多态

C++多态 --- 多态问题抛出与virtual关键字

C++之多态总结(多态的定义及实现,抽象类,多态原理,单继承,多继承中的虚函数表)

C++卷积神经网络实例:tiny_cnn代码详解(12)——从CNN中看多态性