《C++ Primer》之面向对象编程(三)

  • 继承情况下的类作用域

在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好象这些成员是派生类成员一样。如果编写如下代码:

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

名字 book 的使用将这样确定:bulkBulk_item 类对象,在 Bulk_item 类中查找,找不到名字 book;因为从 Item_base 派生 Bulk_item,所以接着在 Item_base 类中查找,找到名字 book,引用成功地确定了。

  • 名字查找在编译时发生

 对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员。例如,可以给 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
     };

只能通过 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*

重新定义 itemP 的访问是错误的,因为基类类型的指针(引用或对象)只能访问对象的基类部分,而在基类中没有定义 discount_policy 成员。//根本原因在于,函数名字需要在编译时决定,所以在编译时itemP->discount_policy(),找不到Item_base类中有discount_policy成员,所以会出现错误

我们举个例子来说明一下,比如说派生类中有成员print_derived,而基类中没有:

而我们尝试用基类指针去调用这个成员:

就会报错:

因为在编译阶段找不到基类对象中有print_derived这个成员,但是,如果我们避免这样使用,而是在基类中有一个虚成员,那么就没问题:

这样再调用:

会成功输出:

一来是因为基类中有了虚成员test_virtual所以在调用它时不会报错;二来是test_virtual是虚函数,实现了动态绑定,而上一个例子二者都不满足,所以无法实现动态绑定。

  • 名字冲突与继承

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

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

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

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

则输出将是 42

  • 使用作用域操作符访问被屏蔽的成员

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

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

作用域操作符指示编译器在 Base 中查找 mem。设计派生类时,只要可能,最好避免与基类成员的名字冲突。

  • 作用域与成员函数

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

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

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

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

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

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

  • 重载函数

像其他任意函数一样,成员函数(无论虚还是非虚)也可以重载。派生类可以重定义所继承的 0 个或多个版本。如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。如果派生类想通过自身类型使用的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。有时类需要仅仅重定义一个重载集中某些版本的行为,并且想要继承其他版本的含义,在这种情况下,为了重定义需要特化的某个版本而不得不重定义每一个基类版本,可能会令人厌烦。//在派生类中新加一个与基类中同名的函数,不会同时重载基类中的同名函数,必须把基类中的每个同名函数都在派生类中重写一遍,才能做到重载

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

  • 虚函数与作用域

还记得吗,要获得动态绑定,必须通过基类的引用或指针调用虚成员。当我们这样做时,编译器器将在基类中查找函数。假定找到了名字,编译器就检查实参是否与形参匹配。现在可以理解虚函数为什么必须在基类和派生类中拥有同一原型了。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。考虑如下(人为的)为集合:

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 中定义的非虚版本。

  • 通过基类调用被屏蔽的虚函数

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

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

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

posted @ 2016-03-11 22:49  _No.47  阅读(446)  评论(0编辑  收藏  举报