面向对象编程
--定义基类和派生类[续]
四、virtual与其他成员函数
C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:
1)只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定。
2)必须通过基类类型的引用或指针进行函数调用。
1、从派生类到基类的转换
因为每个派生类对象都包含基类部分,所以可以将基类类型的引用绑定到派生类对象的基类部分可以用指向基类的指针指向派生类对象:
void print_total(const Item_base &item,size_t n); Item_base item; print_total(item,10); Item_base *p = &item; Bulk_item bulk; print_total(bulk,10); p = &bulk;
无论实际对象具有哪种类型,编译器都将它当做基类类型对象。将派生类对象当做基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即:任何在基类对象上执行的操作也可以通过派生类对象使用。
【释疑】
基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。
2、可以在运行时确定virtual函数的调用
当通过指针或引用调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型向对应的函数:
void print_total(const Item_base &item,size_t n) { cout << "ISBN: " << item.book() << "\t number sold: " << n << "\ttotal price: " << item.net_price(n) << endl; }
因为 item形参是一个引用且net_price是虚函数,item.net_price(n)所调用的net_price版本取决于在运行时绑定到item形参的实参类型:
Item_base base; Bulk_item derived; print_total(base,10); //Item_base::net_price print_total(derived,10); //Bulk_item::net_price
【关键概念:C++中的多态性】
引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态的基石!
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
【理解:】
只有通过引用或指针调用,虚函数才在运行时确定。
3、在编译时确定非virtual调用
非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。尽管item的类型是constItem_base 的引用,但是,无论在运行时item引用的实际对象是什么类型,调用该对象的非虚函数都将会调用Item_base中定义的版
本。
4、覆盖虚函数机制
如果希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,可以使用作用域操作符:
Item_base *baseP = &derived; //显式调用Item_base中的版本,重载时确定 double d = baseP -> Item_base::net_price(42);
【最佳实践】
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
覆盖虚函数机制常用在:派生类虚函数调用基类中的版本,在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作:
【小心地雷】
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了作用域操作符,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归!
5、虚函数与默认实参
像其他任何函数一样,虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。
//P482 习题15.8 struct base { base(string name = ""):baseName(name) {} string name() { return baseName; } virtual void print(ostream &os) { os << baseName; } private: string baseName; }; struct derived : public base { derived(string name = "",int intMem = 0):base(name),mem(intMem) {} void print(ostream &os) { base::print(os); //原来此处形成了无穷递归! os << "" << mem; } private: int mem; };
//习题15.9 理解下面这段程序 int main() { base ba("xiaofang"); base *p = &ba; p -> print(cout); cout << endl; derived de("xiaofang"); p = &de; p -> print(cout); //调用派生类print函数 }
五、公用、私有和受保护的继承
对类所继承的成员的访问由基类中的成员访问级别和派生类列表中使用的访问标号共同控制。每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对继承的成员的访问!
基类本身指定对自身成员的最小访问控制。基类中的private,只有基类和基类的友元可以访问该成员。派生类也不能访问其基类的private成员,当然也不能使自己的用户访问!
如果基类成员为public或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:
1)如果是公用继承public:基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
2)如果是受保护继承protected:基类的public和protected成员在派生类中为protected成员。
3)如果是私有继承private:基类的所有成员在派生类中为private成员。
class Base { public: void baseMem(); protected: int i; }; class Public_derived : public Base { int use_base() { return i; //OK } }; class Private_derived : private Base { int use_base() { return i; //OK } };
上例说明:无论派生列表中是什么访问标号,所有继承Base的类对Base的成员具有相同的访问权限;派生类访问标号将控制派生类的用户对从Base继承而来的成员的访问:
Base b; Public_derived d1; Private_derived d2; b.baseMem(); d1.baseMem(); //OK d2.baseMem(); //Error
派生类访问标号还控制来自非直接派生类的访问:
class Derived_from_Private : public Private_derived { int use_base() { return i; //Error } }; class Derived_from_Public : public Public_derived { int use_base() { return i; //OK } };
其实这也可以理解:因为在类Private_derived中它所继承来的所有东西都变成private的了,这就相当于派生类不能访问基类private成员一样了!而从Public_derived派生的类可以访问来自Base类的i,是因为该成员在Public_derived中仍为protected成员。
1、接口继承与实现继承
public派生类继承基类的接口:它具有与基类相同的接口。设计良好的类层次中,public派生类的对象可以用在任何需要基类对象的地方。【接口继承】
private和protected派生类继承基类的实现:它们“不继承”基类的接口[因为继承过来就相当于成为了派生了的内置实用函数了...],派生类在实现中被继承类但继承基类的部分并未成为其接口的一部分!【实现继承】
[迄今为止:最常见的继承形式是public!]
【关键概念:继承与组合】
定义一个类作为另一个类的公用派生类时,派生类应反映与基类的“是一种(IsA)”关系。在书店例子中,基类表示按规定价格销售的书的概念,Bulk_item是一种书,但具有不同的定价策略。
类型之间另一种常见的关系是称为“有一个(HasA)”的关系。书店例子中的类具有价格和ISBN。通过“有一个”关系而相关的类型暗含有成员关系,因此,书店例子中的类由表示价格和ISBN的成员组成。
2、去除个别成员
如果进行private/protected继承,则基类成员的访问级别在派生类中比在基类中更受限:
class Base { public: std::size_t size() const { return n; } protected: std::size_t n; }; class Derived : private Base { //... }; //测试 int main() { Derived de; std::size_t n = de.size(); //Error }
【注解】
派生类可以恢复继承成员的访问级别,但不能使得访问级别比基类中原来指定的更严格(?)或更宽松!
在上例中,size在 Derived中为private。为了使size在 Derived中恢复往日的地位[public],可以在Derived的public部分增加一个using声明。如下这样改变Derived的定义,可以使size成员能够被用户访问,并使n能够被从Derived派生的类访问:
class Derived : private Base { public: using Base::size; protected: using Base::n; };
正如可以使用using声明从命名空间使用名字,也可以使用using声明访问基类中的名字。
此时:
Derived de; std::size_t n = de.size(); //OK
3、默认继承保护级别
默认继承访问级别根据使用哪个保留关键字定义派生类也不相同:使用class定义的派生类默认具有private继承,而struct定义的类默认具有public继承:
class Base { /* ... */ }; struct D1 : Base //struct D1 : public Base { /* ... */ }; class D2 : Base //class D2 : private Base { /* ... */ };
注意:class与struct默认继承的唯一区别只是默认的成员保护级别和默认的派生保护级别!
class D3 : public Base { public: /* ... */ }; // equivalent definition of D3 struct D3 : Base { /* ... */ }; struct D4 : private Base { private: /* ... */ }; // equivalent definition of D4 class D4 : Base { /* ... */ };
【最佳实践】
尽管私有继承在使用class保留字时是默认情况,但这在实践中相对罕见[所以建议最好不要使用,因为最终阅读你的源码的不只是计算机,还有程序员!]。因为私有继承是如此罕见,通常显式指定 private是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。
六、友元关系与继承
友元关系不能继承。基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
每个类控制对自己的成员的友元关系:
class Base { friend class Frnd; protected: int i; }; //Frnd对D1没有特殊的访问权限 class D1 : public Base { protected: int j; }; class Frnd { public: int mem(const Base &obj) { return obj.i; //OK } int mem(const D1 &obj) { return obj.j; //Error } }; //D2对Base没有特殊的访问权限 class D2 : public Frnd { public: int mem(const Base &obj) { return obj.i; //Error } };
基类的友元对从该基类派生的类型没有特殊访问权限,同样,如果基类和派生类都需要访问一个类,那个类必须特定的将访问权限授予基类和每一个派生类。
七、继承与静态成员
如果基类定义了static成员,则整个继承层次只有一个这样的成员:无论从基类派生出多少个派生类,每个static成员只有一个实例。
static成员遵循常规访问控制:如果成员在基类中为private,则派生类不能访问它。假定可以访问该static成员[如public],则既可以通过基类访问static成员,也可以通过派生类访问static成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
struct Base { static void statMem(); }; struct Drived : public Base { void f (const Drived &); }; void Drived::f(const Drived &derived_obj) { Base::statMem(); Drived::statMem(); derived_obj.statMem(); statMem(); }
//P487 习题15.13 struct ConcreteBase { static std::size_t object_count(); protected: static std::size_t obj_count; }; struct C1 : public ConcreteBase { void f(const ConcreteBase &obj) { ConcreteBase::object_count(); ConcreteBase::obj_count; C1::object_count(); C1::obj_count; obj.object_count(); obj.obj_count; object_count(); obj_count; } }; struct C2 : public ConcreteBase { }; int main() { C2 obj; obj.object_count(); //obj不能直接访问obj_count成员,因为该成员是受保护成员,不能通过对象访问 obj.obj_count; //Error obj.ConcreteBase::object_count(); obj.C2::object_count(); }