c++类——多态(二)

本文最后更新于:1 年前

(一)动态联编:

1. 概念:

编译程序在编译阶段不能确定将要调用的函数,只有在程序执行时才能动态地确定将要调用的同名函数,为此。如果要确切地指明将要调用的函数,就要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或称动态束定,又叫晚期联编。

一旦涉及到虚函数和多态性均应当使用动态联编。

2. 使用:

必须用基类指针调用派生类的不同实现版本,且被调用的必须是虚函数,且必须完成对基类虚函数的重写(稍后说明虚函数和重写),动态联编对成员函数的选择是基于指针所指向的对象类型。(因为基类指针引用派生类对象不需要进行显式转换)

3. 优点:

灵活性强但效率低。

(二)虚函数:

1. 概念:

虚函数是在基类中使用关键字virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

virtual关键字只用在类定义里的函数声明中,写函数体时不用。

c++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此,在子类从新声明该虚函数时,可以加,也可以不加,但习惯上每一层声明函数时都加virtual,使程序更加清晰。

虚函数需要重写,一个虚函数在派生类层界面相同的重载函数都保持虚特性。

  • 虚函数重写:虚函数重写是指在基类中声明实现的虚函数在派生类中再次实现,且基类和派生类中虚函数的原型必须保持一致(返回值类型,函数名称以及参数列表),协变函数(基类或派生类的虚函数返回本身类型的指针或引用)和析构函数除外。

    协变函数:子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。

    重写的访问限定符可以不同。

    如果仅仅返回类型不同,c++认为是错误重载。

    如果函数原型不同,仅函数名相同,会丢失虚特性。

  • 隐藏:不局限于虚函数,但需要和虚函数概念结合理解。

    隐藏是指派生类的函数屏蔽了与其同名的基类函数。

    如果派生类的函数与基类的函数同名,但是参数不同。此时,不论是否为虚函数,基类的函数将被隐藏(注意不是重载,重载是在同一个类中发生)。

    如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意不是重写,重写有virtual关键字)。

——实际上共有四种情况:

  1. 参数相同、有virtual关键字:多态重写;

  2. 参数相同、无virtual关键字:隐藏;与重写区分。

  3. 参数不同、有virtual关键字:隐藏;与重载区分。

  4. 参数不同、无virtual关键字:隐藏;与重载区分。

2. 限制:

  • 只有类的成员函数才能声明为虚函数(友元函数、全局函数等都不可以),虚函数仅适用于有继承关系的类对象。普通函数不能声明为虚函数。

  • 静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。

  • 内联函数不能是虚函数,因为内联函数不能在运行中动态确定位置。

  • 构造函数不能是虚函数:

    建立派生类对象是从类层次的根开始沿着继承路径逐个调用基类的构造函数,声明为虚的无法全部初始化,而且构造函数是创建时自动调用的,不可能用父类的指针引用or调用。

  • 析构函数可以是虚函数,而且建议声明为虚函数。

    虚析构函数用于指引delete运算符正确析构动态对象。否则可能不能正确识别对象类型而不能正确调用析构函数。

    实际上,析构函数被编译器全部换成了Destructor,所以加上virtual就可以成为虚析构函数。只要父类的析构函数用virtual修饰,无论子类是否有virtual,都构成析构。这也解释了为什么派生类不写virtual可以构成重写,因为编译器担心你忘记析构。

    实际上,析构函数即使是虚函数,仍为静态联编。因为它们所调用的虚函数是自己的类,或者基类中定义的函数而不是在任何派生类中重定义的函数

  • 不建议把赋值运算符重载作为虚函数。

3. 工作原理:

C++编译器处理虚函数的方法是给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向存放函数地址的数组的指针(vptr)。这种数组称为虚函数表(vtable)。虚函数表中存储了为类对象进行声明的虚函数的地址。

注意:这个隐藏成员默认是第一个数据成员,在地址中分布在最开始处。

虚函数表只存在于有虚函数的类及其派生类中,会使其多出8字节,这8字节存放了虚函数表本身的地址。

如:基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。

派生类对象将包含一个指向独立地址表(也就是和基类无关)的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。

如果派生类没有重新定义虚函数,该指针将保存原始版本的地址(理论上和基类的虚函数表相同)。如果派生类定义了新的虚函数(指基类没有的),则该函数的地址也将被添加到该指针中。

不论包含的虚函数数量,都只需要在对象中添加一个地址成员,只是大小不同而已。

当调用虚函数时,程序将查看存储在对象中的指针地址,然后转向相应的函数地址表。如果类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行该地址对应的函数。如果使用类声明中的第三个函数,程序将使用地址为数组中的第三个元素的函数。

所以使用虚函数和动态联编,无可避免地会增加内存和时间的开销,

  • 每个对象都将增大,增大量为存储地址的空间;

  • 对于每个类,编译器都会创建一个虚函数地址表(本质上就是数组);

  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

4. C++11 override && final

C++11新增了两个关键字override和final。

被override修饰的虚函数,编译器会检查这个虚函数是否重写。如果没有重写,编译器会报错。

用final修饰的虚函数无法重写。用final修饰的类无法被继承。final像这个单词的意思一样,这就是最终的版本,不用再更新了。

5. 表现:

  • 通过基类指针调用基类和派生类中的同名虚函数时:

    1. 若该指针指向一个基类的对象,那么被调用的是基类的虚函数;
    2. 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

    这种机制就是实际上的“多态”——调用哪个虚函数,取决于指针对象指向哪种类型的对象。

  • 通过基类引用调用基类和派生类中的同名虚函数时:

    1. 若该引用引用了一个基类的对象,那么被调用的是基类的虚函数;
    2. 若该引用引用了一个派生类的对象,那么被调用的是派生类的虚函数。

    这种机制也是实际上的“多态”——调用哪个虚函数,取决于指针对象指向哪种类型的对象。

6.代码:

  • 一般情况的函数的虚函数重写,发生了动态联编(各种错误也在其中):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    #include<iostream>
    using namespace std;
    class Person
    {
    //不允许对友元函数使用virtual
    //virtual friend void fun();
    public:
    //如果下面的静态数据成员和成员函数被声明,不允许加virtual,必须去掉
    //static int i;
    //virtual static void addi(){}

    //不能对构造函数加virtual,必须去掉
    //virtual Person() {}

    //一般虚函数声明
    virtual void fun()
    {
    cout << "Person->fun()" << endl;
    }

    //内联虚函数,能否内联成功取决于编译器对其的判断是否为虚函数
    inline virtual void fun2()
    {
    cout << "Person->fun()2" << endl;
    }
    //不被允许的操作:仅改变返回类型,编译器无法区别。
    //virtual int fun()
    //{
    // cout << "Person->fun()" << endl;
    //}
    };
    //不能对全局函数使用virtual
    //virtual void fun(){}
    class Student:public Person
    {
    public:
    virtual void fun()//子类重写父类虚函数
    {
    cout << "Student->fun()" << endl;
    }
    //重写内联虚函数,能否内联成功取决于编译器对其的判断是否为虚函数
    inline virtual void fun2()
    {
    cout << "Student->fun()2" << endl;
    }
    };
    class Teacher:public Person
    {
    public:
    void fun()//子类重写父类虚函数,不加virtual但编译器默认其为虚函数
    {
    cout << "Teacher->fun()" << endl;
    }
    //下面的fun函数因为有参数,和前面了产生了函数原型的区别,
    //实际上丢失了虚特性
    void fun(int)
    {
    cout << "Person2->fun()" << endl;
    return;
    }
    };
    class GoodStudent :public Student
    {
    virtual void fun()//子类重写父类虚函数
    {
    cout << "GoodStudent->fun()" << endl;
    }
    };
    class GoodTeacher :public Teacher
    {
    void fun()//子类重写父类虚函数,不加virtual但编译器默认其为虚函数
    {
    cout << "GoodTeacher->fun()" << endl;
    }
    };
    int main()
    {
    Student s;
    GoodStudent gs;
    Teacher t;
    GoodTeacher gt;
    Person* p = &s;
    //基类指针指向派生类,因为虚函数的声明,所以调用的是派生类的重写虚函数。
    p->fun();
    //此处发生内联,因为用对象执行成员函数是可以在编译时确定联编的
    s.fun2();
    //此处未发生内联,inline被自动忽略了,因为用指针发生虚函数执行,此时是动态联编,不能直接内联。
    p->fun2();
    p = &t;
    //同p->fun();
    p->fun();
    p = &gs;
    //调用GoodStudent的重写虚函数
    p->fun();
    p = &gt;
    //调用GoodTeacher的重写虚函数
    p->fun();
    Student* ps = &gs;
    //派生类Student指针指向其派生类GoodStudent,调用GoodStudent的重写虚函数
    ps->fun();
    Teacher* pt = &gt;
    pt->fun();
    pt->Teacher::fun();//访问限定符可以不同,强制执行Teacher的fun函数

    cout << endl;
    //基类引用引用派生类,和指针效果相同。
    //注意引用一经确定对象后就无法修改了
    Person& cp1=s;
    cp1.fun();
    cp1.fun2();
    Person& cp2 = t;
    cp2.fun();
    Person& cp3 = gs;
    cp3.fun();
    Person& cp4 = gt;
    cp4.fun();
    return 0;
    }
    //运行结果:
    //Student->fun()
    //Student->fun()2
    //Student->fun()2
    //Teacher->fun()
    //GoodStudent->fun()
    //GoodTeacher->fun()
    //GoodStudent->fun()
    //GoodTeacher->fun()
    //Teacher->fun()
    //
    //Student->fun()
    //Student->fun()2
    //Teacher->fun()
    //GoodStudent->fun()
    //GoodTeacher->fun()
  • 协变函数的重写,发生了动态联编:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    #include<iostream>
    using namespace std;
    class Person
    {
    public:
    virtual Person* fun()//返回父类指针
    {
    cout << "Person->fun()" << endl;
    return nullptr;
    }
    };
    class Student:public Person
    {
    public:
    //返回子类指针,虽然返回值不同,也构成重写
    virtual Student* fun()//子类重写父类虚函数
    {
    cout << "Student->fun()" << endl;
    return nullptr;
    }
    };
    int main()
    {
    Person p;
    Student s;
    Person* pp = &p;
    pp->fun();
    pp = &s;
    pp->fun();//执行Student的成员函数
    return 0;
    }
  • 虚函数表指针的占用大小和实际作用:

    64位和32位的大小不同(因为虚指针vptr在64位占8字节,int发生了自动补齐,需要全部按数据成员最大占8字节处理所有数据成员,32位因为vptr只占4位,不会发生自动补齐)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    #include<iostream>
    using namespace std;
    class Base
    {
    public:
    int i;
    virtual void Print() { } // 虚函数
    virtual void Print2() { } // 虚函数
    };

    class Derived : public Base
    {
    public:
    int n;
    virtual void Print() { } // 虚函数
    virtual void Print2() { } // 虚函数
    };

    class Base2
    {
    public:
    int i;
    void Print() { }
    };

    int main()
    {
    Base2 b;
    Base bb;
    Derived d;
    //64位下运行结果:
    cout << sizeof(b) << endl; //4->int
    cout << sizeof(bb) << endl; //16=8+8->vptr(64位下占8字节)+int(从4补齐到8)
    cout << sizeof(d) << endl; //24=8+8+8->vptr+int+int(均从4补齐到8)

    cout << sizeof(Base2) << endl; //4
    cout << sizeof(Base) << endl; //16
    cout << sizeof(Derived) << endl; //24
    //32位下运行结果:
    cout << sizeof(b) << endl; //4->int
    cout << sizeof(bb) << endl; //8=4+4->vptr(64位下占4字节)+int
    cout << sizeof(d) << endl; //12=4+4+4->vptr+int+int

    cout << sizeof(Base2) << endl; //4
    cout << sizeof(Base) << endl; //8
    cout << sizeof(Derived) << endl; //12

    Base* pb = new Derived();
    pb->Print();
    //Derive
    int* p1 = (int*)&bb;
    int* p2 = (int*)pb;

    *p2 = *p1;
    pb->Print();
    //Base
    return 0;
    }

    Derive原因:pb指向的是Derived对象,因此pb执行Print()时实际执行的是Derived的虚函数。

    p1实际是Base的虚指针(64位请改成double,因为要占8位才能和pvtr对齐),p2实际是Derive的虚指针。

    通过*p2=*p1,将Base的虚指针赋给了Derive的虚指针,从而修改了Derive对应的虚指针,使其也指向了Base的虚函数表,故代码后面的调用最终执行了Base的虚函数。

  • 虚析构函数的实际效果:

    如下,对于基类指针指向派生类对象,如果对析构函数进行了virtual声明,在析构此指针指向对象时调用的是派生类的析构函数(这个析构函数可以再调用基类的析构函数);如果不进行声明,在析构此指针指向对象时调用的是基类的析构函数。

    显然前者是我们更需要的,否则派生类中的某些成员我们无法析构。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    #include<iostream>
    using namespace std;
    class A
    {
    public:
    virtual ~A()
    {
    cout << "~A()" << endl;
    }
    };
    class B : public A
    {
    public:
    virtual ~B()
    {
    cout << "~B()" << endl;
    }
    };
    class A2
    {
    public:
    ~A2()
    {
    cout << "~A2()" << endl;
    }
    };
    class B2 : public A2
    {
    public:
    ~B2()
    {
    cout << "~B2()" << endl;
    }
    };
    int main()
    {
    A* a = new B;
    delete a;
    //~B()
    //~A()
    A2* a2 = new B2;
    delete a2;
    //~A2()
    return 0;
    }
  • override && final:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include<iostream>
    using namespace std;
    class A
    {
    public:
    virtual void fun() {}
    };

    class B : public A
    {
    public:
    //正确,前面有声明的成员虚函数可供其重写,不会报错
    virtual void fun() override{}
    //错误:使用“override”声明的成员函数不能重写基类成员
    //也就是前面必须有基类成员声明为了虚函数可供其重写
    virtual void fun2() override{}
    };

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include<iostream>
    using namespace std;
    class A final //A类无法被继承
    {
    public:
    virtual void fun() final //fun函数无法被重写
    {}
    };

    class B : public A //错误:不能将final类类型作为基类
    {
    public:
    virtual void fun() //错误:不能重写final中的虚函数
    {
    cout << endl;
    }
    };

c++类——多态(二)
https://github.com/xiaohei07/xiaohei07.github.io/2023/04/08/c++类——多态(二)/
作者
07xiaohei
发布于
2023年4月8日
许可协议