Team LiB
Previous Section Next Section

15.5. Class Scope under Inheritance

15.5. 继承情况下的类作用域

Each class maintains its own scope (Section 12.3, p. 444) within which the names of its members are defined. Under inheritance, the scope of the derived class is nested within the scope of its base classes. If a name is unresolved within the scope of the derived class, the enclosing base-class scope(s) are searched for a definition of that name.

每个类都保持着自己的作用域(第 12.3 节),在该作用域中定义了成员的名字。在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。

It is this hierarchical nesting of class scopes under inheritance that allows the members of the base class to be accessed directly as if they are members of the derived class. When we write

正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好象这些成员是派生类成员一样。如果编写如下代码:

     Bulk_item bulk;
     cout << bulk.book();

the use of the name book is resolved as follows:

名字 book 的使用将这样确定:

  1. bulk is an object of the Bulk_item class. The Bulk_item class is searched for book. That name is not found.

    bulkBulk_item 类对象,在 Bulk_item 类中查找,找不到名字 book

  2. Because Bulk_item is derived from Item_Base, the Item_Base class is searched next. The name book is found in the Item_base class. The reference is resolved successfully.

    因为从 Item_base 派生 Bulk_item,所以接着在 Item_base 类中查找,找到名字 book,引用成功地确定了。

15.5.1. Name Lookup Happens at Compile Time

15.5.1. 名字查找在编译时发生

The static type of an object, reference, or pointer determines the actions that the object can perform. Even when the static and dynamic types might differ, as can happen when a reference or pointer to a base type is used, the static type determines what members can be used. As an example, we might add a member to the Disc_item class that returns a pair holding the minimum (or maximum) quantity and the discounted price:

对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员。例如,可以给 Disc_item 类增加一个成员,该成员返回一个保存最小(或最大)数量和折扣价格的 pair 对象:

     class Disc_item : public Item_base {
     public:
         std::pair<size_t, double> discount_policy() const
             { return std::make_pair(quantity, discount); }
         // other members as before
     };

We can access discount_policy only through an object, pointer, or reference of type Disc_item or a class derived from Disc_item:

只能通过 Disc_item 类型或 Disc_item 派生类型的对象、指针或引用访问 discount_policy

     Bulk_item bulk;
     Bulk_item *bulkP = &bulk;  // ok: static and dynamic types are the same
     Item_base *itemP = &bulk;  // ok: static and dynamic types differ
     bulkP->discount_policy();  // ok: bulkP has type Bulk_item*
     itemP->discount_policy();  // error: itemP has type Item_base*

The call through itemP is an error because a pointer (reference or object) to a base type can access only the base parts of an object and there is no discount_policy member defined in the base class.

重新定义 itemP 的访问是错误的,因为基类类型的指针(引用或对象)只能访问对象的基类部分,而在基类中没有定义 discount_policy 成员。

Exercises Section 15.5.1

Exercise 15.21:

Redefine your Item_base hierarchy to include a Disc_item class.

重新定义 Item_base 层次以包含 Disc_item 类。

Exercise 15.22:

Redefine Bulk_item and the class you implemented in the exercises from Section 15.2.3 (p. 567) that represents a limited discount strategy to inherit from Disc_item.

重新定义 Bulk_item 和你在 第 15.2.3 节习题中实现的那个继承 Disc_item 的表示有限折扣策略的类。


15.5.2. Name Collisions and Inheritance

15.5.2. 名字冲突与继承

Although a base-class member can be accessed directly as if it were a member of the derived class, the member retains its base-class membership. Normally we do not care which actual class contains the member. We usually need to care only when a base- and derived-class member share the same name.

虽然可以直接访问基类成员,就像它是派生类成员一样,但是成员保留了它的基类成员资格。一般我们并不关心是哪个实际类包含成员,通常只在基类和派生类共享同一名字时才需要注意。

A derived-class member with the same name as a member of the base class hides direct access to the base-class member.

与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。



     struct Base {
         Base(): mem(0) { }
     protected:
         int mem;
     };
     struct Derived : Base {
         Derived(int i): mem(i) { }    // initializes Derived::mem
         int get_mem() { return mem; } // returns Derived::mem
     protected:
         int mem;   // hides mem in the base
      };

The reference to mem inside get_mem is resolved to the name inside Derived. Were we to write

get_mem 中对 mem 的引用被确定为使用 Derived 中的名字。如果编写如下代码:

     Derived d(42);
     cout << d.get_mem() << endl;   // prints 42

then the output would be 42.

则输出将是 42

Using the Scope Operator to Access Hidden Members
使用作用域操作符访问被屏蔽成员

We can access a hidden base-class member by using the scope operator:

可以使用作用域操作符访问被屏蔽的基类成员:

     struct Derived : Base {
         int get_base_mem() { return Base::mem; }
     };

The scope operator directs the compiler to look for mem starting in Base.

作用域操作符指示编译器在 Base 中查找 mem

When designing a derived class, it is best to avoid name collisions with members of the base class whenever possible.

设计派生类时,只要可能,最好避免与基类成员的名字冲突。



Exercises Section 15.5.2

Exercise 15.23:

Given the following base- and derived-class definitions

对于下面的基类和派生类定义:

     struct Base {
         foo(int);
     protected:
         int bar;
         double foo_bar;
     };

     struct Derived : public Base {
         foo(string);
         bool bar(Base *pb);
         void foobar();
     protected:
         string bar;
     };

identify the errors in each of the following examples and how each might be fixed:

找出下述每个例子中的错误并说明怎样改正:

     (a) Derived d; d.foo(1024);
     (b) void Derived::foobar() { bar = 1024; }
     (c) bool Derived::bar(Base *pb)
              { return foo_bar == pb->foo_bar; }


15.5.3. Scope and Member Functions

15.5.3. 作用域与成员函数

A member function with the same name in the base and derived class behaves the same way as a data member: The derived-class member hides the base-class member within the scope of the derived class. The base member is hidden, even if the prototypes of the functions differ:

在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽:

     struct Base {
         int memfcn();
     };
     struct Derived : Base {
         int memfcn(int); // hides memfcn in the base
     };
     Derived d; Base b;
     b.memfcn();        // calls Base::memfcn
     d.memfcn(10);      // calls Derived::memfcn
     d.memfcn();        // error: memfcn with no arguments is hidden
     d.Base::memfcn();  // ok: calls Base::memfcn

The declaration of memfcn in Derived hides the declaration in Base. Not surprisingly, the first call through b, which is aBase object, calls the version in the base class. Similarly, the second call through d calls the one from Derived. What can be surprising is the third call:

Derived 中的 memfcn 声明隐藏了 Base 中的声明。这并不奇怪,第一个调用通过 Base 对象 b 调用基类中的版本,同样,第二个调用通过 d 调用 Derived 中的版本。可能比较奇怪的是第三个调用:

     d.memfcn(); // error: Derived has no memfcn that takes no arguments

To resolve this call, the compiler looks for the name memfcn, which it finds in the class Derived. Once the name is found, the compiler looks no further. This call does not match the definition of memfcn in Derived, which expects an int argument. The call provides no such argument and so is in error.

要确定这个调用,编译器需要查找名字 memfcn,并在 Derived 类中找到。一旦找到了名字,编译器就不再继续查找了。这个调用与 Derived 中的 memfcn 定义不匹配,该定义希望接受 int 实参,而这个函数调用没有提供那样的实参,因此出错。

Recall that functions declared in a local scope do not overload functions defined at global scope (Section 7.8.1, p. 268). Similarly, functions defined in a derived class do not overload members defined in the base. When the function is called through a derived object, the arguments must match a version of the function defined in the derived class. The base class functions are considered only if the derived does not define the function at all.

回忆一下,局部作用域中声明的函数不会重载全局作用域中定义的函数(第 7.8.1 节),同样,派生类中定义的函数也不重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。



Overloaded Functions
重载函数

As with any other function, a member function (virtual or otherwise) can be over-loaded. A derived class can redefine zero or more of the versions it inherits.

像其他任意函数一样,成员函数(无论虚还是非虚)也可以重载。派生类可以重定义所继承的 0 个或多个版本。

If the derived class redefines any of the overloaded members, then only the one(s) redefined in the derived class are accessible through the derived type.

如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。



If a derived class wants to make all the overloaded versions available through its type, then it must either redefine all of them or none of them.

如果派生类想通过自身类型使用的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。

Sometimes a class needs to redefine the behavior of only some of the versions in an overloaded set, and wants to inherit the meaning for others. It would be tedious in such cases to have to redefine every base-class version in order to redefine the ones that the class needs to specialize.

有时类需要仅仅重定义一个重载集中某些版本的行为,并且想要继承其他版本的含义,在这种情况下,为了重定义需要特化的某个版本而不得不重定义每一个基类版本,可能会令人厌烦。

Instead of redefining every base-class version that it inherits, a derived class can provide a using declaration (Section 15.2.5, p. 574) for the overloaded member. A using declaration specifies only a name; it may not specify a parameter list. Thus, a using declaration for a base-class member function name adds all the overloaded instances of that function to the scope of the derived-class. Having brought all the names into its scope, the derived class need redefine only those functions that it truly must define for its type. It can use the inherited definitions for the others.

派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供 using 声明(第 15.2.5 节)。一个 using 声明只能指定一个名字,不能指定形参表,因此,为基类成员函数名称而作的 using 声明将该函数的所有重载实例加到派生类的作用域。将所有名字加入作用域之后,派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。

15.5.4. Virtual Functions and Scope

15.5.4. 虚函数与作用域

Recall that to obtain dynamic binding, we must call a virtual member through a reference or a pointer to a base class. When we do so, the compiler looks for the function in the base class. Assuming the name is found, the compiler checks that the arguments match the parameters.

还记得吗,要获得动态绑定,必须通过基类的引用或指针调用虚成员。当我们这样做时,编译器器将在基类中查找函数。假定找到了名字,编译器就检查实参是否与形参匹配。

We can now understand why virtual functions must have the same prototype in the base and derived classes. If the base member took different arguments than the derived-class member, there would be no way to call the derived function from a reference or pointer to the base type. Consider the following (artificial) collection of classes:

现在可以理解虚函数为什么必须在基类和派生类中拥有同一原型了。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。考虑如下(人为的)为集合:

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

The version of fcn in D1 does not redefine the virtual fcn from Base. Instead, it hides fcn from the base. Effectively, D1 has two functions named fcn: The class inherits a virtual named fcn from the Base and defines its own, nonvirtual member named fcn that takes an int parameter. However, the virtual from the Base cannot be called from a D1 object (or reference or pointer to D1) because that function is hidden by the definition of fcn(int).

D1 中的 fcn 版本没有重定义 Base 的虚函数 fcn,相反,它屏蔽了基类的 fcn。结果 D1 有两个名为 fcn 的函数:类从 Base 继承了一个名为 fcn 的虚函数,类又定义了自己的名为 fcn 的非虚成员函数,该函数接受一个 int 形参。但是,从 Base 继承的虚函数不能通过 D1 对象(或 D1 的引用或指针)调用,因为该函数被 fcn(int) 的定义屏蔽了。

The class D2 redefines both functions that it inherits. It redefines the virtual version of fcn originally defined in Base and the nonvirtual defined in D1.

D2 重定义了它继承的两个函数,它重定义了 Base 中定义的 fcn 的原始版本并重定义了 D1 中定义的非虚版本。

Calling a Hidden Virtual through the Base Class
通过基类调用被屏蔽的虚函数

When we call a function through a base-type reference or pointer, the compiler looks for that function in the base class and ignores the derived classes:

通过基类类型的引用或指针调用函数时,编译器将在基类中查找该函数而忽略派生类:

     Base bobj;  D1 d1obj;  D2 d2obj;
     Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
     bp1->fcn();   // ok: virtual call, will call Base::fcnat run time
     bp2->fcn();   // ok: virtual call, will call Base::fcnat run time
     bp3->fcn();   // ok: virtual call, will call D2::fcnat run time

All three pointers are pointers to the base type, so all three calls are resolved by looking in Base to see if fcn is defined. It is, so the calls are legal. Next, because fcn is virtual, the compiler generates code to make the call at run time based on the actual type of the object to which the reference or pointer is bound. In the case of bp2, the underlying object is a D1. That class did not redefine the virtual version of fcn that takes no arguments. The call through bp2 is made (at run time) to the version defined in Base.

三个指针都是基类类型的指针,因此通过在 Base 中查找 fcn 来确定这三个调用,所以这些调用是合法的。另外,因为 fcn 是虚函数,所以编译器会生成代码,在运行时基于引用指针所绑定的对象的实际类型进行调用。在 bp2 的情况,基本对象是 D1 类的,D1 类没有重定义不接受实参的虚函数版本,通过 bp2 的函数调用(在运行时)调用 Base 中定义的版本。

Key Concept: Name Lookup and Inheritance

关键概念:名字查找与继承

Understanding how function calls are resolved is crucial to understanding inheritance hierarchies in C++. The following four steps are followed:

理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:

1.
Start by determining the static type of the object, reference, or pointer through which the function is called.
首先确定进行函数调用的对象、引用或指针的静态类型。
2.
Look for the function in that class. If it is not found, look in the immediate base class and continue up the chain of classes until either the function is found or the last class is searched. If the name is not found in the class or its enclosing base classes, then the call is in error.
在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3.
Once the name is found, do normal type-checking (Section 7.1.2, p. 229) to see if this call is legal given the definition that was found.
一旦找到了该名字,就进行常规类型检查(第 7.1.2 节),查看如果给定找到的定义,该函数调用是否合法。
4.
Assuming the call is legal, the compiler generates code. If the function is virtual and the call is through a reference or pointer, then the compiler generates code to determine which version to run based on the dynamic type of the object. Otherwise, the compiler generates code to call the function directly.
假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。

Exercises Section 15.5.4

Exercise 15.24:

Why is it that, given

对于如下代码:

     Bulk_item bulk;
     Item_base item(bulk);
     Item_base *p = &bulk;

the expression

为什么表达式

     p->net_price(10);

invokes the Bulk_item instance of net_price, whereas

调用 net_priceBulk_item 实例,而表达式

     item.net_price(10);

invokes the Item_base instance?

调用 Item_base 实例?

Exercise 15.25:

Assume Derived inherits from Base and that Base defines each of the following functions as virtual. Assuming Derived intends to define its own version of the virtual, determine which declarations in Derived are in error and specify what's wrong.

假定 Derived 继承 Base,并且 Base 将下面的函数定义为虚函数,假定 Derived 打算定义自己的这个虚函数的版本,确定在 Derived 中哪个声明是错误的,并指出为什么错。

     (a) Base* Base::copy(Base*);
         Base* Derived::copy(Derived*);
     (b) Base* Base::copy(Base*);
         Derived* Derived::copy(Base*);
     (c) ostream& Base::print(int, ostream&=cout);
         ostream& Derived::print(int, ostream&);
     (d) void Base::eval() const;
         void Derived::eval();


Team LiB
Previous Section Next Section