一、虚函数
虚函数的标志是“virtual”关键字。即:
|
virtual 类型 函数声明 { }; |
注:等于、不等于操作符和size()成员函数的实现对于其应用的数组类型来说是独立的。因此不把它声明成Virtual
虚函数(virtual function) 是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
class A
{
public:
virtual void foo() { cout << "A::foo() iscalled" << endl;}
};
class B: public A
{
public:
virtual void foo() { cout << "B::foo() iscalled" << endl;}
};
//那么,在使用的时候,我们可以:
int main()
{
A* a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
}
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为“虚”函数。
在C++笔记(一)——C++的基本介绍及构成中关于类的定义的例子中,想查询每个子类的薪资,需要通过
| aWager.CWage::computePay();//对象aWage调用CWage类中的computePay()成员函数 |
这种继承的方法来实现。对于子类较多的函数,以及未来可能会出现的不确定的类,这种方法不太方便。如果能通过一个指针来表示所以可能的职位的薪资,是不是会方便很多呢。正常情况下,我们可以用一个“基类的指针”指向“派生类的对象”,但经由该指针只能够调用基类所定义的函数。如果能让该“基类的指针”调用所以的派生类的对象是不是就简单了呢?
这里就用到了虚函数的概念。如下图,如果我们可以用一个“基类的指针”代表每一个职员,并且在它“实际指向不同种类的职员”时,调用相应子类的computerPay函数或其他函数。这种性质就是多态,靠虚函数来完成。
代码如下:
#include <iostream>
using namespace std;
/*****建立一个员工薪资体系,包括职位、名字、薪资记录方法*****/
class CEmployee //职员
{
public:
CEmployee();
CEmployee(const char* nm)
{
strcpy(m_name, nm);
cout << "名字:" << m_name << endl;
}
virtual float computePay()//若要调用后面的该函数,则需要在父类定义并实例化才可以
{
return 0;
};
private:
char m_name[30];
};
//————————————————————————————————————
class CWage : public CEmployee //小时工;需要知道他的名字、工时m_hours,时薪m_wage
{
public:
CWage(const char* nm) : CEmployee(nm)
{
m_wage = 250.0;//时薪
m_hours = 40.0;//工时
cout << "wage: " << m_wage << endl << "hours: " << m_hours << endl;
}
void setWage(float wg) //成员函数
{
m_wage = wg;
}
void setHours(float hrs)
{
m_hours = hrs;
}
virtual float computePay()
{
float sale = m_hours * m_wage;
cout << "BasicMoney: " << sale << endl;
return sale;
}//计薪
private:
float m_wage; //时薪
float m_hours; //工作时间
};
class CSales : public CWage //销售员:按时计薪,姓名。时薪、时间、销售数量、单件提成额
{
public:
CSales(const char *nm) : CWage(nm)
{
m_comm = 200.0;
m_sale = 5.0;
cout << "comm: " << m_comm << endl << "sale: " << m_sale << endl;
}
void setCommission(float comm) { m_comm = comm; }
void setSale(float sale) { m_sale = sale; }
virtual float computePay();//计薪
private:
float m_comm; //单件提成额
float m_sale; //销售数量
};
float CSales::computePay()
{
float Sales_Money = CWage::computePay() + m_comm * m_sale;
cout << "SalesMoney: " << Sales_Money << endl;
return Sales_Money;
}
class CManager : public CEmployee
{
public:
CManager(const char* nm) : CEmployee(nm)
{
m_salary = 15000.0;
}
void setSalary(float salary) { m_salary = salary; }
virtual float computePay()
{
cout << "ManagerMoney: " << m_salary << endl;
return m_salary;
};//计薪
private:
float m_salary;
};
int main()
{
CEmployee* pEmp;
CWage aWager("BoBO");
pEmp = &aWager;
pEmp->computePay(); //调用的是CWage::computePay
//aWager.CWage::computePay();//aWage调用CWage类中的computePay()成员函数
CSales aSales("小美");
pEmp = &aSales;
pEmp->computePay();
CManager aManager("陈美静");
pEmp = &aManager;
pEmp->computePay();
getchar();
return 0;
}
在程序中我们发现,调用3个派生类中的computePay()函数时,用到代码一样(如下),但结果却不同,这就是虚函数的妙用了。
| pEmp = &aManager; pEmp->computePay(); |
如果不用虚函数,我们还可以使用作用域(::)运算符来指出函数的调用,但程序会比较麻烦,弹性差。
注: 虚函数只能借助于指针或者引用来达到多态的效果。
如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar()
{
A a;
a.foo(); // A::foo()被调用
}
关于多态的介绍见:C++笔记(十一)——类4:多态
二 、函数的overload和override
虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:
override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。
overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数既可以接受整型数作为参数,也可以接受浮点数作为参数。
具体见下一章:C++笔记(十)——类3:成员函数的重载、覆盖、隐藏
三、虚函数的语法
3.1 使用virtual关键字,考虑下面的类层次:
class A
{
public:
virtual void foo();
};
class B: public A
{
public:
void foo(); // 没有virtual关键字!
};
class C: public B // 从B继承,不是从A继承!
{
public:
void foo(); // 也没有virtual关键字!
};
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,
-
注:基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。
3.2 纯虚函数
纯虚函数不需要定义其实际操作,它的存在只是为了在派生类中被重新定义,只是为了提供一个多态接口,只要是拥有纯虚函数的类,就是一种抽象类,它是不能够被实例化(instantiate)的,也就是说,你不能根据它产生一个对象(你怎能说一种形状为“Shape”的物体呢)。
如下声明表示一个函数为纯虚函数:
classA
{
public:
virtual void foo()=0; // =0标志一个虚函数为纯虚函数
};
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。
为什么会出现纯虚函数呢?
如果按C++笔记(十一)——类4:多态中学生收学费的例子进行编程,所有类型都继承小学生类,我们会发现一此小学生自己特定的东东[比如 void 上美术课();],也都被大学生继承来了,虽然不影响大学生的操作,但是随时间的加长,小学生类中自已所特定的东东越来越多,这样下去,大学生中冗余的数据就多了,有什么办法可以解决????
就是定义基类时定义一个抽象类,如学生类,在学生类中实现一此大家都有的操作。这个过程就叫分解。这个例子对纯虚函数的说明还不够明显,换个例子比如:
class 人()
{
public :
//......
void 吃()
{
人吃饭;
}
//......
char *Name;
int age;
};
class 狗()
{
public :
//......
void 吃()
{
狗吃屎;
}
//......
char *Name;
int age;
};
人类、狗类有一些相同的东东,如名字,年纪,吃的动作等,有人想到了为了代码的重用,让人类继承狗类,可是数据的冗余让这个想法完蛋了,所以有人又想出可不可以定义一个这样的类:
这个类界于人类狗类之间,有人类和狗类共有的一些东东,比如年纪,名字,体重什么的,但它是不存在实例的,它的存在意义也是只是为了派生其它类,如人类、狗类,这样可以使系统清淅、。。、反正好处多多。
在这个世界上根本不存在界于人狗之间的东东,所以这个“人狗之间类”中的“吃”也是没什么意义,你也很难为它的内容下个定义,况且也没必要,即:抽象类的实例没意义。所以定义它为纯虚函数,形式为:virtual void 吃()=0; 用来告诉系统:
- 1、这个函数可以没有函数定义;
- 2、拥有本函数的类是抽象类;
你可能会说,即然纯虚函数没什么意义,为什么还要它,它的作用是什么呢?
为实现多态作准备!!!
由于抽象类的实例没意义,所以C++中不允许定义它的实例。(如果定义了这样的实例A,那么你调用A.吃()怎么办?)当然了,你也可以在基类中,virtual 吃(){};这样一来,基类就不是抽象类了,可以有实例,而且对于其它方面都不影响。
但你也要承认这样的对象是没有什么意识的,它的存在只能使你思考上增加负担,除错时还要考虑到是不是有这样类的对象在作怪,所以C++干脆提供了“虚函数”、抽象类,的机制,给我们操作时加了限制也可以认为是保险[不可以有抽象类的对象],试想当代码变得非常之庞大时,它的存在是多么有必要啊!!!
有个关于描述形状的Shpe例子,我们说CShape是抽象的,所以它根本不该有display这个操作。但为了在各具体派生类中绘图,我们又不得不在基类CShape中加上display虚函数,你可以定义它什么也不做(空函数):
class CShape
{
public:
virtual void display();
};
或只是给个消息:
class CShape
{
public:
virtual void display()
{
cout << "Shape \n";
}
};
这两种做法都不高明,因为这个函数根本就不应该调用(CShape是抽象的),我们根本就 不应该定义它。不定义但又必须保留一块空间(spaceholder)给它,于是C++提供了所谓的纯虚函数:
class CShape
{
public:
virtual void display() = 0; //注意 “=0”
};
3.3虚析构函数
析构函数也可以是虚的,甚至是纯虚的。例如:
classA
{
public:
virtual ~A()=0; // 纯虚析构函数
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[]ptra_;} // 非虚析构函数
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A * a = new B;
delete a;
}
在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?
如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。
纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
注:构造函数不能是虚的。
四、虚函数使用技巧
3.1 private的虚函数
考虑下面的例子:
class A
{
public:
void foo() { bar();}
private:
virtual void bar() { ...}
};
class B: public A
{
private:
virtual void bar() { ...}
};
在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。
3.2 构造函数和析构函数中的虚函数调用
一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:
class A
{
public:
A() { foo();} // 在这里,无论如何都是A::foo()被调用!
~A() { foo();} // 同上
virtual void foo();
};
class B: public A
{
public:
virtual void foo();
};
void bar()
{
A * a = new B;
delete a;
}
如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。
3.3 什么时候使用虚函数
在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。
另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。
现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。
四、虚函数的总结
- 若期望派生类重新定义一个成员函数,那么你应该在基类中把此函数设为virtual。
- 多态:以单一指令调用不同函数
- 虚拟函数是C++语言的Polymorphism性质以及动态绑定的关键
- 既然抽象类中的虚函数不打算被调用,我们就不应该定义它,应该把它设为纯虚函数(在函数声明之后加上“=0”即可)
- 为了与具体类做区别,我们可以把拥有纯虚函数的类叫做抽象类
- 抽象类不能产生出对象实例,但我们可以拥有指向抽象类的指针,以便于操作抽象类的各个派生类
- 虚函数派生下去仍为虚函数,且派生的类里可以省略virtual关键词