1. 面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。
在C++ 中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。在C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。
2. 继承
通过继承我们能够定义这样的类,它们对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。派生类(derivedclass)能够继承基类(baseclass)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性。最后,除了从基类继承的成员之外,派生类还可以定义更多的成员。
在C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
3. 动态绑定
动态绑定我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。在C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
4. 基类成员函数
保留字 virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字virtual。除了构造函数之外,任意非static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
5. 访问控制和继承
在基类中,public和private 标号具有普通含义:用户代码可以访问类的public 成员而不能访问private 成员,private成员只能由基类的成员和友元访问。派生类对基类的public 和private 成员的访问权限与程序中任意其他部分一样:它可以访问public 成员而不能访问private 成员。protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。可以认为protected 访问标号是private 和public 的混合:
- 像private 成员一样,protected成员不能被类的用户访问。
- 像public 成员一样,protected成员可被该类的派生类访问。
- 此外,protected还有另一重要性质:
- 派生类只能通过派生类对象访问其基类的protected 成员,派生类对其基类类型对象的protected 成员没有特殊访问权限。
6. 派生类
为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个基类,具有如下形式:
class classname:access-label base-class
这里access-label 是public、protected或private,base-class是已定义的类的名字。类派生列表可以指定多个基类。继承单个基类是为常见。
• 如果是公用继承,基类成员保持自己的访问级别:基类的public 成员为派生类的public 成员,基类的protected 成员为派生类的protected 成员。
• 如果是受保护继承,基类的public 和protected 成员在派生类中为 protected 成员。
• 如果是私有继承,基类的的所有成员在派生类中为private 成员。
例如,考虑下面的继承层次:
class Base {
public:
void basemem(); //public member
protected:
int i; // protectedmember
// ...
};
struct Public_derived: public Base {
int use_base() {return i; } // ok: derived classes can access i
// ...
};
structPrivate_derived : private Base {
int use_base() {return i; } // ok: derived classes can access i
};
无论派生列表中是什么访问标号,所有继承 Base 的类对Base 中的成员具有相同的访问。派生访问标号将控制派生类的用户对从Base 继承而来的成员的访问:
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 thederived class
// 派生访问标号还控制来自非直接派生类的访问:
struct Derived_fromPrivate : public Private_derived {
// error: Base::i isprivate in Private_derived
int use_base() {return i; }
};
structDerived_from_Public : public Public_derived {
// ok: Base::iremains protected in Public_derived
int use_base() {return i; }
};
7. 用作基类的类必须是已定义的
已定义的类才可以用作基类。如果已经声明了Item_base 类,但没有定义它,则不能用Item_base 作基类:
class Item_base; //declared but not defined
// error: Item_base mustbe defined
class Bulk_item :public Item_base { ... };
这一限制的原因应该很容易明白:每个派生类包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道它们是什么。这一规则暗示着不可能从类自身派生出一个类。
8. 任何可以在基类对象上执行的操作也可以通过派生类对象使用。
因为可以使用基类类型的指针或引用来引用派生类型对象,所以,使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型:基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象。无论实际对象具有哪种类型,编译器都将它当作基类类型对象。将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作
基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。
9. C++ 中的多态性
引用和指针的静态类型与动态类型可以不同,这是C++ 用以支持多态性的基石。
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。 从编写代码的角度看我们无需担心。只要正确地设计和实现了类,不管实际对象是基类类型或派生类型,操作都将完成正确的工作。 另一方面,对象是非多态的——对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。
只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。
在编译时确定非 virtual 调用
10. 覆盖虚函数机制
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:
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);
11. 为什么会希望覆盖虚函数机制?
最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
12. 继承与组合
继承层次的设计本身是个复杂的主题,已超出本书的范围。但是,有一个重要的设计指南非常基础,每个程序员都应该熟悉它。定义一个类作为另一个类的公用派生类时,派生类应反映与基类的“是一种(IsA)”关系。在书店例子中,基类表示按规定价格销售的书的概念,Bulk_item是一种书,但具有不同的定价策略。
类型之间另一种常见的关系是称为“有一个(HasA)”的关系。书店例子中的类具有价格和ISBN。通过“有一个”关系而相关的类型暗含有成员关系,因此,书店例子中的类由表示价格和ISBN 的成员组成。
13. 去除个别成员
如果进行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;
// ...
};
14. 默认继承保护级别
struct 和class 保留字定义的类具有不同的默认访问级别,同样,默认继承访问级别根据使用哪个保留字定义派生类也不相同。使用class 保留字定义的派生默认具有private 继承,而用struct 保留字定义的类默认具有public 继承:
class Base { /* ...*/ };
struct D1 : Base { /*... */ }; // public inheritance bydefault
class D2 : Base { /*... */ }; // private inheritance bydefault
15. 友元关系与继承
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
每个类控制对自己的成员的友元关系:
class Base {
friend class Frnd;
protected:
int i;
};
// Frnd has no accessto members in D1
class D1 : publicBase {
protected:
int j;
};
class Frnd {
public:
int mem(Base b) {return b.i; } // ok: Frnd is friend to Base
int mem(D1 d) {return d.i; } // error: friendship doesn't inherit
};
// D2 has no accessto members in Base
class D2 : publicFrnd {
public:
int mem(Base b) {return b.i; } // error: friendship doesn't inherit
};
16. 继承与静态成员
无论从基类派生出多少个派生类,每个static 成员只有一个实例。static 成员遵循常规访问控制:如果成员在基类中为private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问static 成员,也可以通过派生类访问static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
struct Base {
static voidstatmem(); // public by default
};
struct Derived : Base{
void f(constDerived&);
};
void Derived::f(constDerived &derived_obj)
{
Base::statmem(); // ok: Base defines statmem
Derived::statmem(); // ok: Derived in herits statmem
// ok: derivedobjects can be used to access static from base
derived_obj.statmem(); // accessed through Derived object
statmem(); // accessed through this class