虚函数和动态绑定 C++学习
1.什么是虚成员函数
即其声明在返回类型的前面带有关键字virtual的类成员函数。定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
2.动态绑定
通过动态绑定,我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。
用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
3.基类成员函数
成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型的前面加上virtual。除构造函数之外,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数中声明,不能用在类定义体外部出现的函数定义上。
4.派生类与虚函数
尽管不是必须这样做,派生类一般会重新定义所继承的虚函数。如果派生类没有重新定义虚函数,则使用基类中定义的版本。派生类型必须对想要重新定义的每个继承成员进行声明。派生类中虚函数的声明必须与基类中的定义方式一样,但有一个例外:返回基类型的引用(或指针)的虚函数。派生类的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
注意:一旦函数在基类声明中为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
5.virtual与其他成员函数
要触发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。
1)从派生类到基类的转换
因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以将指向基类的指针指向派生类对象。
使用基类类型的指针和引用时,不知道指针和引用所绑定的对象的类型:基类类型的引用或指针既可以引用基类类型对象,又可以引用派生类型对象。无论实际对象是哪一种,编译器都将它当作基类类型对象。(将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即任何作用于基类对象上执行的操作也可以通过派生类对象使用)。
注:基类类型引用和指针的关键点在于静态类型和动态类型可能不同。静态类型,在编译时可知的引用类型或指针类型;动态类型,指针或引用类型绑定的对象的类型,这仅仅是运行时可知。
2)可以在运行时确定virtual函数的调用
c++动态绑定的关键:对象的实际类型(动态类型)可能不同于该对象的引用或指针的静态类型。
3)在编译时确定非virtual函数的调用
非虚函数总是在编译时根据调用该函数的对象、引用、指针的类型而确定。
4)覆盖虚函数机制
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这是可以使用作用域操作符。
如Item_base *baseP = &derived; baseP->Item_base::net_price(42);//该调用编译时确定(如果没有作用域则在运行时调用baseP所指向对象相对应的函数)
使用覆盖虚函数机制的理由是为了派生类虚函数调用基类中的版本。这种情况下,基类版本可以完成基础层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将会是一个自身调用,从而导致无穷递归。
例子:
基类Item_base的定义(Item_base.h)
1 #ifndef ITEM_BASE_H
2 #define ITEM_BASE_H
3 #include<string>
4 class Item_base {
5 public:
6 Item_base(const std::string &s = "", double p = 0):isbn(s), price(p) { }
7 std::string book() const { return isbn; } /*返回书的编号*/
8 virtual double net_price(std::size_t n) const /*返回给定数目的某书的总价(实价)*/
9 {
10 return n * price; //基类中的net_price无折扣
11 }
12 virtual ~Item_base() { }
13 private:
14 std::string isbn;
15 protected:
16 double price; /*price为protected,可以被派生类对象访问但不能被该类型的普通用户访问*/
17 };
18 #endif
派生类 Bulk_item(Bulk_item.h)
1 #ifndef BULK_ITEM_H
2 #define BULK_ITEM_H
3 #include "Item_base.h"
4
5 class Bulk_item:public Item_base
6 {
7 public:
8 Bulk_item(const std::string &s, double price, std::size_t mq, double dc):min_qty(mq), discount(dc), Item_base(s, price)
9 {
10 }
11 double net_price(std::size_t n) const;
12 private:
13 std::size_t min_qty; //折扣的最小购买数量
14 double discount; //折扣
15 };
16 #endif
派生类实现Bulk_item.cpp
1 #include <iostream>
2 #include "Bulk_item.h"
3 using namespace std;
4
5 double Bulk_item::net_price(size_t n) const
6 {
7 if(n >= min_qty)
8 return n * (1 - discount) * price;
9 else
10 return n * price;
11 }
12
13 void print_total(ostream &os, const Item_base &item, size_t n)
14 {
15 os << "book isbn:" << item.book()
16 << "net_price:" << item.net_price(n) << endl;
17 }
测试test.cpp
1 #include <iostream>
2 #include "Item_base.h"
3 #include "Bulk_item.h"
4 using namespace std;
5
6 int main()
7 {
8 Bulk_item bulk("123", 23, 5, 0.8);
9 Item_base &base = bulk;
10
11 cout << base.net_price(6) << endl;
12 cout << base.Item_base::net_price(6) << endl;
13 }
测试结果:27.6
128
使用基类Item_base的引用调用虚成员函数会发生动态绑定,被调用的函数是与动态类型即Bulk_item相对应的函数。
使用作用域操作符覆盖虚函数机制,即Item_base::可以调用基类的版本。