《C++ Primer》之面向对象编程(一)
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。//动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
面向对象编程的关键思想是多态性(polymorphism)。在 C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。
- 继承
我们经常称因继承而相关联的类为构成了一个继承层次。其中有一个类称为根,所以其他类直接或间接继承根类。在书店例子中,我们将定义一个基类,命名为 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 类将定义该函数自己的版本。//继承类中只要实现基类中的虚函数double net_price(size_t)即可,因为继承类只对这个函数的功能进行改变,而book()只需继承下来使用即可。
- 动态绑定
动态绑定我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。
可以定义一个名为 print_total 的函数管理应用程序的这个部分。给定一个项目和数量,函数应打印 ISBN 以及购买给定数量的某书的总价。可以这样写函数
// calculate and print price for given number of copies, applying any discounts void print_total(ostream &os, const Item_base &item, size_t n) { 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_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++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
- 定义基类
// 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 };
Item_base 类定义了两个函数,其中一个前面带有保留字 virtual。保留字 virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字 virtual。除了构造函数之外,任意非 static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。基类通常应将派生类需要重定义的任意函数定义为虚函数。
- 访问控制和继承
在基类中,public 和 private 标号具有普通含义:用户代码可以访问类的 public 成员而不能访问 private 成员,private 成员只能由基类的成员和友元访问。派生类对基类的 public 和 private 成员的访问权限与程序中任意其他部分一样:它可以访问 public 成员而不能访问 private 成员。//public是因为派生类的产生而产生的,为继承所用
有时作为基类的类具有一些成员,它希望允许派生类访问但仍禁止其他用户访问这些成员。对于这样的成员应使用受保护的访问标号。protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。
- 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 和 public 访问级别反映了用户种类的这一分隔:用户只能访问 public 接口,类成员和友元既可以访问 public 成员也可以访问 private 成员。只有类本身和友元可以访问基类的 private 部分,派生类不能访问基类的 private 成员。定义类充当基类时,将成员设计为 public 的标准并没有改变:仍然是接口函数应该为 public 而数据一般不应为 public。被继承的类必须决定实现的哪些部分声明为 protected 而哪些部分声明为 private。希望禁止派生类访问的成员应该设为 private,提供派生类实现所需操作或数据的成员应设为 protected。换句话说,提供给派生类型的接口是 protected 成员和 public 成员的组合。
- 派生类
class classname: access-label base-class
派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员。
在书店应用程序中,将从 Item_base 类派生 Bulk_item 类,因此 Bulk_item 类将继承 book、isbn 和 price 成员。Bulk_item 类必须重定义 net_price 函数定义该操作所需要的数据成员:
// discount kicks in when a specified number of copies of same book are sold // the discount is expressed as a fraction used to reduce the normal price 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 类指出,它将重定义 net_price 函数但将使用 book函数 的继承版本。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。例如,Item_base 类可以定义返回 Item_base* 的虚函数,如果这样,Bulk_item 类中定义的实例可以定义为返回 Item_base* 或 Bulk_item*。
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。
- 派生类对象包含基类对象作为子对象
派生类对象由多个部分组成:派生类本身定义的(非 static)成员加上由基类(非 static)成员组成的子对象。可以认为 Bulk_item 对象由下图表示的两个部分组成。
C++ 语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列,因此, 此图是关于类如何工作的概念表示而不是物理表示。
- 派生类中的函数可以使用基类的成员
像任意成员函数一样,派生类函数可以在类的内部或外部定义,正如这里的 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;//price是public }
因为每个派生类对象都有基类部分,类可以访问共基类的 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 { /* ... */ };
每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。//这里D2是最底层的
- 派生类的声明
如果需要声明(但并不实现)一个派生类,则声明包含类名但不应该包含派生列表。例如,下面的前向声明会导致编译时错误:
// 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++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。要理解这一要求,需要理解在使用继承层次中某一类型的对象的引用或指针时会发生什么。
- 从派生类到基类的转换
因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象:
// function with an Item_base reference parameter double print_total(const Item_base&, size_t); Item_base item; // object of base type // ok: use pointer or reference to Item_base to refer to an Item_base object print_total(item, 10); // passes reference to an Item_base object Item_base *p = &item; // p points to an Item_base object Bulk_item bulk; // object of derived type // ok: can bind a pointer or reference to Item_base to a Bulk_item object print_total(bulk, 10); // passes reference to the Item_base part of bulk p = &bulk; // p points to the Item_base part of bulk
这段代码使用同一基类类型指针指向基类类型的对象和派生类型的对象,该代码还传递基类类型和派生类型的对象来调用需要基类类型引用的函数,两种使用都是正确的,因为每个派生类对象都拥有基类部分。
因为可以使用基类类型的指针或引用来引用派生类型对象,所以,使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型:基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象。无论实际对象具有哪种类型,编译器都将它当作基类类型对象。将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即,任何可以在基类对象上执行的操作也可以通过派生类对象使用。//基类类型的指针,只会限制指针对派生类对象的衍生的成员的使用,不会扩充基类对象使用它自身没有的(而在派生类中才有的)成员。基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。
- 可以在运行时确定virtual函数的调用
将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。
通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。例如,我们再来看 print_total 函数:
// calculate and print price for given number of copies, applying any discounts void print_total(ostream &os, const Item_base &item, size_t n) { 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 形参是一个引用且 net_price 是虚函数,item.net_price(n) 所调用的 net_price 版本取决于在运行时绑定到 item 形参的实参类型:
Item_base base; Bulk_item derived; // print_total makes a virtual call to net_price print_total(cout, base, 10); // calls Item_base::net_price print_total(cout, derived, 10); // calls Bulk_item::net_price
在第一个调用中,item 形参在运行时绑定到 Item_base 类型的对象,因此,print_total 内部调用 Item_base 中定义的 net_price 版本。在第二个调用中,item 形参绑定到 Bulk_item 类型的对象,从 print_total 调用的是 Bulk_item 类定义的 net_price 版本。
- 关键概念:C++中的多态
引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。//静态类型是指指针定义的类型,而动态类型是指它在运行时被绑定的类型
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
- 在编译时确定非virtual调用
不管传给 print_total 的实参的实际类型是什么,对 book 的调用在编译时确定为调用 Item_base::book。即使 Bulk_item 定义了自己的 book 函数版本,这个调用也会调用基类中的版本。非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。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(42);
这段代码强制将 net_price 调用确定为 Item_base 中定义的版本,该调用将在编译时确定。只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。例如,可以定义一个具有虚操作的 Camera 类层次。Camera 类中的 display 函数可以显示所有的公共信息,派生类(如 PerspectiveCamera)可能既需要显示公共信息又需要显示自己的独特信息。可以显式调用 Camera 版本以显示公共信息,而不是在 PerspectiveCamera 的 display 实现中复制 Camera 的操作。在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
- 虚函数与默认实参
像其他任何函数一样,虚函数也可以有默认实参。通常,如果有用在给定调用中的默认实参值,该值将在编译时确定。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。
- 公用、私有和受保护的继承
如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected 成员。
如果是受保护继承,基类的 public 和 protected 成员在派生类中为 protected 成员。
如果是私有继承,基类的的所有成员在派生类中为 private 成员。
例如,考虑下面的继承层次:
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 的类对 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 the derived class
Public_derived 和 Private_derived 都继承了 basemem 函数。当进行 public 继承时,该成员保持其访问标号,所以,d1 可以调用 basemem。在 Private_derived 中,Base 的成员为 private,Private_derived 的用户不能调用 basemem。
派生访问标号还控制来自非直接派生类的访问:
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 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实现中使用被继承但继承基类的部分并未成为其接口的一部分。
- 继承与组合
定义一个类作为另一个类的公用派生类时,派生类应反映与基类的“是一种(Is A)”关系。在书店例子中,基类表示按规定价格销售的书的概念,Bulk_item 是一种书,但具有不同的定价策略。
类型之间另一种常见的关系是称为“有一个(Has A)”的关系。书店例子中的类具有价格和 ISBN。通过“有一个”关系而相关的类型暗含有成员关系,因此,书店例子中的类由表示价格和 ISBN 的成员组成。
- 去除个别成员
如果进行 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; // ... };
- 默认继承保护级别
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 D3 : public Base { public: /* ... */ }; // equivalent definition of D3 struct D3 : Base { // inheritance public by default /* ... */ // initial member access public by default }; struct D4 : private Base { private: /* ... */ }; // equivalent definition of D4 class D4 : Base { // inheritance private by default /* ... */ // initial member access private by default
- 友元关系与继承
像其他类一样,基类或派生类可以使其他类或函数成为友元。友元可以访问类的 private 和 protected 数据。友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
每个类控制对自己的成员的友元关系:
class Base { friend class Frnd; protected: int i; }; // Frnd has no access to members in D1 class D1 : public Base { 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 access to members in Base class D2 : public Frnd { public: int mem(Base b) { return b.i; } // error: friendship doesn't inherit };
如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做:基类的友元对从该基类派生的类型没有特殊访问权限。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类和它的每一个派生类。
- 继承与静态成员
如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
struct Base { static void statmem(); // public by default }; struct Derived : Base { void f(const Derived&); }; void Derived::f(const Derived &derived_obj) { Base::statmem(); // ok: Base defines statmem Derived::statmem(); // ok: Derived in herits statmem // ok: derived objects can be used to access static from base derived_obj.statmem(); // accessed through Derived object statmem(); // accessed through this class
我们已经看到,每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类型引用到基类类型引用的自动转换,即,可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。
基类类型对象既可以作为独立对象存在,也可以作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是(作为独立对象存在时)一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。
相对于引用或指针而言,对象转换的情况更为复杂。虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但,没有从派生类型对象到基类类型对象的直接转换。
- 派生类到基类的转换
如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。但是,一般可以使用派生类型对象对基类对象进行赋值或初始化。对对象进行初始化和/或赋值以及可以自动转换引用或指针,这之间的区别是微妙的,必须好好理解。
- 引用转换不同于转换对象
我们已经看到,可以将派生类型的对象传给希望接受基类引用的函数。也许会因此认为对象进行转换,但是,事实并非如此。将对象传给希望接受引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。
- 用派生类对象对基类对象进行初始化或赋值
对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。
用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现:
class Derived; class Base { public: Base(const Derived&); // create a new Base from a Derived Base &operator=(const Derived&); // assign from a Derived // ... };
在这种情况下,这些成员的定义将控制用 Derived 对象对 Base 对象进行初始化或赋值时会发生什么。
然而,类显式定义怎样用派生类型对象对基类类型进行初始化或赋值并不常见,相反,基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参是基类类型的(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 类的复制构造函数或赋值操作符时,将发生下列步骤:
将 Bulk_item 对象转换为 Item_base 引用,这仅仅意味着将一个 Item_base 引用绑定到 Bulk_item 对象;将该引用作为实参传给复制构造函数或赋值操作符;那些操作符使用 Bulk_item 的 Item_base 部分分别对调用构造函数或赋值的 Item_base 对象的成员进行初始化或赋值;//就是用派生类对象中的基类部分给基类对象赋值 一旦操作符执行完毕,对象即为 Item_base。它包含 Bulk_item 的 Item_base 部分的副本,但实参的 Bulk_item 部分被忽略。在这种情况下,我们说 bulk 的 Bulk_item 部分在对 item 进行初始化或赋值时被“切掉”了。Item_base 对象只包含基类中定义的成员,不包含由任意派生类型定义的成员,Item_base 对象中没有派生类成员的存储空间。
- 派生类到基类转换的可访问性//什么是转换的可访问性
像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号。要确定到基类的转换是否可访问,可以考虑基类的 public 成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的。
如果是 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 申请在运行时进行检查。