C++学习笔记十五-面向对象编程(一)

一、概述:面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在 C++ 中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。

      1.通过继承我们能够定义这样的类,它们对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。

      2.我们经常称因继承而相关联的类为构成了一个继承层次。其中有一个类称为根,所以其他类直接或间接继承根类。

      3.在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。

      4.动态绑定我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。

      5.在 C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。

 

二、定义基类和派生类:

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

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

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

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

     5.protected 成员:派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 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
     }

     6.派生类:类派生列表指定了一个或多个基类,具有如下形式:

     class classname: access-label base-class
     这里 access-label 是 publicprotectedprivate,base-class 是已定义的类的名字。类派生列表可以指定多个基类。继承单个基类是为常见.
 
   7.派生类和虚函数:
      a.尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。
      b.派生类型必须对想要重定义的每个继承成员进行声明。
c.派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。 d.
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。
 
   8.派生类对象包含基类对象作为子对象:
派生类对象由多个部分组成:派生类本身定义的(非 static)成员加上由基类(非 static)成员组成的子对象。C++ 语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列.
 
9.
派生类中的函数可以使用基类的成员:因为每个派生类对象都有基类部分,类可以访问共基类的 publicprotected 成员,就好像那些成员是派生类自己的成员一样。
 
  10.用作基类的类必须是已定义的:
已定义的类才可以用作基类。如果已经声明了 Item_base 类,但没有定义它,则不能用 Item_base 作基类.
 
11.
用派生类作基类:每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。
从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
 
  12.派生类的声明:如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。

        正确的前向声明为:

     // forward declarations of both derived and nonderived class
     class Bulk_item;
     class Item_base;
  13.virtual 与其他成员函数:C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。
     a.从派生类型到基类的转换:因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象.
     b.基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。
     c.对象是非多态的——对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。
 
   14.非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。
 
   15.覆盖虚函数机制:在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:
     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);

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

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

 

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

     17.公用、私有和受保护的继承:每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。

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

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

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

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

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

     18.接口继承与实现继承:

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

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

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

          在这一继承层次中,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;
         // ...
      };

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

     struct 保留字定义的类与用 class 定义的类唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别.

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

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

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

 

三、转换与继承

    概述:每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类型引用到基类类型引用的自动转换,即,可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。

            基类类型对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。

            虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但,没有从派生类型对象到基类类型对象的直接转换。

   1.派生类到基类的转换:如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。

   2。用派生类对象对基类对象进行初始化或赋值:基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参是基类类型的(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

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

   3.派生类到基类转换的可访问性:像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号。

        a.要确定到基类的转换是否可访问,可以考虑基类的 public成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的。

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

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

    4.基类到派生类的转换:没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那么就可以试图使用该派生类对象访问不存在的成员。

    5.有时更令人惊讶的是,甚至当基类指针或引用实际绑定到绑定到派生类对象时,从基类到派生类的转换也存在限制:

     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 申请在运行时进行检查。

 

四、构造函数和复制控制

      概述:每个派生类对象由派生类中定义的(非 static)成员加上一个或多个基类子对象构成,这一事实影响着派生类型对象时,也会构造、复制、赋值和撤销这些基类子对象。构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。

      1.基类构造函数和复制控制:本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。

      2.派生类构造函数:派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。

         a.合成的派生类默认构造函数:派生类的合成默认构造函数与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。

         b.构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。

     3.只能初始化直接基类:一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。

     4.尊重基类接口:构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。同样,派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。如果那些成员为 publicprotected,派生构造函数可以在构造函数函数体中给基类成员赋值,但是,这样做会违反基类的接口。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。

    5.复制控制和继承:像任意其他类一样,派生类也可以使用合成复制控制成员。合成操作对对象的基类部分连同派生部分的成员一起进行复制、赋值或撤销,使用基类的复制构造函数、赋值操作符或析构函数对基类部分进行复制、赋值或撤销

       a.只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。

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

   6.定义派生类复制构造函数:如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分.

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

   8.派生类析构函数:析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员.对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。

 

   9.处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数.如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同.

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

     b.即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。

    10.构造函数和赋值操作符不是虚函数:

         a.构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。对象都没创建,没有虚函数表呢,构造函数倒是可以在一定程度上调用虚函数.

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

将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。

   11.如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。

        无论由构造函数(或析构函数)直接调用虚函数,或者从构造函数(或析构函数)所调用的函数间接调用虚函数,都应用这种绑定。

posted @ 2012-06-27 09:51  ForFreeDom  阅读(497)  评论(0编辑  收藏  举报