【C++复习】第八章 多态性(2)(虚函数,纯虚函数)

虚函数动态绑定的基础,必须是非静态的成员函数

1、一般虚函数

1.1 引例

程序 运行结果&解释
#include <iostream>
using namespace std;

class Base1
{
public:
    void display() const;//这个不是虚函数
};
class Base2 : public Base1
{
public:
    void display() const;
};
class Derived : public Base2
{
public:
    void display() const;
};
void Base1::display() const
{
    cout << "Base1::display()" << endl;
}
void Base2::display() const
{
    cout << "Base2::display()" << endl;
}
void Derived::display() const
{
    cout << "Derived::display()" << endl;
}
void fun(Base1 *p)
{
    p->display();
}
int main()
{
    Base1 b1;
    Base2 b2;
    Derived d1;
    fun(&b1);
    fun(&b2);
    fun(&d1);
    return 0;
}

运行结果:

Base1::display()
Base1::display()
Base1::display()

解释:

使用对象名,绑定发生在编译过程中。

根据类型兼容性规则,派生类对象的地址会被转换为指向基类的指针,fun中的p->display()会执行基类中的display()

/*
8-4 P316
虚函数成员
*/
#include <iostream>
using namespace std;

class Base1
{
public:
    virtual void display() const; //这个函数是虚函数,可多态
};
class Base2 : public Base1
{
public:
    void display() const;
};
class Derived : public Base2
{
public:
    void display() const;
};
void Base1::display() const
{
    cout << "Base1::display()" << endl;
}
void Base2::display() const
{
    cout << "Base2::display()" << endl;
}
void Derived::display() const
{
    cout << "Derived::display()" << endl;
}
void fun(Base1 *p)
{
    p->display();
}
int main()
{
    Base1 b1;
    Base2 b2;
    Derived d1;
    fun(&b1);
    fun(&b2);
    fun(&d1);
    return 0;
}

运行结果:

Base1::display()
Base2::display()
Derived::display()

解释:

fun()中的实参p绑定到了不同子类的指针,程序知道这一点,故执行各子类的display(),实现了多态(运行时)

1.2 一般虚函数

1.2.1 什么是一般虚函数?

  1. 首先需要在基类中将这个同名函数声明为虚函数,这样通过基类类型的指针(引用)就可以使属于不同派生类的不同对象产生不同的行为,实现运行过程的多态
  2. 虚函数声明只能出现在类定义中的函数原型声明中,而不能出现在成员函数
    实现的时候!(一般不声明为内联函数)
  3. 虚函数成员语法:virtual 函数类型 函数名(形参表);

1.2.2 运行时多态需满足三个条件:

  1. 类之间满足兼容规则
  2. 声明虚函数
  3. 成员函数来调用,或者通过指针引用访问虚函数

1.2.3 系统如何判断派生类的函数成员是否为虚函数?

  1. 该函数是否与基类的虚函数有相同的名称
  2. 该函数是否与基类的虚函数有相同的参数个数及相同的对应数类型?(包括const)
  3. 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型返回值

1.2.4 一般虚函数的补充说明

  1. 只有虚函数是动态绑定的(如果派生类需要重写与基类函数同名的函数时,应该在基类中将相应的函数声明为虚函数)
  2. 基类中声明的非虚函数,通常代表那些不希望被派生类修改的函数,是不能实现多态的。
  3. 在重写继承来的虚函数时,如果函数有缺省值,不要重新定义不同的值(虚函数是动态绑定的,但缺省形参值是静态绑定的,缺省形参值只能来自基类的定义
  4. 只有通过基类指针或者引用调用虚函数时,才会发生动态绑定!(基类的指针可以指向派生类的对象,基类的引用可以作为派生类对象的别名,但基类的对象不能表示派生类的对象)
    Derived d;
    Base *ptr=&d; //基类的指针ptr可以指向派生类的对象
    Base &ref=d;  //基类的引用ref可以作为派生类对象的别名
    Base b=d;     //调用Base1的复制构造函数用d构造b,b的类型是Base而非Derived
  5. 不能声明虚构造函数,但可以声明虚析构函数(保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作)

2、虚析构函数

2.1 引例

程序 运行结果&解释
#include <iostream>
using namespace std;
class Base
{
public:
    ~Base(); //不是虚函数
};
Base::~Base()
{
    cout << "Base destructor" << endl;
}
class Derived : public Base
{
public:
    Derived();
    ~Derived(); //不是虚函数
private:
    int *p;
};

Derived::Derived()
{
    p = new int(0);
}
Derived::~Derived()
{
    cout << "Derived destructor" << endl;
    delete p;
}
void fun(Base *b)
{
    delete b; //静态绑定,只会调用~Base()
}
int main()
{
    Base *b = new Derived();
    fun(b);
    return 0;
}

运行结果:

Base destructor

解释:

此时通过基类指针删除派生类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有被释放,造成了内存泄露。

对于长期运行的程序来说,这是非常危险的!

/*
8-5 P320
虚析构函数举例
*/
#include <iostream>
using namespace std;
class Base
{
public:
    // ~Base();         //不是虚函数
    virtual ~Base(); //是虚函数
};
Base::~Base()
{
    cout << "Base destructor" << endl;
}
class Derived : public Base
{
public:
    Derived();
    // ~Derived(); //不是虚函数
    virtual ~Derived(); //是虚函数,基类虚函数要加virtual,派生类的函数不加virtual也行
private:
    int *p;
};

Derived::Derived()
{
    p = new int(0);
}
Derived::~Derived()
{
    cout << "Derived destructor" << endl;
    delete p;
}
void fun(Base *b)
{
    delete b; //静态绑定,只会调用~Base()
}
int main()
{
    Base *b = new Derived();
    fun(b);
    return 0;
}

运行结果:

Derived destructor
Base destructor

解释:

将基类析构析构函数声明为虚函数,在函数fun()中会执行派生类的析构函数,派生类中动态申请的内存空间被释放了。实现了多态。

2.2 虚析构函数的说明

  1. 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。
  2. 派生类中如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的,但是该函数与虚函数是相互独立的,派生类中的函数并没有覆盖掉基类的版本

2.3 override说明符

override与final都不是语言关键字(keyword),只有在特定的位置才有特别含意,其他地方仍旧可以作为一般标识符(identifier)使用。

  1. 用于声明显式函数覆盖。用于在编译期间发现未覆盖的错误
  2. 运用显式覆盖,编译器会检查派生类中声明overrid的函数,在基类中是否存在可被覆盖的虚函数,若不存在,则会报错。
  3. 作用:
    为了使派生类能覆盖基类的虚函数,但是容易弄错了参数列表,导致无法完成目标。
    想通过调试发现此类错误十分困难,故使用override(C++11),在编译时编译器就会发现此类错误

例:

class Base
{
public:
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
class Derived1 : public Base
{
public:
    void f1(int) const override; //正确,f1与基类中的f1匹配
    void f2(int) override;       //错误,基类中没有形如f2(int)的函数
    void f3() override;          //错误,f3不是虚函数
    void f4() override;          //错误,基类中没有名为f4的函数
};

2.4 final说明符

  1. 用来避免类被继承,或是避免基类的函数被覆盖
class Base1 final
{
};
class Derived1 : Base1// 编译错误:Base1为final,不允许被继承
{
}; 
class Base2
{
    virtual void f() final;
};
class Derived2 : Base2
{
    void f(); // 编译错误:Base2::f 为final,不允许被覆盖
};

3、纯虚函数,抽象类

3.1 纯虚函数(了解概念即可)

纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本

纯虚函数的声明格式为:

virtual 函数类型 函数名(参数表) = 0;

3.2 抽象类(不k)

带有纯虚函数的类称为抽象类

class 类名
{
    virtual 函数类型 函数名(参数表) = 0;
    //其他成员……
};

作用:

  1. 抽象类为抽象设计的目的而声明
  2. 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为
  3. 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现

注意:

  1. 抽象类只能作为基类来使用
  2. 不能定义抽象类的对象
  3. 可以定义抽象类的指针或者引用(从而运用虚函数多态)

3.3 例8-6

/*
8-6 P323
抽象类举例
*/
#include <iostream>
using namespace std;

class Base1 //基类Base1定义
{
public:
    virtual void display() const = 0; //纯虚函数
};

class Base2 : public Base1 //公有派生类Base2定义
{
public:
    void display() const; //覆盖基类纯虚函数
};
void Base2::display() const
{
    cout << "Base2::display" << endl;
}

class Derived : public Base2 //公有派生类Derived定义
{
public:
    void display() const;
};
void Derived::display() const
{
    cout << "Derived::display" << endl;
}
void fun(Base1 *ptr)
{
    ptr->display();
}
int main()
{
    // Base1 b1;//错误,声明基类对象
    Base1 *p;   //正确,声明基类指针
    Base2 b2;   //声明派生类对象
    Derived d1; //声明派生类对象

    p = &b2;
    fun(p); //调用派生类Base2函数成员
    p = &d1;
    fun(p); //调用派生类Derived函数成员
    
    return 0;
}

运行结果:

Base2::display
Derived::display

参考:

C++语言程序设计(第5版),郑莉,清华大学

posted @ 2023-02-08 17:26  尚方咸鱼  阅读(36)  评论(0编辑  收藏  举报