C++ Primer 4 第十五章 面向对象编程

本文深入探讨了 C++ 中的面向对象编程,包括虚函数、继承、构造函数、析构函数和纯虚函数等核心概念。重点讨论了动态绑定、基类与派生类的关系、成员访问控制以及接口继承与实现继承。文中还强调了析构函数为虚函数的重要性,以确保正确销毁继承层次的对象。此外,还提到了抽象基类的概念以及容器与继承的交互,建议使用句柄类来管理继承层次的对象,以减轻用户处理指针的责任。
摘要由CSDN通过智能技术生成
 

第十五章 面向对象编程

1. 面向对象编程:概述

在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。在 C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。

2. 定义基类和派生类

继承层次的根类一般都要定义虚析构函数。

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

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

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

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

派生类型必须对想要重定义的每个继承成员进行声明,派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。Item_base 类可以定义返回 Item_base* 的虚函数,如果这样,Bulk_item 类中定义的实例可以定义为返回 Item_base* 或 Bulk_item*。

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

     class Bulk_item : public Item_base;

         正确的前向声明为:

     class Bulk_item;

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

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

通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。

引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。对象是非多态的——对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。

非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。

覆盖虚函数机制:在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归:

     Item_base *baseP = &derived;

     double d = baseP->Item_base::net_price(42);

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

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

         虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。

公用、私有和受保护的继承:派生类中定义的成员访问控制的处理与任意其他类中完全一样。派生类可以定义零个或多个访问标号,指定跟随其后的成员的访问级别。对类所继承的成员的访问由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。派生类可以进一步限制但不能放松对所继承的成员的访问。

派生类不能访问基类的 private 成员,也不能使自己的用户能够访问那些成员。如果基类成员为 public 或 protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:

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

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

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

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

         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

     };

         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

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

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

     };

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

接口继承与实现继承:public 派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,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 声明。如下这样改变 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;

         // ...

      };

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

使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承:

     class Base { /* ... */ };

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

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

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

友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。

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

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

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

3. 转换与继承

存在从派生类型引用到基类类型引用的自动转换,即,可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。基类类型对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。对象转换的情况更为复杂。虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但,没有从派生类型对象到基类类型对象的直接转换。

派生类到基类的转换:如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。但是,一般可以使用派生类型对象对基类对象进行赋值或初始化。对对象进行初始化和/或赋值以及可以自动转换引用或指针,这之间的区别是微妙的。

引用转换不同于转换对象:将对象传给希望接受引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。即,一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值。

用派生类对象对基类对象进行初始化或赋值:对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。

用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现,在这种情况下,这些成员的定义将控制用 Derived 对象对 Base 对象进行初始化或赋值时会发生什么:

     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 类的复制构造函数或赋值操作符时,将发生下列步骤:

         1)将 Bulk_item 对象转换为 Item_base 引用,这仅仅意味着将一个 Item_base 引用绑定到 Bulk_item 对象。

         2)将该引用作为实参传给复制构造函数或赋值操作符。

         3)那些操作符使用 Bulk_item 的 Item_base 部分分别对调用构造函数或赋值的 Item_base 对象的成员进行初始化或赋值。

         4)一旦操作符执行完毕,对象即为 Item_base。它包含 Bulk_item 的 Item_base 部分的副本,但实参的 Bulk_item 部分被忽略。

在这种情况下,我们说 bulk 的 Bulk_item 部分在对 item 进行初始化或赋值时被“切掉”了。Item_base 对象只包含基类中定义的成员,不包含由任意派生类型定义的成员,Item_base 对象中没有派生类成员的存储空间。

如果是 public 继承,则用户代码和后代类都可以使用派生类到基类的转换。如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生类型对象转换为基类对象。如果是 private 继承,则从 private 继承类派生的类不能转换为基类。如果是 protected 继承,则后续派生类的成员可以转换为基类类型。

无论是什么派生访问标号,派生类本身都可以访问基类的 public 成员,因此,派生类本身的成员和友元总是可以访问派生类到基类的转换。

基类到派生类的转换:没有从基类类型到派生类型的(自动)转换,甚至当基类指针或引用实际绑定到绑定到派生类对象时,从基类到派生类的转换也存在限制:

     Bulk_item bulk;

     Item_base *itemP = &bulk;  // ok: dynamic type is Bulk_item

     Bulk_item *bulkP = itemP;  // error: can't convert base to derived

编译器在编译时无法知道特定转换在运行时实际上是安全的。编译器确定转换是否合法,只看指针或引用的静态类型。在这些情况下,如果知道从基类到派生类的转换是安全的,就可以使用 static_cast强制编译器进行转换。或者,可以用 dynamic_cast 申请在运行时进行检查。

4. 构造函数和复制控制

构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果派生类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。

本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。构造函数看起来像已经见过的许多构造函数一样.继承对基类构造函数的唯一影响是,在确定提供哪些构造函数时,必须考虑一类新用户。像任意其他成员一样,构造函数可以为 protected 或 private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为 protected.

派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化.

因为 Bulk_item 具有内置类型成员,所以应定义自己的默认构造函数:

     class Bulk_item : public Item_base {

     public:

         Bulk_item(): min_qty(0), discount(0.0) { }

         // as before

     };

这个构造函数使用构造函数初始化列表初始化 min_qty 和 discount 成员,该构造函数还隐式调用 Item_base 的默认构造函数初始化对象的基类部分。运行这个构造函数的效果是,首先使用 Item_base 的默认构造函数初始化 Item_base 部分,那个构造函数将 isbn 置为空串并将 price 置为 0。Item_base 的构造函数执行完毕后,再初始化 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

      };

这个构造函数使用有两个形参的Item_base 的构造函数初始化基类子对象,它将自己的 book 和 sales_price 实参传递给该构造函数。这个构造函数可以这样使用:

     Bulk_item bulk("0-201-82470-1", 50, 5, .19);

要建立 bulk,首先运行 Item_base 构造函数,该构造函数使用从 Bulk_item 构造函数初始化列表传来的实参初始化 isbn 和 price。Item_base 构造函数执行完毕之后,再初始化 Bulk_item 的成员。最后,运行 Bulk_item 构造函数的(空)函数体。

一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类 C 从类 B 派生,类 B 从类 A 派生,则 B 是 C 的直接基类。虽然每个 C 类对象包含一个 A 类部分,但 C 的构造函数不能直接初始化 A 部分。相反,需要类 C 初始化类 B,而类 B 的构造函数再初始化类 A。这一限制的原因是,类 B 的作者已经指定了怎样构造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。

派生类也可以使用合成复制控制成员。合成操作对对象的基类部分连同派生部分的成员一起进行复制、赋值或撤销,使用基类的复制构造函数、赋值操作符或析构函数对基类部分进行复制、赋值或撤销。Item_base 类及其派生类可以使用复制控制操作的合成版本。复制 Bulk_item 对象时,调用(合成的)Item_base 复制构造函数复制 isbn 和 price 成员。使用 string 复制构造函数复制 isbn,直接复制 price 成员。一旦复制了基类部分,就复制派生部分。Bulk_item 的两个成员都是 double 型,直接复制这些成员。赋值操作符和析构函数类似处理。

如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。

如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:

     class Base { /* ... */ };

     class Derived: public Base {

     public:

         // Base::Base(const Base&) not invoked automatically

         Derived(const Derived& d):

              Base(d) /* other member initialization */ { /*... */ }

     };

初始化函数 Base(d) 将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。如果省略基类初始化函数,如下代码:

     Derived(const Derived& d) /* derived member initizations */

               {/* ... */ }

效果是运行 Base 的默认构造函数初始化对象的基类部分。假定 Derived 成员的初始化从 d 复制对应成员,则新构造的对象将具有奇怪的配置:它的 Base 部分将保存默认值,而它的 Derived 成员是另一对象的副本。

派生类赋值操作符:赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。

     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;

     }

赋值操作符必须防止自身赋值。假定左右操作数不同,则调用 Base 类的赋值操作符给基类部分赋值。该操作符可以由类定义,也可以是合成赋值操作符,这没什么关系——我们可以直接调用它。基类操作符将释放左操作数中基类部分的值,并赋以来自 rhs 的新值。该操作符执行完毕后,接着要做的是为派生类中的成员赋值。

派生类析构函数:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:

     class Derived: public Base {

     public:

         // Base::~Base invoked automatically

         ~Derived()    { /* do what it takes to clean up derived members */ }

      };

         对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。

虚析构函数:删除指向动态分配对象的指针时,需要在释放对象的内存之前运行析构函数清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数:

     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

像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。

只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。

虽然可以在基类中将成员函数 operator= 定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。每个类有自己的赋值操作符,派生类中的赋值操作符有一个与类本身类型相同的形参,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。

将赋值操作符设为虚函数可能会令人混淆,因为虚函数必须在基类和派生类中具有同样的形参。基类赋值操作符有一个形参是自身类类型的引用,如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的 operator=。但是,对派生类而言,这个操作符与赋值操作符是不同的。

构造函数和析构函数中的虚函数:构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。

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

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

与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。可以使用作用域操作符访问被屏蔽的基类成员:

     struct Derived : Base {

         int get_base_mem() { return 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

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

成员函数(无论虚还是非虚)也可以重载。派生类可以重定义所继承的 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) 的定义屏蔽了.

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

     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

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

         1)首先确定进行函数调用的对象、引用或指针的静态类型。

         2)在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。

         3)一旦找到了该名字,就进行常规类型检查(第 7.1.2 节),查看如果给定找到的定义,该函数调用是否合法。

         4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数

6. 纯虚函数

将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用.

含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象.

在函数形参表后面写上 = 0 以指定纯虚函数:

     class Disc_item : public Item_base {

     public:

         double net_price(std::size_t) const = 0;

     };

试图创建抽象基类的对象将发生编译时错误:

     Disc_item discounted; // error: can't define a Disc_item object

7. 容器与继承

如果定义 multiset 保存基类类型的对象:

     multiset<Item_base> basket;

     Item_base base;

     Bulk_item bulk;

     basket.insert(base);  // ok: add copy of base to basket

     basket.insert(bulk);  // ok: but bulk sliced down to its base part

则加入派生类型的对象时,只将对象的基类部分保存在容器中。不能通过定义容器保存派生类对象来解决这个问题。在这种情况下,不能将 Item_base 对象放入容器——没有从基类类型到派生类型的标准转换。可以显式地将基类对象强制转换为派生类对象并将结果对象加入容器,但是,如果这样做,当试图使用这样的元素时,会产生大问题:在这种情况下,元素可以当作派生类对象对待,但派生类部分的成员将是未初始化的.

唯一可行的选择可能是使用容器保存对象的指针。这个策略可行,但代价是需要用户面对管理对象和指针的问题,用户必须保证只要容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证在容器消失时适当地释放对象。

8. 句柄类与继承

C++ 中面向对象编程不能使用对象支持面向对象编程,相反,必须使用指针或引用。但是,使用指针或引用会加重类用户的负担。

C++ 中一个通用的技术是定义包装(cover)类或句柄类。句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此,句柄的用户可以获得动态行为但无须操心指针的管理。

包装了继承层次的句柄有两个重要的设计考虑因素:

         1)像对任何保存指针的类一样,必须确定对复制控制做些什么。包装了继承层次的句柄通常表现得像一个智能指针或者像一个值。

         2)句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。

  指针型句柄实例(省略)【Coding】

9. 再谈文本查询示例(省略)【Coding】

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值