面向对象编程(Visual)
- 在C++中,基类必须指出希望派生类重写哪些函数,定义为
virtual
的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。 - 在C++中,
通过基类的引用(或指针)调用虚函数时,发生动态绑定
。引用(或指针)既可以指向基类对象也可以指向派生类对象
,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。 - 保留字
virtual
的目的是启用动态绑定。成员默认为非虚函数
,对非虚函数的调用在编译时确定。 - 为了指明函数为虚函数,在其返回类型前面加上保留字virtual。
除了构造函数之外,任意非static成员函数都可以是虚函数
。 - 基类通常应将派生类需要重定义的任意函数定义为虚函数。
- 尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义
-
派生类中
虚函数的声明必须与基类中的定义方式完全匹配
, 但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
- 一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
virtual与其他成员函数
- C++中的函数调用默认不使用动态绑定,触发动态绑定条件:
- 只有指定为
虚函数的成员函数才能进行动态绑定
- 必须通过
基类类型的引用或指针进行函数调用
- 只有指定为
- 因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。
- 使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型,
编译器都将它当作基类类型对象
。 - 基类类型引用和指针的关键点在于
静态类型
(在编译时可知的引用类型或指针类型)和动态类型
(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。 - 非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定
-
覆盖虚函数机制
- 使用作用域操作符强制函数调用使用虚函数的特定版本[Code1]
- 在派生类中虚函数调用基类版本时,必须显式使用作用域操作符,如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归
-
虚函数与默认实参
- 像其他任何函数一样,虚函数也可以有默认实参,如果有用在给定调用中的默认实参值,该值将在
编译时确定
。 - 如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的
类型定义,与对象的动态类型无关
。 通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值
。- 在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。
- 像其他任何函数一样,虚函数也可以有默认实参,如果有用在给定调用中的默认实参值,该值将在
为什么会希望覆盖虚函数机制?
最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
例如,可以定义一个具有虚操作的Camera类层次。Camera类中的display函数可以显示所有的公共信息,派生类(如PerspectiveCamera)可能既需要显示公共信息又需要显示自己的独特信息。可以显式调用Camera版本以显示公共信息,而不是在PerspectiveCamera的display实现中复制Camera的操作。 在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。
虚析构函数
- 处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。
- 如果删除
基类指针
,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。
要保证运行适当的析构函数,基类中的析构函数必须为虚函数
。[Code2] - 如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,
无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
- 即使析构函数没有工作要做,继承层次的根类最好也应该定义一个虚析构函数。
-
在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。
构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。 - 将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
- 虽然可以在基类中将成员函数 operator= 定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。
- 每个类有自己的赋值操作符,
派生类中的赋值操作符有一个与类本身类型相同的形参
,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。 - 如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的 operator= 。但是,对派生类而言,这个操作符与赋值操作符是不同的。
构造函数和析构函数中的虚函数
-
尽量不要在构造函数和析构函数中调用虚函数
,因为构造或析构期间的对象类型对虚函数的绑定有影响。- 在
基类构造函数或析构函数中
,将派生类对象当作基类类型对象
对待。- 构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。
- 撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。 在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。
- 为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
- 在
- 如果在构造函数或析构函数中调用虚函数,则运行的是为
构造函数或析构函数自身类型定义的版本
。
虚函数与作用域
- 要获得动态绑定,必须通过基类的引用或指针调用虚成员。当我们这样做时,编译器器将在基类中查找函数。
假定找到了名字,编译器就检查实参是否与形参匹配。
-
虚函数必须在基类和派生类中拥有同一原型
。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。[Code3] - 通过基类调用被屏蔽的虚函数。[Code4]
-
名字查找与继承
- 首先确定进行函数调用的对象、引用或指针的静态类型。
- 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。 如果不能在类或其相关基类中找到该名字,则调用是错误的。
- 一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
- 假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
纯虚函数
- 含有(或继承)
一个或多个纯虚函数
的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象
。 - 纯虚函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。重要的是,用户将不能创建对象象。
- 纯虚函数的定义:
在函数形参表后面写上 = 0
,double net_price(std::size_t) const = 0;
虚继承
- 虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个
共享的基类子对象
。共享的基类子对象称为虚基类。[Code5] - 指定虚派生只影响从指定了虚基类的类派生的类。除了影响派生类自己的对象之外,它也是关于派生类与自己的未来派生类的关系的一个陈述。
- 任何可被指定为基类的类也可以被指定为虚基类,虚基类可以包含通常由非虚基类支持的任意类元素。虚继承支持到基类到常规转换。
- 使用虚基类的多重继承层次比没有虚继承的
引起更少的二义性问题
- 虚基类成员的可见性,假定通过多个派生路径继承名为 X 的成员
- 如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。
- 如果在某个路径中 X 是虚基类的成员,而在另一路径中 X 是后代派生类的成员,也没有二义性——特定派生类实例的优先级高于共享虚基类实例。
- 如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的。
Code
Code1:覆盖虚函数机制
Item_base *baseP = &derived; //calls version from the base class regardless of the dynamic type of baseP double d = baseP->Item_base::net_price(42);
Code2:虚析构函数
/*如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:*/ Item_base *itemP = new Item_base; // same static and dynamic type delete itemP; // ok: destructor for Item_base called itemP = new Bulk_item; // ok: static and dynamic types differ delete itemP; // ok: destructor for Bulk_item called
Code3:虚函数必须在基类和派生类中拥有同一原型
class Base { public: virtual int fcn(); }; class D1 : public Base { public: // hides fcn in the base; this fcn is not virtual int fcn(int); // parameter list differs from fcn in Base // D1 inherits definition of Base::fcn() }; class D2 : public D1 { public: int fcn(int); // nonvirtual function hides D1::fcn(int) int fcn(); // redefines virtual fcn from Base }; /*D1 中的 fcn 版本没有重定义 Base 的虚函数 fcn,相反,它屏蔽了基类的 fcn。结果 D1 有两个名 为 fcn 的函数:类从 Base 继承了一个名为 fcn 的虚 函数,类又定义了自己的名为 fcn 的非虚成员函 数,该函数接受一个 int 形参。 但是,从 Base 继承的虚函数不能通过 D1 对象(或 D1 的引用或指针) 调用, 因为该函数被 fcn(int) 的定义屏蔽了。 类 D2 重定义了它继承的两个函数,它重定义了 Base 中定义的 fcn 的原始版本并重定义了 D1 中定义 的非虚版本。*/
Code4:通过基类调用被屏蔽的虚函数
Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); bp2->fcn(); bp3->fcn(); // ok: virtual call, will call Base::fcnat run time // ok: virtual call, will call Base::fcnat run time // ok: virtual call, will call D2::fcnat run time 三个指针都是基类类型的指针,因此通过在 Base 中查找 fcn 来确定这三 个调用,所以这些调用是合法 的。另外,因为 fcn 是虚函数,所以编译器会生成代码,在运行时基于引用指针所绑定的对象的实际类型进 行调用。在 bp2 的情况,基本对象是 D1 类的,D1 类没有重定义不接受实参的虚函数版本,通过 bp2 的 函数调用(在运行时)调用 Base 中定义的版本。
Code5:虚继承
/*每个 IO 库类都继承了一个共同的抽象基类,那个抽象基类管理流的条件状态并保存流所读写的缓冲区。 istream 和 ostream 类直接继承这个公共基类,库定义了另一个名为 iostream 的类,它同时继承 istream 和 ostream,iostream 类既可以对流进行读又可以对流进行写。 我们知道,多重继承的类从它的每个父类继承状态和动作,如果 IO 类 型使用常规继承,则每个 iostream 对象可能包含两个 ios 子对象:一个包含 在它的 istream 子对象中,另一个包含在它的 ostream 子对 象中,从设计角度讲,这个实现正是错误的:iostream 类想要对单个缓冲区进行读和写,它希望跨越输入和输 出操作符共享条件状态。如果有两个单独的 ios 对象,这种共享是不可能的。 在 C++ 中,通过使用虚继承解决这类问题。*/ class istream : public virtual ios { ... }; class ostream : virtual public ios { ... }; // iostream inherits only one copy of its ios base class class iostream: public istream, public ostream { ... };
From: