面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。
15.1. 面向对象编程:概述
面向对象编程的关键思想是多态性(polymorphism)。
之所以称通过继承而相关联的类型为多态类型,是因为在许多情况下可以互换地使用派生类型或基类型的“许多形态”。正如我们将看到的,在 C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。
继承
派生类(derived class)能够继承基类(baseclass)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性。
• 名为 book 的操作,返回 ISBN。
• 名为 net_price 的操作,返回购买指定数量的书的价格。
Item_base 的派生类将无须改变地继承 book 函数:派生类不需要重新定义获取 ISBN 的含义。另一方面,每个派生类需要定义自己的 net_price 函数版本,以实现适当的折扣价格策略。
在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
讨论过这些之后,可以看到我们的类将定义三个(const)成员函数:
• 非虚函数 std::string book(),返回 ISBN。由 Item_base 定义,Bulk_item 继承。
• 虚函数 double net_price(size_t) 的两个版本,返回给定数目的某书的总价。Item_base 类和 Bulk_item 类将定义该函数自己的版本。
动态绑定
动态绑定我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。
书店应用程序可以允许顾客在一次交易中选择几本书,当顾客购书时,应用程序可以计算总的应付款,指出最终账单的一个部分将是为每本书打印一行,以显示总数和售价。
可以定义一个名为 print_total 的函数管理应用程序的这个部分。给定一个项目和数量,函数应打印 ISBN 以及购买给定数量的某书的总价。这个函数的输出应该像这样:
ISBN: 0-201-54848-8 number sold: 3 total price: 98
ISBN: 0-201-82470-1 number sold: 5 total price: 202.5
可以这样编写 print_total 函数:
// calculate and print price for given number of copies, applying any discounts void print_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // calls Item_base::book << "\tnumber sold: " << n << "\ttotal price: " // virtual call: which version of net_price to call is resolved at run time << item.net_price(n) << endl; }
该函数的工作很普通:调用其 item 形参的 book 和 net_price 函数,打印结果。关于这个函数,有两点值得注意。
第一,虽然这个函数的第二形参是 Item_base 的引用但可以将 Item_base对象或 Bulk_item 对象传给它。
第二,因为形参是引用且 net_price 是虚函数,所以对 net_price 的调用将在运行时确定。调用哪个版本的 net_price 将依赖于传给 print_total 的实参。如果传给 print_total 的实参是一个 Bulk_item 对象,将运行 Bulk_item中定义的应用折扣的 net_price;如果实参是一个 Item_base 对象,则调用由Item_base 定义的版本。
15.2. 定义基类和派生类
15.2.1. 定义基类(编写程序)
像任意其他类一样,基类也有定义其接口和实现的数据和函数成员。在(非常简化的)书店定价应用程序的例子中,Item_base 类定义了 book 和net_price 函数并且需要存储每本书的 ISBN 和标准价格:
// Item sold at an undiscounted price // derived classes will define various discount strategies class Item_base { public: Item_base(const std::string &book = "",double sales_price = 0.0): isbn(book), price(sales_price) { } std::string book() const { return isbn; } // returns total sales price for a specified number of items // derived classes will override and apply different discount algorithms virtual double net_price(std::size_t n) const { return n * price; } virtual ~Item_base() { } private: std::string isbn; // identifier for the item protected: double price; // normal, undiscounted price };
15.2.2. protected 成员
派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限。
假定 Bulk_item 定义了一个成员函数,接受一个 Bulk_item 对象的引用和一个 Item_base 对象的引用,该函数可以访问自己对象的 protected 成员以及 Bulk_item 形参的 protected 成员,但是,它不能访问 Item_base 形参的 protected 成员。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b) { // attempt to use protected member double ret = price; // ok: uses this->price ret = d.price; // ok: uses price from a Bulk_item object ret = b.price; // error: no access to price from an Item_base }
15.2.3. 派生类
为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个基类,具有如下形式:
class classname: access-label base-class
定义派生类(编写程序)
在书店应用程序中,将从 Item_base 类派生 Bulk_item 类,因此Bulk_item 类将继承 book、isbn 和 price 成员。Bulk_item 类必须重定义net_price 函数定义该操作所需要的数据成员:
// discount kicks in when a specified number of copies of same book are sold // the discount is expressed as a fraction used to reduce the normal price class Bulk_item : public Item_base { public: // redefines base version so as to implement bulk purchase discount policy double net_price(std::size_t) const; private: std::size_t min_qty; // minimum purchase for discount to apply double discount; // fractional discount to apply };
每个 Bulk_item 对象包含四个数据成员:从 Item_base 继承的 isbn 和price,自己定义的 min_qty 和 discount,后两个成员指定最小数量以及购买
超过该数量时给的折扣。
派生类和虚函数
尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类对象包含基类对象作为子对象
派生类对象由多个部分组成:派生类本身定义的(非 static)成员加上由基类(非 static)成员组成的子对象。
派生类中的函数可以使用基类的成员
// if specified number of items are purchased, use discounted price double Bulk_item::net_price(size_t cnt) const { if (cnt >= min_qty) return cnt * (1 - discount) * price; else return cnt * price; }
用作基类的类必须是已定义的
已定义的类才可以用作基类。如果已经声明了 Item_base 类,但没有定义它,则不能用 Item_base 作基类:
class Item_base; // declared but not defined // error: Item_base must be defined class Bulk_item : public Item_base { ... };
用派生类作基类
基类本身可以是一个派生类:
class Base { /* ... */ }; class D1: public Base { /* ... */ }; class D2: public D1 { /* ... */ };
每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
派生类的声明
如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。例如,下面的前向声明会导致编译时错误
// error: a forward declaration must not include the derivation list class Bulk_item : public Item_base;
正确的前向声明为:
// forward declarations of both derived and nonderived class class Bulk_item; class Item_base;
15.2.4. virtual 与其他成员函数
C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:
第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;
第二,必须通过基类类型的引用或指针进行函数调用。
从派生类型到基类的转换
因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象:
// function with an Item_base reference parameter double print_total(const Item_base&, size_t); Item_base item; // object of base type // ok: use pointer or reference to Item_base to refer to an Item_base object print_total(item, 10); // passes reference to an Item_base object Item_base *p = &item; // p points to an Item_base object Bulk_item bulk; // object of derived type // ok: can bind a pointer or reference to Item_base to a Bulk_item object print_total(bulk, 10); // passes reference to the Item_base part of bulk p = &bulk;
可以在运行时确定 virtual 函数的调用
通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。
// calculate and print price for given number of copies, applying any discounts void print_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // calls Item_base::book << "\tnumber sold: " << n << "\ttotal price: " // virtual call: which version of net_price to call is resolved at run time << item.net_price(n) << endl; }
因为 item 形参是一个引用且 net_price 是虚函数,item.net_price(n) 所调用的 net_price 版本取决于在运行时绑定到 item 形参的实参类型:
Item_base base; Bulk_item derived; // print_total makes a virtual call to net_price print_total(cout, base, 10); // calls Item_base::net_price print_total(cout, derived, 10); // calls Bulk_item::net_price
在编译时确定非 virtual 调用
不管传给 print_total 的实参的实际类型是什么,对 book 的调用在编译时确定为调用 Item_base::book。
非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。item 的类型是 const Item_base 的引用,所以,无论在运行时 item 引用的实际对象是什么类型,调用该对象的非虚函数都将会调用 Item_base 中定义的版本。
覆盖虚函数机制
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:
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);
虚函数与默认实参
像其他任何函数一样,虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。
15.2.5. 公用、私有和受保护的继承
派生类中定义的成员访问控制的处理与任意其他类中完全一样(第 12.1.2节)。派生类可以定义零个或多个访问标号,指定跟随其后的成员的访问级别。
基类本身指定对自身成员的最小访问控制。如果成员在基类中为 private,则只有基类和基类的友元可以访问该成员。派生类不能访问基类的 private 成员,也不能使自己的用户能够访问那些成员。如果基类成员为 public 或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:
• 如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected成员。
• 如果是受保护继承,基类的 public 和 protected 成员在派生类中为protected 成员。
• 如果是私有继承,基类的的所有成员在派生类中为 private 成员。
接口继承与实现继承
public 派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,public 派生类的对象可以用在任何需要基类对象的地方。
使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实现中使用被继承但继承基类的部分并未成为其接口的一部分。
迄今为止,最常见的继承形式是 public。
去除个别成员
如果进行 private 或 protected 继承,则基类成员的访问级别在派生类中比在基类中更受限:
派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。
class Base { public: std::size_t size() const { return n; } protected: std::size_t n; }; class Derived : private Base { . . . };
在这一继承层次中,size 在 Base 中为 public,但在 Derived 中为private。为了使 size 在 Derived 中成为 public,可以在 Derived 的 public部分增加一个 using 声明。
class Derived : private Base { public: // maintain access levels for members related to the size of the object using Base::size; protected: using Base::n; // ... };
默认继承保护级别
struct 和 class 保留字定义的类具有不同的默认访问级别,同样,默认继承访问级别根据使用哪个保留字定义派生类也不相同。
class Base { /* ... */ }; struct D1 : Base { /* ... */ }; // public inheritance by default class D2 : Base { /* ... */ }; // private inheritance by default
有一种常见的误解认为用 struct 保留字定义的类与用 class 定义的类有更大的区别。唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别:
class D3 : public Base { public: /* ... */ }; // equivalent definition of D3 struct D3 : Base { // inheritance public by default /* ... */ // initial member access public by default }; struct D4 : private Base { private: /* ... */ }; // equivalent definition of D4 class D4 : Base { // inheritance private by default /* ... */ // initial member access private by default };
15.2.6. 友元关系与继承
像其他类一样,基类或派生类可以使其他类或函数成为友元(第 12.5 节)。友元可以访问类的 private 和 protected 数据。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
class Base { friend class Frnd; protected: int i; }; // Frnd has no access to members in D1 class D1 : public Base { protected: int j; }; class Frnd { public: int mem(Base b) { return b.i; } // ok: Frnd is friend to Base int mem(D1 d) { return d.i; } // error: friendship doesn‘t inherit }; // D2 has no access to members in Base class D2 : public Frnd { public: int mem(Base b) { return b.i; } // error: friendship doesn‘t inherit };
15.2.7. 继承与静态成员
如果基类定义 static 成员(第 12.6 节),则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。
15.3. 转换与继承
我们已经看到,每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类型引用到基类类型引用的自动转换,即,可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。
15.3.1. 派生类到基类的转换
如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。
引用转换不同于转换对象
可以将派生类型的对象传给希望接受基类引用的函数。也许会因此认为对象进行转换,但是,事实并非如此。将对象传给希望接受引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。
将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。
一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。
用派生类对象对基类对象进行初始化或赋值
对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。
用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现:
class Derived; class Base { public: Base(const Derived&); // create a new Base from a Derived Base &operator=(const Derived&); // assign from a Derived // ... };
然而,类显式定义怎样用派生类型对象对基类类型进行初始化或赋值并不常见,相反,基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符(第十三章),这些成员接受一个形参,该形参是基类类型的(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值:
Item_base item; // object of base type Bulk_item bulk; // object of derived type // ok: uses Item_base::Item_base(const Item_base&) constructor Item_base item(bulk); // bulk is "sliced down" to its Item_base portion // ok: calls Item_base::operator=(const Item_base&) item = bulk; // bulk is "sliced down" to its Item_base portion
用 Bulk_item 类型的对象调用 Item_base 类的复制构造函数或赋值操作符时,将发生下列步骤:
• 将 Bulk_item 对象转换为 Item_base 引用,这仅仅意味着将一个Item_base 引用绑定到 Bulk_item 对象。
• 将该引用作为实参传给复制构造函数或赋值操作符。
• 那些操作符使用 Bulk_item 的 Item_base 部分分别对调用构造函数或赋值的 Item_base 对象的成员进行初始化或赋值。
• 一旦操作符执行完毕,对象即为 Item_base。它包含 Bulk_item 的Item_base 部分的副本,但实参的 Bulk_item 部分被忽略。
派生类到基类转换的可访问性
像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号。
如果是 public 继承,则用户代码和后代类都可以使用派生类到基类的转换。如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生
类型对象转换为基类对象。如果是 private 继承,则从 private 继承类派生的类不能转换为基类。如果是 protected 继承,则后续派生类的成员可以转换为基类类型。
15.3.2. 基类到派生类的转换
从基类到派生类的自动转换是不存在的。需要派生类对象时不能使用基类对象:
Item_base base; Bulk_item* bulkP = &base; // error: can‘t convert base to derived Bulk_item& bulkRef = base; // error: can‘t convert base to derived Bulk_item bulk = base; // error: can‘t convert base to derived
没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那么就可以试图使用该派生类对象访问不存在的成员。
15.4. 构造函数和复制控制
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
15.4.1. 基类构造函数和复制控制
继承对基类构造函数的唯一影响是,在确定提供哪些构造函数时,必须考虑一类新用户。像任意其他成员一样,构造函数可以为 protected 或 private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。
15.4.2. 派生类构造函数
派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
合成的派生类默认构造函数
派生类的合成默认构造函数(第 12.4.3 节与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
对于 Bulk_item 类,合成的默认构造函数会这样执行:
1. 调用 Item_base 的默认构造函数,将 isbn 成员初始化空串,将 price成员初始化为 0。
2. 用常规变量初始化规则初始化 Bulk_item 的成员,也就是说,qty 和discount 成员会是未初始化的。
定义默认构造函数
因为 Bulk_item 具有内置类型成员,所以应定义自己的默认构造函数:
class Bulk_item : public Item_base { public: Bulk_item(): min_qty(0), discount(0.0) { } // as before };
这个构造函数使用构造函数初始化列表(第 7.7.3 节)初始化 min_qty 和discount 成员,该构造函数还隐式调用 Item_base 的默认构造函数初始化对象的基类部分。
向基类构造函数传递实参
除了默认构造函数之外,Item_base 类还使用户能够初始化 isbn 和 price成员,我们希望支持同样 Bulk_item 对象的初始化,事实上,我们希望用户能够指定整个 Bulk_item 的值,包括折扣率和数量。
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // as before };
要建立 bulk,首先运行 Item_base 构造函数,该构造函数使用从Bulk_item 构造函数初始化列表传来的实参初始化 isbn 和 price。Item_base构造函数执行完毕之后,再初始化 Bulk_item 的成员。最后,运行 Bulk_item 构造函数的(空)函数体。
在派生类构造函数中使用默认实参
也可以将这两个 Bulk_item 构造函数编写为一个接受默认实参的构造函数:
class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // as before };
只能初始化直接基类
一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类 C 从类 B 派生,类 B 从类 A 派生,则 B 是 C 的直接基类。虽然每个 C 类
对象包含一个 A 类部分,但 C 的构造函数不能直接初始化 A 部分。相反,需要类 C 初始化类 B,而类 B 的构造函数再初始化类 A。这一限制的原因是,类B 的作者已经指定了怎样构造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。
关键概念:重构
将 Disc_item 加到 Item_base 层次是重构(refactoring)的一个例子。重构包括重新定义类层次,将操作和/或数据从一个类移到另一个类。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构。
要实现这个设计,首先需要定义 Disc_item 类:
// class to hold discount rate and quantity // derived classes will implement pricing strategies using these data class Disc_item : public Item_base { public: Disc_item(const std::string& book = "", double sales_price = 0.0, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), quantity(qty), discount(disc_rate) { } protected: std::size_t quantity; // purchase size for discount to apply double discount; // fractional discount to apply };
这个类继承 Item_base 类并定义了自己的 discount 和 quantity 成员。它唯一的成员函数是构造函数,用以初始化基类和 Disc_item 定义的成员。其次,可以重新实现 Bulk_item 以继承 Disc_item,而不再直接继承Item_base:
// discount kicks in when a specified number of copies of same book are sold // the discount is expressed as a fraction to use to reduce the normal price class Bulk_item : public Disc_item { public: Bulk_item(const std::string& book = "", double sales_price = 0.0, std::size_t qty = 0, double disc_rate = 0.0): Disc_item(book, sales_price, qty, disc_rate) { } // redefines base version so as to implement bulk purchase discount policy double net_price(std::size_t) const; };
Bulk_item 类现在有一个直接基类 Disc_item,还有一个间接基类Item_base。每个 Bulk_item 对象有三个子对象:一个(空的)Bulk_item 部分和一个 Disc_item 子对象,Disc_item 子对象又有一个 Item_base 基类子对象。
虽然 Bulk_item 没有自己的数据成员,但为获取值用来初始化其继承成员,它定义了一个构造函数。
派生类构造函数只能初始化自己的直接基类,在 Bulk_item 类的构造函数初始化列表中指定 Item_base 是一个错误。
15.4.3. 复制控制和继承
合成操作对对象的基类部分连同派生部分的成员一起进行复制、赋值或撤销,使用基类的复制构造函数、赋值操作符或析构函数对基类部分进行复制、赋值或撤销。
只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。
定义派生类复制构造函数
如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。
如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ }; class Derived: public Base { public: // Base::Base(const Base&) not invoked automatically Derived(const Derived& d): Base(d) /* other member initialization */ { /*... */ } };
派生类赋值操作符
赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
// Base::operator=(const Base&) not invoked automatically Derived &Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); // assigns the base part // do whatever needed to clean up the old value in the derived part // assign the members from the derived } return *this; }
派生类析构函数
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:
class Derived: public Base { public: // Base::~Base invoked automatically ~Derived() { /* do what it takes to clean up derived members */ } };
对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。
15.4.4. 虚析构函数
删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数:
class Item_base { public: // no work, but virtual destructor needed // if base pointer that points to a derived object is ever deleted virtual ~Item_base() { } };
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:
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
基类析构函数是三法则(第 13.3 节)的一个重要例外。三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构
造函数。
构造函数和赋值操作符不是虚函数
将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
15.4.5. 构造函数和析构函数中的虚函数
构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。
撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。
在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
无论由构造函数(或析构函数)直接调用虚函数,或者从构造函数(或析构函数)所调用的函数间接调用虚函数,都应用这种绑定。
虚函数的派生类版本很可能会访问派生类对象的成员,毕竟,如果派生类版本不需要使用派生类对象的成员,派生类多半能够使用基类中的定义。但是,对象的派生部分的成员不会在基类构造函数运行期间初始化,实际上,如果允许这样的访问,程序很可能会崩溃。