第十五章. 面向对象编程

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

继承和动态绑定在两个方面简化了我们的程序:能够容易地定义与其他类相似但又不相同的新类,能够更容易地编写忽略这些相似类型之间区别的程序。

许多应用程序的特性可以用一些相关但略有不同的概念来描述。例如,书店可以为不同的书提供不同的定价策略,有些书可以只按给定价格出售,另一些书可以根据不同的折扣策略出售。可以给购买某书一定数量的顾客打折,或者,购买一定数量以内可以打折而超过给定限制就付全价。

面向对象编程(Object-oriented programming,OOP)与这种应用非常匹配。通过继承可以定义一些类型,以模拟不同种类的书,通过动态绑定可以编写程序,使用这些类型而又忽略与具体类型相关的差异。

继承和动态绑定的思想在概念上非常简单,但对于如何创建应用程序以及对于程序设计语言必须支持哪些特性,它们的含义深远。在讨论 C++ 如何支持面向对象编程之前,我们将介绍这种编程风格的一些基本概念。

15.1. 面向对象编程:概述

面向对象编程的关键思想是多态性(polymorphism)。多态性派生于一个希腊单词,意思是“许多形态”。之所以称通过继承而相关联的类型为多态类型,是因为在许多情况下可以互换地使用派生类型或基类型的“许多形态”。正如我们将看到的,在 C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。

继承

通过继承我们能够定义这样的类,它们对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。派生类(derived class)能够继承基类(baseclass)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性。最后,除了从基类继承的成员之外,派生类还可以定义更多的成员。

我们经常称因继承而相关联的类为构成了一个继承层次。其中有一个类称为根,所以其他类直接或间接继承根类。在书店例子中,我们将定义一个基类,命名为 Item_base,命名为 Bulk_item,表示带数量折扣销售的书。

这些类至少定义如下操作:
  • 名为 book 的操作,返回 ISBN。

  • 名为 net_price 的操作,返回购买指定数量的书的价格。

    Item_base 的派生类将无须改变地继承 book 函数:派生类不需要重新定义获取 ISBN 的含义。另一方面,每个派生类需要定义自己的 net_price 函数版本,以实现适当的折扣价格策略。

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

    讨论过这些之后,可以看到我们的类将定义三个(const)成员函数:

  • 非虚函数 std::string book(),返回 ISBN。由 Item_base 定义,Bulk_item 继承。

  • 虚函数 double net_price(size_t) 的两个版本,返回给定数目的某书的总价。Item_base 类和 Bulk_item 类将定义该函数自己的版本。

    动态绑定

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

    例如,书店应用程序可以允许顾客在一次交易中选择几本书,当顾客购书时,应用程序可以计算总的应付款,指出最终账单的一个部分将是为每本书打印一行,以显示总数和售价。

    可以定义一个名为 print_total 的函数管理应用程序的这个部分。给定一个项目和数量,函数应打印 ISBN 以及购买给定数量的某书的总价。这个函数的输出应该像这样:

         ISBN: 0-201-54848-8 number sold: 3 total price: 98
         ISBN: 0-201-82470-1 number sold: 5 total price: 202.5
    

    可以这样编写 print_total 函数:

  1. // calculate and print price for given number of copies, applyingany discounts

  1. void print_total(ostream &os,const Item_base &item, size_t n)
    

    {

  2.    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;

      }

     该函数的工作很普通:调用其 item 形参的 book 和 net_price 函数,打印结果。关于这个函数,有两点值得注意。

     第一,虽然这个函数的第二形参是 Item_base 的引用但可以将 Item_base对象或 Bulk_item 对象传给它。

     第二,因为形参是引用且 net_price 是虚函数,所以对 net_price 的调用将在运行时确定。调用哪个版本的 net_price 将依赖于传给 print_total 的实参。如果传给 print_total 的实参是一个 Bulk_item 对象,将运行 Bulk_item中定义的应用折扣的 net_price;如果实参是一个 Item_base 对象,则调用由Item_base 定义的版本。

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

15.2. 定义基类和派生类

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

15.2.1.定义基类

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

};

[~构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样的一个特殊函数,它可以完成所需的资源回收,作为类构造函数的补充。]

[const 修饰函数是 为函数重载提供了一个参考,上一个函数的重载]

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

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


访问控制和继承

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

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

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

15.2.2. protected 成员
可以认为 protected 访问标号是 private 和 public 的混合:

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

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

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

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

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

}

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

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

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

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

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


15.2.3. 派生类

为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个

基类,具有如下形式:
class classname: access-label base-class

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

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

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

定义派生类

在书店应用程序中,将从 Item_base 类派生 Bulk_item 类,因此Bulk_item 类将继承 book、isbn 和 price 成员。Bulk_item 类必须重定义net_price 函数定义该操作所需要的数据成员:

// discount kicks in when a specified number of copies of same bookare sold

// the discount is expressed as a fraction used to reduce the normalprice

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

每个 Bulk_item 对象包含四个数据成员:从 Item_base 继承的 isbn 和price,自己定义的 min_qty 和 discount,后两个成员指定最小数量以及购买


超过该数量时给的折扣。Bulk_item 类还需要定义一个构造函数,我们将在第15.4 节定义它。


派生类和虚函数

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

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

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

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

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


派生类对象包含基类对象作为子对象

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

派生类中的函数可以使用基类的成员

像任意成员函数一样,派生类函数可以在类的内部或外部定义,正如这里的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;

}

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

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

用作基类的类必须是已定义的

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

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

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

用派生类作基类

基类本身可以是一个派生类:
     class Base { /* ... */ };
     class D1: public Base { /* ... */ };
     class D2: public D1 { /* ... */ };

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

派生类的声明

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

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

正确的前向声明为:

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



 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值