博主头像
小雨淅沥

Some things were meant to be.

OOP课程笔记:第五章 虚函数和多态性

第五章 虚函数和多态性

多态性:调用同一个函数名,可以根据需要但实现不同的功能。

编译时的多态性是函数重载,运行时的多态性使用虚函数实现

运行时的多态性是指在程序执行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况来动态地确定


1.基类指针访问基类函数

可以将一个派生类对象的地址赋给基类的指针变量

basep 只能引用从基类继承来的成员。

Base *basep;
basep = &b         // 基类地址
basep = &d;        // 派生类地址

下例中,使用指针调用的是基类中有的公有函数

class Point{
    float x, y;
public:
    Point() {}
    Point(float i, float j){
        x = i;
        y = j;
    }
    float area(void) {return 0.0;}
};
const float Pi = 3.14159;
class Circle : public Point{ // 类 Point 的派生类
        float radius;
public:
    Circle(float r){radius = r;}
    float area(void) {return Pi * radius * radius;}
};
void main(void){
    Point *pp; // 基类指针,可以将派生类对象的地址赋给基类指针
    Circle c(5.4321);
    pp = &c;
    cout << pp->area() << // 调用的是基类中有的公有函数
}

2.基类指针访问派生类函数

若要访问派生类中相同名字的函数,必须将基类中的同名函数定义为虚函数

将不同的派生类对象的地址赋给基类的指针变量后,就可以动态地根据这种赋值语句调用不同类中的函数 。

实现动态联编方式的前提:

1.先要声明虚函数

2.类之间满足赋值兼容规则

3.通过指针与引用来调用虚函数

area() 声明为虚函数,编译器对其进行动态联编,按照实际对象 c 调用了 Circle 中的函数 area() 。使 Point 类中的 area()Circle 类中的 area() 有一个统一的接口。

class Point{
    float x, y;
public:
    Point() {}
    Point(float i, float j){
        x = i;
        y = j;
    }
    virtual float area(void) {return 0.0;}    // 首先声明虚函数
};
const float Pi = 3.14159;
class Circle : public Point    // 类 Point 的派生类
{ 
    float radius;
public:
    Circle(float r){radius = r;}
    float area(void) {return Pi * radius * radius;}    // 虚函数再定义
};
void main(void){
    Point *pp;
    Circle c(5.4321);
    pp = &c;
    cout << pp->area() << // 调用的是派生类中的函数
}

一旦把基类的成员函数定义为虚函数,由基类所派生出来的所有派生类中,该函数均保持虚函数的特性

在派生类中重新定义基类中的虚函数时,可不用关键字 virtual 来修饰这个成员函数


注意:

1.当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名 参数的类型 、顺序 、参数的个
数必须一一对应,函数的返回的类型也相同 ,若函数名相同,但参数的个数不同或者参数的类型不同时,则属于函数的隐藏而不是虚函数

2.实现这种动态的多态性时,必须使用基类类型的指针变量 ,并使该指针指向不同的派生类对象并通过调用指针所指向的虚函数才能实现动态的多态性。

3.虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。

4.在派生类中 没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时, 则调用其基类中的虚函数 。

5.可把析构函数定义为虚函数,不能将构造函数定义为虚函数

6.虚函数与一般的成员函数相比较调用时的执行速度要慢一些

为了实现多态性,在每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现的

因此除了要编写一些通用的程序 并一定要使用虚函数才能完成其功能要求外,通常不必使用虚函数

7.一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性,以实现“一个接口,多个形态”。


关键字: override

C++11的关键字 override 用来标记派生类中的 虚函数 ,使程序员覆盖基类虚函数的意图更清晰
如果覆盖时不小心写错了形参列表,编译器会报告错误

class B{
    virtual void f1() const;
    virtual void f2 (int);
    void f3();
};
class D1 : public B{
    void f1() const override; // 正确: f1 与基类的 f1 匹配
    void f2() override;       // 错误:参数类型不匹配,不是覆盖
    void f2();                // 正确:参数表不同,隐藏基类的 f2
    void f3() override;       // 错误: f3 不是虚函数
    void f4() override;       // 错误: B 中没有 f4
};

3.纯虚函数

在基类中不对虚函数给出有意义的实现,它只是在派生类中有具体的意义。这个虚函数称为纯虚函数

含有纯虚函数的类是抽象类,不能定义对象

注意:

1.在定义纯虚函数时 不能定义虚函数的实现部分。

2.把函数名赋于 0 本质上是将指向函数体的指针值赋为初值 0 ,在没有重新定义这种纯虚函数之前是不能调用这种函数的

3.把至少包含一个纯虚函数的类,称为抽象类。这种类只能作为派生类的基类,不能用来说明这种类的对象

4.在以抽象类作为基类的派生类中必须有纯虚函数的实现部分,否则不可以产生对象

抽象类的唯一用途是为派生类提供基类,纯虚函数作用是作为派生类中的成员函数的基础并实现动态多态性

4.运行时类别识别(RTTI)

利用虚函数实现运行时的多态性

基类指针 可以访问派生类同名的成员函数

基类指针 不能访问派生类增加的成员函数

class employee {
    public:
    virtual void salary();
};
class manager : public employee {
    public:
    void salary();
};
class programmer : public employee {
    public:
    void salary();
    void bonus();
};
int main(){
    manager Harry;
    employee *pe=&Harry;
    pe -> salary();
    programmer Ron;
    pe=&Ron;
    pe -> salary();
    pe -> bonus();  //Error,基类指针不能访问派生类新增的成员
    return 0;
}

5.基类向派生类的指针转换

基类指针到派生类指针的类型转换安全吗?

基类指针指向派生类对象时,向下类型转换是安全的;

基类指针指向基类对象或者其他派生类的对象 ,那么不安全


dynamic_cast 与向下类型转换

显式类型转换运算符 dynamic_cast 操作数类型必须是带有一个或多个虚函数的类

把一个类类型对象的指针转换成同一类层次结构中的其他类的指针

把一个类类型对象的左值转换为同一类层次结构中其他类的引用


dynamic_cast 运行时异常

如果指针或左值操作数不能被安全地转换为目标类型,则 dynamic_cast 将失败

如果是对 指针 类型的 dynamic_cast 失败,则结果是空指针

如果针对 引用 类型的 dynamic_cast 失败,则抛出一个 bad_cast 类型的异常

dynamic_cast 先检验所请求的转换是否有效,只有在有效时才会执行转换,而检验过程发生在程序运行时

dynamic_cast 用于基类指针到派生类指针的安全转换,它被称为安全的向下类型转换。

如果必须通过基类指针(或引用)使用派生类的特性,而该特性又没有出现在基类接口中时,可以使用 dynamic_cast
使用 dynamic_cast 时注意:

必须对一个含有 虚函数 的类层次进行操作

结果必须 在检测是否为 0 之后才能使用

class employee{
public:
    virtual void salary();    // 虚函数
};
class manager : public employee{
public:
    void salary();
};
class programmer : public employee{
public:
    void salary();
    void bonus();    // 多一个奖金
};
void payroll(employee *pe){
    pe->salary();    // 调用salary()函数
}

void payroll(employee *pe){
    programmer *pm = dynamic_cast<programmer *>(pe); // 向下转化指针为派生类
    if (pm){
        pm->bonus();    // 非空,允许调用bonus()
    }                
    pe->salary();
}

在上例中,使用

programmer *pm = dynamic_cast<programmer *>(pe);

语句将 employee 类型的指针转化为 programmer 类型指针,使用的是 dynamic_cast


typeid 运算符

typeid 运算符对类型或表达式进行操作,返回操作数的确切类型

typeid 运算符返回一个 type_info 类型的引用,type_info 在头文件 typeinfo.h 中定义,type_info::name() 返回 C 风格字符串表示的类型名

typeid 运算符必须与表达式或类型名一起使用

typeid 运算符的操作数是类类型,但不是带有虚函数的类类型时,typeid 运算符会指出操作数的类型,而不是底层对象的类型

#include <typeinfo>
class Base{ /*没有虚函数*/ };
class Derived : public Base
{ /*没有虚函数*/ };
void main() {
    Derived dobj;
    Base* pb = &dobj;
    cout << typeid(*pb).name() << endl;        // 结果是Base
}
#include <typeinfo>
class Base{ /*含有虚函数*/ };
class Derived : public Base
{ /*含有虚函数*/ };
void main() {
    Derived dobj;
    Base* pb = &dobj;
    cout << typeid(*pb).name() << endl;        // 结果是Derived
}
OOP课程笔记:第五章 虚函数和多态性
https://www.rainerseventeen.cn/index.php/Code-Basic/16.html
本文作者 Rainer
发布时间 2025-10-18
许可协议 CC BY-NC-SA 4.0

评论已关闭