C++ - 虚函数、纯虚函数与抽象类

参考网址:

  1. WIKI-虚函数
  2. C++ 虚函数和纯虚函数的区别
  3. 《Essential C++》

目录

  • 一、前言
  • 二、虚函数
    • 2.1 概述
    • 2.2 引入目的(解决问题)
    • 2.3 示例
  • 三、纯虚函数
    • 3.1 概述
    • 3.2 C++ 示例:
    • 3.3 引入目的(解决问题)
  • 四、抽象类
    • 4.1 抽象类的定义
    • 4.2 抽象类的作用
    • 4.3 注意事项
    • 4.4 设计抽象基类
  • 五、数列(num_sequence)抽象基类与其派生类(Fibonacci、Pell)代码示例
    • num_sequence.h
    • num_sequence.cpp
    • Fibonacci.h
    • Fibonacci.cpp
    • Pell.h
    • Pell.cpp
    • main.cpp
    • 输出结果

一、前言

  1. 定义一个函数为虚函数,不代表函数为不被实现的函数。
  2. 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  3. 定义一个函数为纯虚函数,才代表函数没有被实现。
  4. 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

二、虚函数

2.1 概述

在面向对象程序设计领域,C++、Object Pascal 等语言中有虚函数(英语:virtual function)或虚方法(英语:virtual method)的概念。这种函数或方法可以被子类继承和覆盖,通常使用动态分派实现。这一概念是面向对象程序设计中(运行时)多态的重要组成部分。简言之,虚函数可以给出目标函数的定义,但该目标的具体指向在编译期可能无法确定。

虚函数在设计模式方面扮演重要角色。例如,《设计模式》一书中提到的23种设计模式中,仅5个对象创建模式就有4个用到了虚函数(抽象工厂、工厂方法、生成器、原型),只有单例没有用到。

2.2 引入目的(解决问题)

在面向对象程序设计中,派生类继承自基类。使用指针或引用访问派生类对象时,指针或引用本身所指向的类型是基类而不是派生类。如果派生类覆盖了基类中的方法,通过上述指针或引用调用该方法时,可以有两种结果:

  1. 调用到基类的方法:编译器根据指针或引用的类型决定,称作“早绑定”;
  2. 调用到派生类的方法:语言的运行时系统根据对象的实际类型决定,称作“迟绑定”。

虚函数的效果属于后者。如果问题中基类的函数是“虚”的,则调用到的都是最终派生类(英语:most-derived class)中的函数实现,与指针或引用的类型无关。反之,如果函数非“虚”,调用到的函数就在编译期根据指针或者引用所指向的类型决定。
有了虚函数,程序甚至能够调用编译期还不存在的函数。
在 C++ 中,在基类的成员函数声明前加上关键字 virtual 即可让该函数成为虚函数,派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本。

2.3 示例

#include 
#include 

using namespace std;

class Animal
{
public:
    virtual void eat() const { cout << "I eat like a generic Animal" << endl; }
    // virtual ~Animal();
};

class Fish : public Animal
{
public:
    void eat() const { cout << "I eat like a fish!" << endl; }
};

class GoldFish : public Fish
{
public:
    void eat() const { cout << "I eat like a goldfish!" << endl; }
};

class OtherAnimal : public Animal
{
};

int main()
{
    std::vector<Animal*> animals;
    animals.push_back(new Animal());
    animals.push_back(new Fish());
    animals.push_back(new GoldFish());
    animals.push_back(new OtherAnimal());

    for (std::vector<Animal*>::const_iterator it = animals.begin(); it != animals.end(); ++it)
    {
        (*it)->eat();
        delete* it;
    }
    return 0;
}

上述程序输出结果为:

I eat like a generic Animal
I eat like a fish!
I eat like a goldfish!
I eat like a generic Animal

若将Animal基类中的virtual字段删掉,则输出结果为:

I eat like a generic Animal
I eat like a generic Animal
I eat like a generic Animal
I eat like a generic Animal

三、纯虚函数

3.1 概述

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,需要被非抽象的派生类覆盖(override)。包含纯虚方法的类被称作抽象类; 抽象类不能被直接实例化。
一个抽象基类的一个子类只有在所有的纯虚函数在该类(或其父类)内给出实现时, 才能直接实例化. 纯虚方法通常只有声明(签名)而没有定义(实现),但有特例情形要求纯虚函数必须给出函数体定义。
虽然纯虚方法通常在定义它的类中没有实现, 在 C++ 语言中, 允许纯虚函数在定义它的类中包含其实现, 这为派生类提供了备用或默认的行为。C++的虚基类的虚析构函数必须提供函数体定义,否则链接时(linking)在析构该抽象类的派生实例对象的语句处会报错。

3.2 C++ 示例:

在C++语言中, 纯虚函数用一种特别的语法[=0]定义(但 VS 也支持 abstract 关键字:virtual ReturnType Function()abstract;)

class Abstract
{
public:
    virtual void pure_virtual() = 0;
};

纯虚函数的定义仅提供方法的原型. 虽然在抽象类中通常不提供纯虚函数的实现, 但是抽象类中可以包含其实现, 而且可以不在声明的同时给出定义。每个非抽象子类仍然需要重载该方法。

void Abstract::pure_virtual() {
    cout << "function: pure_virtual" << endl;
}

class Child : public Abstract {
    virtual void pure_virtual(); //该类可能会被实例化
};

void Child::pure_virtual() {
    Abstract::pure_virtual();
}

3.3 引入目的(解决问题)

  1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。编译器要求在派生类中必须予以重写以实现多态性。
  2. 在很多情况下,基类本身生成对象是不合情理的。基类往往指代着更具有“抽象性/理性”的理念(范畴)。eg:动物作为一个基类可以派生出老虎、孔雀等子类(实体)来做出“进食”动作,但是动物(理念)本身是无法做出“进食”动作的。因此含有纯虚拟函数的类称为抽象类,它不能生成对象。

纯虚函数的意义:
让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

四、抽象类

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

4.1 抽象类的定义

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

4.2 抽象类的作用

抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

4.3 注意事项

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

根据一般规则,凡基类定义有一个(或多个)虚函数,应该要将其destructor声明为virtual,如:

class num_sequence
{
public:
    virtual ~num_sequence(){};
};

4.4 设计抽象基类

  1. 找出所有子类共通的操作行为。
  2. 设法找出哪些操作行为与类型相关(type-dependent)。
  3. 找出每个操作行为的访问层级(access level)。
    1)某个操作行为应该让一般程序均能访问,我们应该将它声明为public
    2)某个操作行为在基类之外不需要被用到,我们就将它声明为private
    3)该操作行为可让派生类访问,却不允许一般程序使用,我们就将它声明为protected

五、数列(num_sequence)抽象基类与其派生类(Fibonacci、Pell)代码示例

num_sequence.h

#ifndef _TEST_H_
#define _TEST_H_

#include 
#include 

class num_sequence
{
public:

    friend std::ostream& operator<<(std::ostream &os, const num_sequence &ns);

    virtual ~num_sequence(){};

    // 将虚函数赋值为0,意思便是令它为一个纯虚函数。

    // 使用_pmf所指函数,产生对应pos的元素。
    virtual int elem(int pos) const = 0;
    // 返回确切的数列类型。
    virtual const char *what_am_i() const = 0;
    // 返回所支持的最大位置值
    static int max_elems() { return _max_elems; }
    // 将所有元素写入os。
    virtual std::ostream &print(std::ostream &os = std::cout) const = 0;

protected:
    // 产生直到pos位置的所有元素。
    virtual void gen_elems(int pos) const = 0;
    // 检查pos是否为有效位置。
    // bool check_integrity(int pos) const;
    // 新的check_integrity函数,以增加其泛用性
    bool check_integrity(int pos, int size) const;
    const static int _max_elems = 1024;
};

#endif

num_sequence.cpp

#include "num_sequence.h"
#include 

bool num_sequence::check_integrity(int pos, int size) const
{
    if (pos <= 0 || pos > _max_elems)
    {
        std::cerr << "!! invalid position: " << pos
                  << " Cannot honor request\n";
        return false;
    }

    // 将判断放在基类函数中
    if (pos > size)
        // gen_elems()通过虚拟机制调用
        gen_elems(pos);

    return true;
}

std::ostream &operator<<(std::ostream &os, const num_sequence &ns)
{
    return ns.print(os);
}

Fibonacci.h

#include "num_sequence.h"
#include 
#include 

class Fibonacci : public num_sequence
{
public:
    Fibonacci(int len = 1, int beg_pos = 1)
        : _length(len), _beg_pos(beg_pos) {}

    virtual int elem(int pos) const;
    virtual const char *what_am_i() const { return "Fibonacci"; }
    virtual std::ostream &print(std::ostream &os = std::cout) const;
    int length() const { return _length; }
    int beg_pos() const { return _beg_pos; }

protected:
    // 当派生类有某个member与其基类的member同名,便会遮掩住基类的那份member。
    // bool check_integrity(int pos) const;
    virtual void gen_elems(int pos) const;
    int _length;
    int _beg_pos;
    static std::vector<int> _elems;
};

Fibonacci.cpp

#include "Fibonacci.h"
#include 
#include 

std::vector<int> Fibonacci::_elems;

int Fibonacci::elem(int pos) const
{
    // 现在调用的是Fibonacci的check_integrity()。
    if (!check_integrity(pos, _elems.size()))
        return 0;
    if (pos > _elems.size())
        // 我们希望跳过虚函数机制,使该函数在编译时就完成解析,不必等到运行时才解析。
        Fibonacci::gen_elems(pos);

    return _elems[pos - 1];
}

void Fibonacci::gen_elems(int pos) const
{
    if (_elems.empty())
    {
        _elems.push_back(1);
        _elems.push_back(1);
    }

    if (_elems.size() <= pos)
    {
        int ix = _elems.size();
        int n_2 = _elems[ix-2];
        int n_1 = _elems[ix-1];

        for (; ix <= pos; ++ix)
        {
            int elem = n_2 + n_1;
            _elems.push_back(elem);
            n_2 = n_1;
            n_1 = elem;
        }
    }
}

std::ostream &Fibonacci::
    print(std::ostream &os) const
{
    int elem_pos = _beg_pos - 1;
    int end_pos = elem_pos + _length;

    if (end_pos > _elems.size())
        Fibonacci::gen_elems(end_pos);

    while (elem_pos < end_pos)
        os << _elems[elem_pos++] << " ";
    return os;
}

// 旧的解决方式:
// 由于在基类中check_integrity并未视为虚函数,
// 因此每次通过基类的pointer和reference来调用check_integrity(),
// 解析出来的都是num_sequence那一份,
// 未考虑到pointer或reference实际指向的究竟是什么对象。
/*
inline bool Fibonacci::
    check_integrity(int pos) const
{
    if (!num_sequence::check_integrity(pos))
        return false;

    if (pos > _elems.size())
        Fibonacci::gen_elems(pos);
    return true;
}
*/

Pell.h

#include "num_sequence.h"
#include 
#include 

class Pell : public num_sequence
{
public:
    Pell(int len = 1, int beg_pos = 1)
        : _length(len), _beg_pos(beg_pos) {}

    virtual int elem(int pos) const;
    virtual const char *what_am_i() const { return "Pell"; }
    virtual std::ostream &print(std::ostream &os = std::cout) const;
    int length() const { return _length; }
    int beg_pos() const { return _beg_pos; }

protected:
    virtual void gen_elems(int pos) const;
    int _length;
    int _beg_pos;
    static std::vector<int> _elems;
};

Pell.cpp

#include "Pell.h"
#include 
#include 

std::vector<int> Pell::_elems;

int Pell::elem(int pos) const
{
    if (!check_integrity(pos, _elems.size()))
        return 0;
    if (pos > _elems.size())
        Pell::gen_elems(pos);

    return _elems[pos - 1];
}

void Pell::gen_elems(int pos) const
{
    if (_elems.empty())
    {
        _elems.push_back(1);
        _elems.push_back(2);
    }

    if (_elems.size() <= pos)
    {
        int ix = _elems.size();
        int n_2 = _elems[ix-2];
        int n_1 = _elems[ix-1];

        for (; ix <= pos; ++ix)
        {
            int elem = n_2 + 2 * n_1;
            _elems.push_back(elem);
            n_2 = n_1;
            n_1 = elem;
        }
    }
}

std::ostream &Pell::
    print(std::ostream &os) const
{
    int elem_pos = _beg_pos - 1;
    int end_pos = elem_pos + _length;

    if (end_pos > _elems.size())
        Pell::gen_elems(end_pos);

    while (elem_pos < end_pos)
        os << _elems[elem_pos++] << " ";
    return os;
}

main.cpp

#include 
#include "Fibonacci.h"
#include "Pell.h"
#include 

inline void display(std::ostream &os, const num_sequence &ns, int pos)
{
    os << "The element at position "
       << pos << " for the "
       << ns.what_am_i() << " sequence is "
       << ns.elem(pos) << std::endl;
}

int main()
{
    num_sequence *ps = new Fibonacci(8, 12);
    std::cout << "fib: beginning at element 12 for 8 elements: "
              << *ps << std::endl;

    const int pos = 8;

    Fibonacci fib;
    display(std::cout, fib, pos);
    Pell pell;
    display(std::cout, pell, pos);

    return 0;
}

输出结果

fib: beginning at element 12 for 8 elements: 144 233 377 610 987 1597 2584 4181 
The element at position 8 for the Fibonacci sequence is 21
The element at position 8 for the Pell sequence is 408

你可能感兴趣的:(C/C++,c++,开发语言,算法)