参考网址:
- WIKI-虚函数
- C++ 虚函数和纯虚函数的区别
- 《Essential C++》
在面向对象程序设计领域,C++、Object Pascal 等语言中有虚函数(英语:virtual function)或虚方法(英语:virtual method)的概念。这种函数或方法可以被子类继承和覆盖,通常使用动态分派实现。这一概念是面向对象程序设计中(运行时)多态的重要组成部分。简言之,虚函数可以给出目标函数的定义,但该目标的具体指向在编译期可能无法确定。
虚函数在设计模式方面扮演重要角色。例如,《设计模式》一书中提到的23种设计模式中,仅5个对象创建模式就有4个用到了虚函数(抽象工厂、工厂方法、生成器、原型),只有单例没有用到。
在面向对象程序设计中,派生类继承自基类。使用指针或引用访问派生类对象时,指针或引用本身所指向的类型是基类而不是派生类。如果派生类覆盖了基类中的方法,通过上述指针或引用调用该方法时,可以有两种结果:
- 调用到基类的方法:编译器根据指针或引用的类型决定,称作“早绑定”;
- 调用到派生类的方法:语言的运行时系统根据对象的实际类型决定,称作“迟绑定”。
虚函数的效果属于后者。如果问题中基类的函数是“虚”的,则调用到的都是最终派生类(英语:most-derived class)中的函数实现,与指针或引用的类型无关。反之,如果函数非“虚”,调用到的函数就在编译期根据指针或者引用所指向的类型决定。
有了虚函数,程序甚至能够调用编译期还不存在的函数。
在 C++ 中,在基类的成员函数声明前加上关键字 virtual 即可让该函数成为虚函数,派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本。
#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
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,需要被非抽象的派生类覆盖(override)。包含纯虚方法的类被称作抽象类; 抽象类不能被直接实例化。
一个抽象基类的一个子类只有在所有的纯虚函数在该类(或其父类)内给出实现时, 才能直接实例化. 纯虚方法通常只有声明(签名)而没有定义(实现),但有特例情形要求纯虚函数必须给出函数体定义。
虽然纯虚方法通常在定义它的类中没有实现, 在 C++ 语言中, 允许纯虚函数在定义它的类中包含其实现, 这为派生类提供了备用或默认的行为。C++的虚基类的虚析构函数必须提供函数体定义,否则链接时(linking)在析构该抽象类的派生实例对象的语句处会报错。
在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();
}
纯虚函数的意义:
让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
带有纯虚函数的类为抽象类。
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
根据一般规则,凡基类定义有一个(或多个)虚函数,应该要将其destructor声明为virtual,如:
class num_sequence
{
public:
virtual ~num_sequence(){};
};
#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
#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);
}
#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;
};
#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;
}
*/
#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;
};
#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;
}
#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