Team LiB
Previous Section Next Section

15.2. Defining Base and Derived Classes

15.2. 定义基类和派生类

In many ways, base and derived classes are defined like other classes we have already seen. However, there are some additional features that are required when defining classes in an inheritance hierarchy. This section will present those features. Subsequent sections will see how use of these features impacts classes and the programs we write using inherited classes.

基类和派生类的定义在许多方面像我们已见过的其他类一样。但是,在继承层次中定义类还需要另外一些特性,本节将介绍这些特性,后续的章节将介绍这些特性的使用对类以及使用继承类编写的程序有何影响。

15.2.1. Defining a Base Class

15.2.1. 定义基类

Like any other class, a base class has data and function members that define its interface and implementation. In the case of our (very simplified) bookstore pricing application, our Item_base class defines the book and net_price functions and needs to store an ISBN and the standard price for the book:

像任意其他类一样,基类也有定义其接口和实现的数据和函数成员。在(非常简化的)书店定价应用程序的例子中,Item_base 类定义了 booknet_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
     };

For the most part, this class looks like others we have seen. It defines a constructor along with the functions we have already described. That constructor uses default arguments (Section 7.4.1, p. 253), which allows it to be called with zero, one, or two arguments. It initializes the data members from these arguments.

这个类的大部分看起来像我们已见过的其他类一样。它定义了一个构造函数以及我们已描述过的函数,该构造函数使用默认实参(第 7.4.1 节),允许用 0 个、1 个或两个实参进行调用,它用这些实参初始化数据成员。

The new parts are the protected access label and the use of the virtual keyword on the destructor and the net_price function. We'll explain virtual destructors in Section 15.4.4 (p. 587), but for now it is worth noting that classes used as the root class of an inheritance hierarchy generally define a virtual destructor.

新的部分是 protected 访问标号以及对析构函数和 net_price 函数所使用的保留字 virtual。我们将第 15.4.4 节解释虚析构函数,现在只需注意到继承层次的根类一般都要定义虚析构函数即可。

Base-Class Member Functions
基类成员函数

The Item_base class defines two functions, one of which is preceded by the keyword virtual. The purpose of the virtual keyword is to enable dynamic binding. By default, member functions are nonvirtual. Calls to nonvirtual functions are resolved at compile time. To specify that a function is virtual, we precede its return type by the keyword virtual. Any nonstatic member function, other than a constructor, may be virtual. The virtual keyword appears only on the member-function declaration inside the class. The virtual keyword may not be used on a function definition that appears outside the class body.

Item_base 类定义了两个函数,其中一个前面带有保留字 virtual。保留字 virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字 virtual。除了构造函数之外,任意非 static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。

We'll have more to say about virtual functions in Section 15.2.4 (p. 566).

第 15.2.4 节将进一步介绍虚函数。

A base class usually should define as virtual any function that a derived class will need to redefine.

基类通常应将派生类需要重定义的任意函数定义为虚函数。



Access Control and Inheritance
访问控制和继承

In a base class, the public and private labels have their ordinary meanings: User code may access the public members and may not access the private members of the class. The private members are accessible only to the members and friends of the base class. A derived class has the same access as any other part of the program to the public and private members of its base class: It may access the public members and has no access to the private members.

在基类中,publicprivate 标号具有普通含义:用户代码可以访问类的 public 成员而不能访问 private 成员,private 成员只能由基类的成员和友元访问。派生类对基类的 publicprivate 成员的访问权限与程序中任意其他部分一样:它可以访问 public 成员而不能访问 private 成员。

Sometimes a class used as a base class has members that it wants to allow its derived classes to access, while still prohibiting access to those same members by other users. The protected access label is used for such members. A protected member may be accessed by a derived object but may not be accessed by general users of the type.

有时作为基类的类具有一些成员,它希望允许派生类访问但仍禁止其他用户访问这些成员。对于这样的成员应使用受保护的访问标号protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。

Our Item_base class expects its derived classes to redefine the net_price function. To do so, those classes will need access to the price member. Derived classes are expected to access isbn in the same way as ordinary users: through the book access function. Hence, the isbn member is private and is inaccessible to classes that inherit from Item_base.

我们的 Item_base 类希望它的派生类重定义 net_price 函数,为了重定义 net_price 函数,这些类将需要访问 price 成员。希望派生类用与普通用户一样通过 book 访问函数访问 isbn,因此,isbn 成员为 private,不能被 Item_base 的继承类所访问。

Exercises Section 15.2.1

Exercise 15.1:

What is a virtual member?

什么是虚成员?

Exercise 15.2:

Define the protected access label. How does it differ from private?

给出 protected 访问标号的定义。它与 private 有何不同?

Exercise 15.3:

Define your own version of the Item_base class.

定义自己的 Item_base 类版本。

Exercise 15.4:

A library has different kinds of materials that it lends outbooks, CDs, DVDs, and so forth. Each of the different kinds of lending material has different check-in, check-out, and overdue rules. The following class defines a base class that we might use for this application. Identify which functions are likely to be defined as virtual and which, if any, are likely to be common among all lending materials. (Note: we assume that LibMember is a class representing a customer of the library, and Date is a class representing a calendar day of a particular year.)

图书馆可以借阅不同种类的资料——书、CD、DVD 等等。不同种类的借阅资料有不同的登记、检查和过期规则。下面的类定义了这个应用程序可以使用的基类。指出在所有借阅资料中,哪些函数可能定义为虚函数,如果有,哪些函数可能是公共的。(注:假定 LibMember 是表示图书馆读者的类,Date 是表示特定年份的日历日期的类。)

     class Library {
     public:
         bool check_out(const LibMember&);
         bool check_in (const LibMember&);
         bool is_late(const Date& today);
         double apply_fine();
         ostream& print(ostream& = cout);
         Date due_date() const;
         Date date_borrowed() const;
         string title() const;
         const LibMember& member() const;
     };


15.2.2. protected Members

15.2.2. protected 成员

The protected access label can be thought of as a blend of private and public:

可以认为 protected 访问标号是 privatepublic 的混合:

  • Like private members, protected members are inaccessible to users of the class.

    private 成员一样,protected 成员不能被类的用户访问。

  • Like public members, the protected members are accessible to classes derived from this class.

    public 成员一样,protected 成员可被该类的派生类访问。

In addition, protected has another important property:

此外,protected 还有另一重要性质:

  • A derived object may access the protected members of its base class only through a derived object. The derived class has no special access to the protected members of base type objects.

    派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限。

As an example, let's assume that Bulk_item defines a member function that takes a reference to a Bulk_item object and a reference to an Item_base object. This function may access the protected members of its own object as well as those of its Bulk_item parameter. However, it has no special access to the protected members in its Item_base parameter:

例如,假定 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
     }

The use of d.price is okay, because the reference to price is through an object of type Bulk_item. The use of b.price is illegal because Bulk_item has no special access to objects of type Item_base.

d.price 的使用正确,因为是通过 Bulk_item 类型对象引用 priceb.price 的使用非法,因为对 Base_item 类型的对象没有特殊访问访问权限。

Key Concept: Class Design and Protected Members

关键概念:类设计与受保护成员

In the absence of inheritance, a class has two kinds of users: members of the class itself and the users of that class. This separation between kinds of users is reflected in the division of the class into private and public access levels. Users may access only the public interface; class members and friends may access both the public and private members.

如果没有继承,类只有两种用户:类本身的成员和该类的用户。将类划分为 privatepublic 访问级别反映了用户种类的这一分隔:用户只能访问 public 接口,类成员和友元既可以访问 public 成员也可以访问 private 成员。

Under inheritance, there is now a third kind of user of a class: programmers who will define new classes that are derived from the class. The provider of a derived class often (but not always) needs access to the (ordinarily private) base-class implementation. To allow that access while still preventing general access to the implementation, an additional access label, protected, is provided. The data and function members in a protected section of a class remain inaccessible to the general program, yet are accessible to the derived class. Anything placed within a private section of the base class is accessible only to the class itself and its friends. The private members are not accessible to the derived classes.

有了继承,就有了类的第三种用户:从类派生定义新类的程序员。派生类的提供者通常(但并不总是)需要访问(一般为 private 的)基类实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了附加的 protected 访问标号。类的 protected 部分仍然不能被一般程序访问,但可以被派生类访问。只有类本身和友元可以访问基类的 private 部分,派生类不能访问基类的 private 成员。

When designing a class to serve as a base class, the criteria for designating a member as public do not change: It is still the case that interface functions should be public and data generally should not be public. A class designed to be inherited from must decide which parts of the implementation to declare as protected and which should be private. A member should be made private if we wish to prevent subsequently derived classes from having access to that member. A member should be made protected if it provides an operation or data that a derived class will need to use in its implementation. In other words, the interface to the derived type is the combination of both the protected and public members.

定义类充当基类时,将成员设计为 public 的标准并没有改变:仍然是接口函数应该为 public 而数据一般不应为 public。被继承的类必须决定实现的哪些部分声明为 protected 而哪些部分声明为 private。希望禁止派生类访问的成员应该设为 private,提供派生类实现所需操作或数据的成员应设为 protected。换句话说,提供给派生类型的接口是 protected 成员和 public 成员的组合。


15.2.3. Derived Classes

15.2.3. 派生类

To define a derived class, we use a class derivation list to specify the base class(es). A class derivation list names one or more base classes and has the form

为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个基类,具有如下形式:

     class classname: access-label base-class

where access-label is one of public, protected, or private, and base-class is the name of a previously defined class. As we'll see, a derivation list might name more than one base class. Inheritance from a single base class is most common and is the topic of this chapter. Section 17.3 (p. 731) covers use of multiple base classes.

这里 access-labelpublicprotectedprivatebase-class 是已定义的类的名字。类派生列表可以指定多个基类。继承单个基类是为常见,也是本章的主题。第 17.3 节讨论多个基类的使用。

We'll have more to say about the access label used in a derivation list in Section 15.2.5 (p. 570). For now, what's useful to know is that the access label determines the access to the inherited members. When we want to inherit the interface of a base class, then the derivation should be public.

第 15.2.5 节将进一步介绍派生列表中使用的访问标号,现在,只需要了解访问标号决定了对继承成员的访问权限。如果想要继承基类的接口,则应该进行 public 派生。

A derived class inherits the members of its base class and may define additional members of its own. Each derived object contains two parts: those members that it inherits from its base and those it defines itself. Typically, a derived class (re)defines only those aspects that differ from or extend the behavior of the base.

派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员。一般而言,派生类只(重)定义那些与基类不同或扩展基类行为的方面。

Defining a Derived Class
定义派生类

In our bookstore application, we will derive Bulk_item from Item_base, so Bulk_item will inherit the book, isbn, and price members. Bulk_item must redefine its net_price function and define the data members needed for that operation:

在书店应用程序中,将从 Item_base 类派生 Bulk_item 类,因此 Bulk_item 类将继承 bookisbnprice 成员。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
      };

Each Bulk_item object contains four data elements: It inherits isbn and price from Item_base and defines min_qty and discount. These latter two members specify the minimum quantity and the discount to apply once that number of copies are purchased. The Bulk_item class also needs to define a constructor, which we shall do in Section 15.4 (p. 580).

每个 Bulk_item 对象包含四个数据成员:从 Item_base 继承的 isbnprice,自己定义的 min_qtydiscount,后两个成员指定最小数量以及购买超过该数量时给的折扣。Bulk_item 类还需要定义一个构造函数,我们将在第 15.4 节定义它。

Derived Classes and virtual Functions
派生类和虚函数

Ordinarily, derived classes redefine the virtual functions that they inherit, although they are not requried to do so. If a derived class does not redefine a virtual, then the version it uses is the one defined in its base class.

尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。

A derived type must include a declaration for each inherited member it intends to redefine. Our Bulk_item class says that it will redefine the net_price function but will use the inherited version of book.

派生类型必须对想要重定义的每个继承成员进行声明。Bulk_item 类指出,它将重定义 net_price 函数但将使用 book 的继承版本。

With one exception, the declaration (Section 7.4, p. 251)of a virtual function in the derived class must exactly match the way the function is defined in the base. That exception applies to virtuals that return a reference (or pointer) to a type that is itself a base class. A virtual function in a derived class can return a reference (or pointer) to a class that is publicly derived from the type returned by the base-class function.

派生类中虚函数的声明(第 7.4 节)必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。

For example, the Item_base class might define a virtual function that returned an Item_base*. If it did, then the instance defined in the Bulk_item class could be defined to return either an Item_base* or a Bulk_item*. We'll see an example of this kind of virtual in Section 15.9 (p. 607).

例如,Item_base 类可以定义返回 Item_base* 的虚函数,如果这样,Bulk_item 类中定义的实例可以定义为返回 Item_base*Bulk_item*第 15.9 节将介绍这种虚函数的一个例子。

Once a function is declared as virtual in a base class it remains virtual; nothing the derived classes do can change the fact that the function is virtual. When a derived class redefines a virtual, it may use the virtual keyword, but it is not required to do so.

一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。



Derived Objects Contain Their Base Classes as Subobjects
派生类对象包含基类对象作为子对象

A derived object consists of multiple parts: the (nonstatic) members defined in the derived class itself plus the subobjects made up of the (nonstatic) members of its base class. We can think of our Bulk_item class as consisting of two parts as represented in Figure 15.1.

派生类对象由多个部分组成:派生类本身定义的(非 static)成员加上由基类(非 static)成员组成的子对象。可以认为 Bulk_item 对象由图 15.1 表示的两个部分组成。

Figure 15.1. Conceptual Structure of a Bulk_item Object
图 15.1. Bulk_item 对象的概念结构


There is no requirement that the compiler lay out the base and derived parts of an object contiguously. Hence, Figure 15.1 is a conceptual, not physical, representation of how classes work.

C++ 语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列,因此,图 15.1 是关于类如何工作的概念表示而不是物理表示。



Functions in the Derived May Use Members from the Base
派生类中的函数可以使用基类的成员

As with any member function, a derived class function can be defined inside the class or outside, as we do here for the net_price function:

像任意成员函数一样,派生类函数可以在类的内部或外部定义,正如这里的 net_price 函数一样:

     // 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;
     }

This function generates a discounted price: If the given quantity is more than min_qty, we apply the discount (which was stored as a fraction) to the price.

该函数产生折扣价格:如果给定数量多于 min_qty,就对 price 应用 discountdiscount 存储为分数)。

Because each derived object has a base-class part, classes may access the public and protected members of its base class as if those members were members of the derived class itself.

因为每个派生类对象都有基类部分,类可以访问共基类的 publicprotected 成员,就好像那些成员是派生类自己的成员一样。



A Class Must Be Defined to Be Used as a Base Class
用作基类的类必须是已定义的

A class must be defined before it can be used as a base class. Had we declared, but not defined, Item_base, we could not use it as our base class:

已定义的类才可以用作基类。如果已经声明了 Item_base 类,但没有定义它,则不能用 Item_base 作基类:

     class Item_base; // declared but not defined
     // error: Item_base must be defined
     class Bulk_item : public Item_base { ... };

The reason for this restriction should already be easy to see: Each derived class contains, and may access, the members of its base class. To use those members, the derived class must konw what they are. One implication of this rule is that it is impossible to derive a class from itself.

这一限制的原因应该很容易明白:每个派生类包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道它们是什么。这一规则暗示着不可能从类自身派生出一个类。

Using a Derived Class as a Base Class
用派生类作基类

A base class can itself be a derived class:

基类本身可以是一个派生类:

     class Base { /* ... */ };
     class D1: public Base { /* ... */ };
     class D2: public D1 { /* ... */ };

Each class inherits all the members of its base class. The most derived type inherits the members of its base, which in turn inherits the members of its base and so on up the inheritance chain. Effectively, the most derived object contains a subobject for each of its immediate-base and indirect-base classes.

每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。从效果来说,最底层的派生类对象包含其每个直接基类间接基类的子对象。

Declarations of Derived Classes
派生类的声明

If we need to declare (but not yet define) a derived class, the declaration contains the class name but does not include its derivation list. For example, the following forward declaration of Bulk_item results in a compile-time error:

如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。例如,下面的前向声明会导致编译时错误:

     // error: a forward declaration must not include the derivation list
     class Bulk_item : public Item_base;

The correct forward declarations are:

正确的前向声明为:

     // forward declarations of both derived and nonderived class
     class Bulk_item;
     class Item_base;

Exercises Section 15.2.3

Exercise 15.5:

Which of the following declarations, if any, are incorrect?

如果有,下面声明中哪些是错误的?

     class Base { ... };

     (a) class Derived : public Derived { ... };
     (b) class Derived : Base { ... };
     (c) class Derived : private Base { ... };
     (d) class Derived : public Base;
     (e) class Derived inherits Base { ... };

Exercise 15.6:

Write your own version of the Bulk_item class.

编写自己的 Bulk_item 类版本。

Exercise 15.7:

We might define a type to implement a limited discount strategy. This class would give a discount for books purchased up to a limit. If the number of copies purchased exceeds that limit, then the normal price should be applied to any books purchased beyond the limit. Define a class that implements this strategy.

可以定义一个类型实现有限折扣策略。这个类可以给低于某个上限的购书量一个折扣,如果购买的数量超过该上限,则超出部分的书应按正常价格购买。定义一个类实现这种策略。


15.2.4. virtual and Other Member Functions

15.2.4. virtual 与其他成员函数

By default, function calls in C++ do not use dynamic binding. To trigger dynamic binding, two conditions must be met: First, only member functions that are specified as virtual can be dynamically bound. By default, member functions are not virtual; nonvirtual functions are not dynamically bound. Second, the call must be made through a reference or a pointer to a base-class type. To understand this requirement, we need to understand what happens when we use a reference or pointer to an object that has a type from an inheritance hierarchy.

C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。要理解这一要求,需要理解在使用继承层次中某一类型的对象的引用或指针时会发生什么。

Derived to Base Conversions
从派生类型到基类的转换

Because every derived object contains a base part, we can bind a base-type reference to the base-class part of a derived object. We can also use a pointer to base to point to a derived object:

因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象:

     // 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;                // p points to the Item_base part of bulk

This code uses the same base-type pointer to point to an object of the base type and to an object of the derived type. It also calls a function that expects a reference to the base type, passing an object of the base-class type and also passing an object of the derived type. Both uses are fine, because every derived object has a base part.

这段代码使用同一基类类型指针指向基类类型的对象和派生类型的对象,该代码还传递基类类型和派生类型的对象来调用需要基类类型引用的函数,两种使用都是正确的,因为每个派生类对象都拥有基类部分。

Because we can use a base-type pointer or reference to refer to a derived-type object, when we use a base-type reference or pointer, we don't know the type of the object to which the pointer or reference is bound: A base-type reference or pointer might refer to an object of base type or an object of derived type. Regardless of which actual type the object has, the compiler treats the object as if it is a base type object. Treating a derived object as if it were a base is safe, because every derived object has a base subobject. Also, the derived class inherits the operations of the base class, meaning that any operation that might be performed on a base object is available through the derived object as well.

因为可以使用基类类型的指针或引用来引用派生类型对象,所以,使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型:基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象。无论实际对象具有哪种类型,编译器都将它当作基类类型对象。将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即,任何可以在基类对象上执行的操作也可以通过派生类对象使用。

The crucial point about references and pointers to base-class types is that the static typethe type of the reference or pointer, which is knowable at compile timeand the dynamic typethe type of the object to which the pointer or reference is bound, which is knowable only at run timemay differ.

基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。



Calls to virtual Functions May Be Resolved at Run time
可以在运行时确定 virtual 函数的调用

Binding a base-type reference or pointer to a derived object has no effect on the underlying object. The object itself is unchanged and remains a derived object. The fact that the actual type of the object might differ from the static type of the reference or pointer addressing that object is the key to dynamic binding in C++.

将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。

When a virtual function is called through a reference or pointer, the compiler generates code to decide at run time which function to call. The function that is called is the one that corresponds to the dynamic type. As an example, let's look again at the print_total function:

通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。例如,我们再来看 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;
     }

Because the item parameter is a reference and net_price is virtual, the version of net_price that is called in item.net_price(n) depends at run time on the actual type of the argument bound to the item parameter:

因为 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

In the first call, the item parameter is bound, at run time, to an object of type Item_base. As a result, the call to net_price inside print_total calls the version defined in Item_base. In the second call, item is bound to an object of type Bulk_item. In this call, the version of net_price called from print_total will be the one defined by the Bulk_item class.

在第一个调用中,item 形参在运行时绑定到 Item_base 类型的对象,因此,print_total 内部调用 Item_base 中定义的 net_price 版本。在第二个调用中,item 形参绑定到 Bulk_item 类型的对象,从 print_total 调用的是 Bulk_item 类定义的 net_price 版本。

Key Concept: Polymorphism in C++

关键概念:C++ 中的多态性

The fact that the static and dynamic types of references and pointers can differ is the cornerstone of how C++ supports polymorphism.

引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。

When we call a function defined in the base class through a base-class reference or pointer, we do not know the precise type of the object on which the function is executed. The object on which the function executes might be of the base type or it might be an object of a derived type.

通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。

If the function called is nonvirtual, then regardless of the actual object type, the function that is executed is the one defined by the base type. If the function is virtual, then the decision as to which function to run is delayed until run time. The version of the virtual function that is run is the one defined by the type of the object to which the reference is bound or to which the pointer points.

如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。

From the perspective of the code that we write, we need not care. As long as the classes are designed and implemented correctly, the operations will do the right thing whether the actual object is of base or derived type.

从编写代码的角度看我们无需担心。只要正确地设计和实现了类,不管实际对象是基类类型或派生类型,操作都将完成正确的工作。

On the other hand, an object is not polymorphicits type is known and unchanging. The dynamic type of an object (as opposed to a reference or pointer) is always the same as the static type of the object. The function that is run, virtual or nonvirtual, is the one defined by the type of the object.

另一方面,对象是非多态的——对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。

Virtuals are resolved at run time only if the call is made through a reference or pointer. Only in these cases is it possible for an object's dynamic type to be unknown until run time.

只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。




Nonvirtual Calls Are Resolved at Compile Time
在编译时确定非 virtual 调用

Regardless of the actual type of the argument passed to print_total, the call of book is resolved at compile time to Item_base::book.

不管传给 print_total 的实参的实际类型是什么,对 book 的调用在编译时确定为调用 Item_base::book

Even if Bulk_item defined its own version of the book function, this call would call the one from the base class.

即使 Bulk_item 定义了自己的 book 函数版本,这个调用也会调用基类中的版本。



Nonvirtual functions are always resolved at compile time based on the type of the object, reference, or pointer from which the function is called. The type of item is reference to const Item_base, so a call to a nonvirtual function on that object will call the one from Item_base regardless of the type of the actual object to which item refers at run time.

非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。item 的类型是 const Item_base 的引用,所以,无论在运行时 item 引用的实际对象是什么类型,调用该对象的非虚函数都将会调用 Item_base 中定义的版本。

Overriding the Virtual Mechanism
覆盖虚函数机制

In some cases, we want to override the virtual mechanism and force a call to use a particular version of a virtual function. We can do so by using the scope operator:

在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:

     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);

This code forces the call to net_price to be resolved to the version defined in Item_base. The call will be resolved at compile time.

这段代码强制将 net_price 调用确定为 Item_base 中定义的版本,该调用将在编译时确定。

Only code inside member functions should ever need to use the scope operator to override the virtual mechanism.

只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。



Why might we wish to override the virtual mechanism? The most common reason is when a derived-class virtual calls the version from the base. In such cases, the base-class version might do work common to all types in the hierarchy. Each derived type adds only whatever is particular to its own type.

为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。

For example, we might define a Camera hierarchy with a virtual display operation. The display function in the Camera class would display information common to all Cameras. A derived class, such as PerspectiveCamera, would need to display both that common information and the information unique to PerspectiveCamera. Rather than duplicate the Camera operations within PerspectiveCamera's implementation of display, we could explicitly invoke the Camera version to display the common information. In a case such as this one, we'd know exactly which instance to invoke, so there would be no need to go through the virtual mechanism.

例如,可以定义一个具有虚操作的 Camera 类层次。Camera 类中的 display 函数可以显示所有的公共信息,派生类(如 PerspectiveCamera)可能既需要显示公共信息又需要显示自己的独特信息。可以显式调用 Camera 版本以显示公共信息,而不是在 PerspectiveCameradisplay 实现中复制 Camera 的操作。在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。

When a derived virtual calls the base-class version, it must do so explicitly using the scope operator. If the derived function neglected to do so, then the call would be resolved at run time and would be a call to itself, resulting in an infinite recursion.

派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。



Virtual Functions and Default Arguments
虚函数与默认实参

Like any other function, a virtual function can have default arguments. As usual, the value, if any, of a default argument used in a given call is determined at compile time. If a call omits an argument that has a default value, then the value that is used is the one defined by the type through which the function is called, irrespective of the object's dynamic type. When a virtual is called through a reference or pointer to base, then the default argument is the value specified in the declaration of the virtual in the base class. If a virtual is called through a pointer or reference to derived, the default argument is the one declared in the version in the derived class.

像其他任何函数一样,虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。

Using different default arguments in the base and derived versions of the same virtual is almost guaranteed to cause trouble. Problems are likely to arise when the virtual is called through a reference or pointer to base, but the version that is executed is the one defined by the derived. In such cases, the default argument defined for the base version of the virtual will be passed to the derived version, which was defined using a different default argument.

在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。

Exercises Section 15.2.4

Exercise 15.8:

Given the following classes, explain each print function:

对于下面的类,解释每个函数:

     struct base {
        string name() { return basename; }
        virtual void print(ostream &os) { os << basename; }
     private:
           string basename;
     };

     struct derived {
        void print() { print(ostream &os); os << " " << mem; }
     private:
           int mem;
     };

If there is a problem in this code, how would you fix it?

如果该代码有问题,如何修正?

Exercise 15.9:

Given the classes in the previous exercise and the following objects, determine which function is called at run time:

给定上题中的类和如下对象,确定在运行时调用哪个函数:

     base bobj;    base *bp1 = &base;  base &br1 = bobj;
     derived dobj; base *bp2 = &doboj; base &br2 = dobj;

     (a) bobj.print(); (b) dobj.print();   (c) bp1->name();
     (d) bp2->name();  (e) br1.print();    (f) br2.print();


15.2.5. Public, Private, and Protected Inheritance

15.2.5. 公用、私有和受保护的继承

Access to members defined within a derived class is controlled in exactly the same way as access is handled for any other class (Section 12.1.2, p. 432). A derived class may define zero or more access labels that specify the access level of the members following that label. Access to the members the class inherits is controlled by a combination of the access level of the member in the base class and the access label used in the derived class' derivation list.

派生类中定义的成员访问控制的处理与任意其他类中完全一样(第 12.1.2 节)。派生类可以定义零个或多个访问标号,指定跟随其后的成员的访问级别。对类所继承的成员的访问由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。

Each class controls access to the members it defines. A derived class may further restrict but may not loosen the access to the members that it inherits.

每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。



The base class itself specifies the minimal access control for its own members. If a member is private in the base class, then only the base class and its friends may access that member. The derived class has no access to the private members of its base class, nor can it make those members accessible to its own users. If a base class member is public or protected, then the access label used in the derivation list determines the access level of that member in the derived class:

基类本身指定对自身成员的最小访问控制。如果成员在基类中为 private,则只有基类和基类的友元可以访问该成员。派生类不能访问基类的 private 成员,也不能使自己的用户能够访问那些成员。如果基类成员为 publicprotected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:

  • In public inheritance, the members of the base retain their access levels: The public members of the base are public members of the derived and the protected members of the base are protected in the derived.

    如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected 成员。

  • In protected inheritance, the public and protected members of the base class are protected members in the derived class.

    如果是受保护继承,基类的 publicprotected 成员在派生类中为 protected 成员。

  • In private inheritance, all the members of the base class are private in the derived class.

    如果是私有继承,基类的的所有成员在派生类中为 private 成员。

As an example, consider the following hierarchy:

例如,考虑下面的继承层次:

     class Base {
     public:
         void basemem();   // public member
     protected:
         int i;            // protected member
         // ...
     };
     struct Public_derived : public Base {
         int use_base() { return i; } // ok: derived classes can access i
         // ...
     };
     struct Private_derived : private Base {
         int use_base() { return i; } // ok: derived classes can access i
     };

All classes that inherit from Base have the same access to the members in Base, regardless of the access label in their derivation lists. The derivation access label controls the access that users of the derived class have to the members inherited from Base:

无论派生列表中是什么访问标号,所有继承 Base 的类对 Base 中的成员具有相同的访问。派生访问标号将控制派生类的用户对从 Base 继承而来的成员的访问:

     Base b;
     Public_derived d1;
     Private_derived d2;
     b.basemem();   // ok: basemem is public
     d1.basemem();  // ok: basemem is public in the derived class
     d2.basemem();  // error: basemem is private in the derived class

Both Public_derived and Private_derived inherit the basemem function. That member retains its access level when the inheritance is public, so d1 can call basemem. In Private_derived, the members of Base are private; users of Private_derived may not call basemem.

Public_derivedPrivate_derived 都继承了 basemem 函数。当进行 public 继承时,该成员保持其访问标号,所以,d1 可以调用 basemem。在 Private_derived 中,Base 的成员为 privatePrivate_derived 的用户不能调用 basemem

The derivation access label also controls access from indirectly derived classes:

派生访问标号还控制来自非直接派生类的访问:

     struct Derived_from Private : public Private_derived {
         // error: Base::i is private in Private_derived
         int use_base() { return i; }
     };
     struct Derived_from_Public : public Public_derived {
         // ok: Base::i remains protected in Public_derived
         int use_base() { return i; }
     };

Classes derived from Public_derived may access i from the Base class because that member remains a protected member in Public_derived. Classes derived from Private_derived have no such access. To them all the members that Private_base inherited from Base are private.

Public_derived 派生的类可以访问来自 Base 类的 i,是因为该成员在 Public_derived 中仍为 protected 成员。从 Private_derived 派生的类没有这样的访问,对它们而言,Private_derivedBase 继承的所有成员均为 private

Interface versus Implementation Inheritance
接口继承与实现继承

A publicly derived class inherits the interface of its base class; it has the same interface as its base class. In well-designed class hierarchies, objects of a publicly derived class can be used wherever an object of the base class is expected.

public 派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,public 派生类的对象可以用在任何需要基类对象的地方。

Classes derived using either private or protected do not inherit the base-class interface. Instead, these derivations are often referred to as implementation inheritance. The derived class uses the inherited class in its implementation but does not expose the fact of the inheritance as part of its interface.

使用 privateprotected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实现中使用被继承但继承基类的部分并未成为其接口的一部分。

As we'll see in Section 15.3 (p. 577), whether a class uses interface or implementation inheritance has important implications for users of the derived class.

第 15.3 节所介绍的,类是使用接口继承还是实现继承对派生类的用户具有重要含义。

By far the most common form of inheritance is public.

迄今为止,最常见的继承形式是 public



Key Concept: Inheritance versus Composition

关键概念:继承与组合

The design of inheritance hierarchies is a complex topic in its own right and well beyond the scope of this language primer. However, there is one important design guide that is so fundamental that every programmer should be familiar with it.

继承层次的设计本身是个复杂的主题,已超出本书的范围。但是,有一个重要的设计指南非常基础,每个程序员都应该熟悉它。

When we define one class as publicly inherited from another, the derived class should reflect a so-called "Is A" relationship to the base class. In our bookstore example, our base class represents the concept of a book sold at a stipulated price. Our Bulk_item is a kind of book, but one with a different pricing strategy.

定义一个类作为另一个类的公用派生类时,派生类应反映与基类的“是一种(Is A)”关系。在书店例子中,基类表示按规定价格销售的书的概念,Bulk_item 是一种书,但具有不同的定价策略。

Another common relationship among types is a so-called "Has A" relationship. Our bookstore classes have a price and they have an ISBN. Types related by a "Has A" relationship imply membership. Thus, our bookstore classes are composed from members representing the price and the ISBN.

类型之间另一种常见的关系是称为“有一个(Has A)”的关系。书店例子中的类具有价格和 ISBN。通过“有一个”关系而相关的类型暗含有成员关系,因此,书店例子中的类由表示价格和 ISBN 的成员组成。


Exempting Individual Members
去除个别成员

When inheritance is private or protected, the access level of members of the base may be more restrictive in the derived class than it was in the base:

如果进行 privateprotected 继承,则基类成员的访问级别在派生类中比在基类中更受限:

     class Base {
     public:
         std::size_t size() const { return n; }
     protected:
         std::size_t n;
     };
     class Derived : private Base { . . . };

The derived class can restore the access level of an inherited member. The access level cannot be made more or less restrictive than the level originally specified within the base class.

派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。



In this hierarchy, size is public in Base but private in Derived. To make size public in Derived we can add a using declaration for it to a public section in Derived. By changing the definition of Derived as follows, we can make the size member accessible to users and n accessible to classes subsequently derived from Derived:

在这一继承层次中,sizeBase 中为 public,但在 Derived 中为 private。为了使 sizeDerived 中成为 public,可以在 Derivedpublic 部分增加一个 using 声明。如下这样改变 Derived 的定义,可以使 size 成员能够被用户访问,并使 n 能够被从 Derived 派生的类访问:

     class Derived : private Base {
     public:
        // maintain access levels for members related to the size of the object
        using Base::size;
     protected:
         using Base::n;
         // ...
      };

Just as we can use a using declaration (Section 3.1, p. 78) to use names from the std namespace, we may also use a using declaration to access a name from a base class. The form is the same except that the left-hand side of the scope operator is a class name instead of a namespace name.

正如可以使用 using 声明(第 3.1 节)从命名空间使用名字,也可以使用 using 声明访问基类中的名字,除了在作用域操作符左边用类名字代替命名空间名字之外,使用形式是相同的。

Default Inheritance Protection Levels
默认继承保护级别

In Section 2.8 (p. 65) we learned that classes defined with the struct and class keywords have different default access levels. Similarly, the default inheritance access level differs depending on which keyword is used to define the derived class. A derived class defined using the class keyword has private inheritance. A class is defined with the struct keyword, has public inheritance:

第 2.8 节介绍过用 structclass 保留字定义的类具有不同的默认访问级别,同样,默认继承访问级别根据使用哪个保留字定义派生类也不相同。使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承:

     class Base { /* ... */ };
     struct D1 : Base { /* ... */ };   // public inheritance by default
     class D2 : Base { /* ... */ };    // private       inheritance by default

It is a common misconception to think that there are deeper differences between classes defined using the struct keyword and those defined using class. The only differences are the default protection level for members and the default protection level for a derivation. There are no other distinctions:

有一种常见的误解认为用 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
     };

Although private inheritance is the default when using the class keyword, it is also relatively rare in practice. Because private inheritance is so rare, it is usually a good idea to explicitly specify private, rather than rely on the default. Being explicit makes it clear that private inheritance is intended and not an oversight.

尽管私有继承在使用 class 保留字时是默认情况,但这在实践中相对罕见。因为私有继承是如此罕见,通常显式指定 private 是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。



Exercises Section 15.2.5

Exercise 15.10:

In the exercises to Section 15.2.1 (p. 562) you wrote a base class to represent the lending policies of a library. Assume the library offers the following kinds of lending materials, each with its own check-out and check-in policy. Organize these items into an inheritance hierarchy:

第 15.2.1 节的习题中编写了一个表示图书馆借阅政策的基类。假定图书馆提供下列种类的借阅资料,每一种有自己的检查和登记政策。将这些项目组织成一个继承层次:

     book                     audio book                  record
     children's puppet        sega video game             video
     cdrom book               nintendo video game        rental book
     sony play stationvideo game

Exercise 15.11:

Choose one of the following general abstractions containing a family of types (or choose one of your own). Organize the types into an inheritance hierarchy:

在下列包含一族类型的一般抽象中选择一种(或者自己选择一个),将这些类型组织成一个继承层次。



     (a) Graphical file formats (such as gif, tiff, jpeg, bmp)
     (a) 图像文件格式(如 gif,tiff,jpeg,bmp)
     (b) Geometric primitives (such as box, circle, sphere, cone)
     (b) 几何图元(如矩形,圆,球形,锥形)
     (c) C++ language types (such as class, function, member
     (c) C++ 语言的类型(如类,函数,成员函数)
 function)

Exercise 15.12:

For the class you chose in the previous exercise, identify some of the likely virtual functions as well as public and protected members.

对上题中选择的类,标出可能的虚函数以及 publicprotected 成员。


15.2.6. Friendship and Inheritance

15.2.6. 友元关系与继承

As with any other class, a base or derived class can make other class(es) or function(s) friends (Section 12.5, p. 465). Friends may access the class' private and protected data.

像其他类一样,基类或派生类可以使其他类或函数成为友元(第 12.5 节)。友元可以访问类的 privateprotected 数据。

Friendship is not inherited. Friends of the base have no special access to members of its derived classes. If a base class is granted friendship, only the base has special access. Classes derived from that base have no access to the class granting friendship.

友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。



Each class controls friendship to its own members:

每个类控制对自己的成员的友元关系:

     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
     };

If a derived class wants to grant access to its members to the friends of its base class, the derived class must do so explicitly: Friends of the base have no special access to types derived from that base class. Similarly, if a base and its derived types all need access to another class, that class must specifically grant access to the base and each derived class.

如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做:基类的友元对从该基类派生的类型没有特殊访问权限。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类的和每一个派生类。

15.2.7. Inheritance and Static Members

15.2.7. 继承与静态成员

If a base class defines a static member (Section 12.6, p. 467) there is only one such member defined for the entire hierarchy. Regardless of the number of classes derived from the base class, there exists a single instance of each static member. static members obey normal access control: If the member is private in the base class, then derived classes have no access to it. Assuming the member is accessible, we can access the static member either through the base or derived class. As usual, we can use either the scope operator or the dot or arrow member access operators.

如果基类定义 static 成员(第 12.6 节),则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。

     struct Base {
         static void statmem(); // public by default
     };
     struct Derived : Base {
         void f(const Derived&);
     };
     void Derived::f(const Derived &derived_obj)
     {
        Base::statmem();      // ok: Base defines statmem
        Derived::statmem();   // ok: Derived in herits statmem
        // ok: derived objects can be used to access static from base
        derived_obj.statmem();     // accessed through Derived object
        statmem();                 // accessed through this class

Exercises Section 15.2.7

Exercise 15.13:

Given the following classes, list all the ways a member function in C1 might access the static members of ConcreteBase. List all the ways an object of type C2 might access those members.

对于下面的类,列出 C1 中的成员函数访问 ConcreteBasestatic 成员的所有方式,列出 C2 类型的对象访问这些成员的所有方式。

     struct ConcreteBase {
         static std::size_t object_count();
     protected:
         static std::size_t obj_count;
     };
     struct C1 : public ConcreteBase { /* . . . */ };
     struct C2 : public ConcreteBase { /* . . . */ };


Team LiB
Previous Section Next Section