C++ 面向对象编程

《C++ Primer 4th》读书笔记

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

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

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

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

通过继承我们能够定义这样的类,它们对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。

通过动态绑定我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。我们经常称因继承而相关联的类为构成了一个继承层次。

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

定义基类

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

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

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

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

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

• 像 public 成员一样,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

}

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

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

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

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

尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。

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

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

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

class Item_base; // declared but not defined

// error: Item_base must be defined

class Bulk_item : public Item_base { ... };

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

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

// error: a forward declaration must not include the derivation list

class Bulk_item : public Item_base;

正确的前向声明为:

// forward declarations of both derived and nonderived class

class Bulk_item;

class Item_base;

从派生类型到基类的转换

因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即,任何可以在基类对象上执行的操作也可以通过派生类对象使用。

在运行时确定 virtual 函数的调用

基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。

关键概念:C++ 中的多态性

引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生

类型的。

如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。

从编写代码的角度看我们无需担心。只要正确地设计和实现了类,不管实际对象是基类类型或派生类型,操作都将完成正确的工作。

另一方面,对象是非多态的——对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。

只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。

注:多态实现形式包括:接口,抽象类 和 重写。而重载不应该认为是多态的实现。

成员函数的重载、覆盖与隐藏

重载与覆盖

成员函数被重载的特征:

(1)相同的范围(在同一个类中);

(2)函数名字相同;

(3)参数不同;

(4)virtual 关键字可有可无。

覆盖是指派生类函数覆盖基类函数,特征是:

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有virtual 关键字。

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

在编译时确定非 virtual 调用

非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。item 的类型是 const Item_base 的引用,所以,无论在运行时 item 引用的实际对象是什么类型,调用该对象的非虚函数都将会调用 Item_base 中定义的版本。

覆盖虚函数机制

可以使用作用域操作符:

Item_base *baseP = &derived;

// calls version from the base class regardless of the dynamic type of baseP

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

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

为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。

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

虚函数与默认实参

虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。

公用、私有和受保护的继承

对类所继承的成员的访问由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。

派生类不能访问基类的 private 成员,也不能使自己的用户能够访问那些成员。如果基类成员为 public 或

protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:

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

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

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

接口继承与实现继承

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

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

迄今为止,最常见的继承形式是 public。

去除个别成员

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

class Base {

public:

std::size_t size() const { return n; }

protected:

std::size_t n;

};

class Derived : private Base { . . . };

为了使 size 在 Derived 中成为 public,可以在 Derived 的 public部分增加一个 using 声明。如下这样改变 Derived 的定义,可以使 size 成员能够被用户访问,并使 n 能够被从 Derived 派生的类访问:

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

class Derived : private Base {

public:

// maintain access levels for members related to the size of the object

using Base::size;

protected:

using Base::n;

// ...

};

默认继承保护级别

用 struct 和 class 保留字定义的类具有不同的默认访问级别,同样,默认继承访问级别根据使用哪个保留字定义派生类也不相同。使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承:

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

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

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

有一种常见的误解认为用 struct 保留字定义的类与用 class 定义的类有更大的区别。唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别。

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

友元关系与继承

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

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

转换与继承

基类类型对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分。

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

引用转换不同于转换对象

将对象传给希望接受引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的

引用,对象本身未被复制,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。

将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。

一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。

用派生类对象对基类对象进行初始化或赋值

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

第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现:

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 的 Bulk_item 部分在对 item 进行初始化或赋值时被“切掉”了。

派生类到基类转换的可访问性

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

基类到派生类的转换

从基类到派生类的自动转换是不存在的。

Item_base base;

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

Bulk_item& bulkRef = base; // error: can't convert base to derived

Bulk_item bulk = base; // error: can't convert base to derived

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

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

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

构造函数和复制控制

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

继承对基类构造函数的唯一影响是,在确定提供哪些构造函数时,必须考虑一类新用户。像任意其他成员一样,构造函数可以为 protected 或 private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。

派生类构造函数

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

派生类的合成默认构造函数除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。

定义默认构造函数

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

class Bulk_item : public Item_base {

public:

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

// as before

};

运行这个构造函数的效果是,首先使用 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 = , double disc_rate = 0.0):

Item_base(book, sales_price),

min_qty(qty), discount(disc_rate) { }

// as before

};

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

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

关键概念:重构

将 Disc_item 加到 Item_base 层次是重构(refactoring)的一个例子。重构包括重新定义类层次,将操作和/或数据从一个类移到另一个类。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构。重构常见在面向对象应用程序中非常常见。值得注意的是,虽然改变了继承层次,使用 Bulk_item 类或 Item_base 类的代码不需要改变。然而,对类进行重构,或以任意其他方式改变类,使用这些类的任意代码都必须重新编译。

关键概念:尊重基类接口

构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。定义 Disc_item 时,通过定义它的构造函数指定了怎样初始化Disc_item 对象。一旦类定义了自己的接口,与该类对象的所有交互都应该通过该接口,即使对象是派生类对象的一部分也不例外。同样,派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。

如果那些成员为 public 或 protected,派生构造函数可以在构造函数函数体中给基类成员赋值,但是,这样做会违反基类的接口。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。

复制控制和继承

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

派生类析构函数

析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:

class Derived: public Base {

public:

// Base::~Base invoked automatically

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

*/ }

};

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

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

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

class Derived: public Base {

public:

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

Derived(const Derived& d):

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

};

如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值, 赋值操作符必须防止自身赋值。

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

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;

}

虚析构函数

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

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

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

要理解这种行为,考虑如果从基类构造函数(或析构函数)调用虚函数的派生类版本会怎么样。虚函数的派生类版本很可能会访问派生类对象的成员,毕竟,如果派生类版本不需要使用派生类对象的成员,派生类多半能够使用基类中的定义。但是,对象的派生部分的成员不会在基类构造函数运行期间初始化,实际上,如果允许这样的访问,程序很可能会崩溃。

继承情况下的类作用域

在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。

名字查找在编译时发生

对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员。

名字冲突与继承

虽然可以直接访问基类成员,就像它是派生类成员一样,但是成员保留了它的基类成员资格。一般我们并不关心是哪个实际类包含成员,通常只在基类和派生类共享同一名字时才需要注意。与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。可以使用作用域操作符访问被屏蔽的基类成员.设计派生类时,只要可能,最好避免与基类成员的名字冲突。

作用域与成员函数

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

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(); // calls Derived::memfcn

d.memfcn(); // error: memfcn with no arguments is hidden

d.Base::memfcn(); // ok: calls Base::memfcn

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

重载函数

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

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

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

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

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

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

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

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

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

纯虚函数

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

class Disc_item : public Item_base {

public:

double net_price(std::size_t) const = ;

};

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

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

// Disc_item declares pure virtual functions

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

Bulk_item bulk; // ok: Disc_item subobject within Bulk_item

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

容器与继承

我们希望使用容器(或内置数组)保存因继承而相关联的对象。但是,对象不是多态的,这一事实对将容器用于继承层次中的类型有影响。记住,将派生类对象复制到基类对象时,派生类对象将被切掉.

唯一可行的选择可能是使用容器保存对象的指针。这个策略可行,但代价是需要用户面对管理对象和指针的问题,用户必须保证只要容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证在容器消失时适当地释放对象。包装(cover)类或句柄类对这个问题更好更通用的解决方案。

C++ 中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。

句柄类与继承

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

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

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

// use counted handle class for the Item_base hierarchy

class Sales_item {

public:

// default constructor: unbound handle

Sales_item(): p(), use(new std::size_t()) { }

// attaches a handle to a copy of the Item_base object

Sales_item(const Item_base&);

// copy control members to manage the use count and pointers

Sales_item(const Sales_item &i):

p(i.p), use(i.use) { ++*use; }

~Sales_item() { decr_use(); }

Sales_item& operator=(const Sales_item&);

// member access operators

const Item_base *operator->() const { if (p) return p;

else throw std::logic_error("unbound Sales_item"); }

const Item_base &operator*() const { if (p) return *p;

else throw std::logic_error("unbound Sales_item"); }

private:

Item_base *p; // pointer to shared item

std::size_t *use; // pointer to shared use count

// called by both destructor and assignment operator to free pointers

void decr_use()

{ if (--*use == ) { delete p; delete use; } }

};

// use-counted assignment operator; use is a pointer to a shared use count

Sales_item& Sales_item::operator=(const Sales_item &rhs)

{

++*rhs.use;

decr_use();

p = rhs.p;

use = rhs.use;

return *this;

}

句柄类经常需要在不知道对象的确切类型时分配书籍对象的新副本。解决这个问题的通用方法是定义虚操作进行复制,我们称将该操作命名为 clone。

对于派生类的返回类型必须与基类实例的返回类型完全匹配的要求,但有一个例外。如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)。

class Item_base {

public:

virtual Item_base* clone() const

{ return new Item_base(*this); }

};

class Bulk_item : public Item_base {

public:

Bulk_item* clone() const

{ return new Bulk_item(*this); }

};

Sales_item::Sales_item(const Item_base &item):

p(item.clone()), use(new std::size_t()) { }
上一篇:Java集合系列:-----------03ArrayList源码分析


下一篇:老师说的都对 - Beta阶段博客作业