1: 类继承基本概念
面向对象编程基于的三个基本概念:数据抽象、继承和动态绑定。
在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序。
- 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;
- }
- virtual double net_price(std::size_t n) const //<需要继承
- {
- return n * price;
- }
- virtual ~Item_base() {}
- private:
- std::string isbn;
- protected:
- double price;
- };
在C++中,基类必须指出希望派生类重写哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
讨论过这些之后,可以看到我们的类将定义三个(const)成员函数:
1)非虚函数std::stringbook(),返回ISBN。由Item_base定义,Bulk_item 继承。
2)虚函数doublenet_price(size_t) 的两个版本(其中一个已经定义出),返回给定数目的某书的总价。Item_base类和Bulk_item类将定义该函数自己的版本。
2: 动态绑定
- void print_total(ostream &os,const Item_base &item,size_t n)
- {
- os << "ISBN: " << item.book()
- << "\t number sold: " << n << "\ttotal price: "
- << item.net_price(n) << endl;
- }
像其他类一样,基类也有其接口和实现的数据和函数成员:
- 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;
- }
- virtual double net_price(std::size_t n) const
- {
- return n * price;
- }
- //继承层次的根类一般都要定义虚析构函数
- virtual ~Item_base() {}
- private:
- std::string isbn;
- protected:
- double price;
- };
除了构造函数之外,任意非static成员都可以使虚函数。保留字virtual只在类内部的成员函数声明中出现,不能在类定义体外部出现在类定义体上。
基类通常应将派生类需要重定义的任意函数定义为虚函数。
在基类中,public和 private标号具有普通含义:
用户代码可以访问类的public成员而不能访问private成员,private成员只能由基类的成员和友元访问。派生类对基类的public和 private成员的访问权限与程序中任意其他部分一样:它可以访问public成员而不能访问private成员。
protected成员可以被派生类访问但不能被该类型的普通用户访问。
二、protected成员
可以认为protected访问标号是private和public的混合:
1)像private成员一样,protected成员不能被类的用户访问。
2)像public成员一样,protected成员可被该类的派生类访问。
此外,protected还有另一重要性质:
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象protected成员没有特殊访问权限。
就是说,在派生类中想访问基类的保护成员,只能通过派生类对象访问,不能通过积累对象访问
Bulk_item成员函数可以访问自己的protected成员&d的protected成员,但是不能访问基类&b的保护成员
- void Bulk_item::memfcn(const Bulk_item &d,const Item_base &b)
- {
- double ret = price;
- ret = d.price;
- ret = b.price; //Error
- }
派生类的提供者通常(但并不总是)需要访问(一般为private的)基类实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了附加的protected访问标号。
定义类充当基类时,将成员设计为public的标准并没有改变:接口函数应该为 public,数据一般应为private。被继承的类必须决定实现的哪些部分声明为protected而哪些部分声明为private。
希望禁止派生类访问的成员应该设为private,提供派生类实现所需操作或数据的成员应设为protected。
4: 派生类
为了定义派生类,使用派生类列表指定基类。派生类列表指定了一个或多个基类以及访问权限:
- class ClassName: access-label base-class
其中,以继承单个基类最为常见。然后访问标号[public,private,protected]决定了对继承成员的访问权限。如果想要继承基类的接口,则应该进行public派生。
派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员。一般而言,派生类只(重)定义那些与基类不同或扩展基类行为的方面。
从Item_base类派生Bulk_item类,Bulk_item类将继承book、isbn和price成员。Bulk_item类必须重定义net_price函数并定义该操作所需要的数据成员:
- class Bulk_item:public Item_base
- {
- public:
- double net_price(std::size_t ) const;
- private:
- std::size_t min_qty;
- double discount;
- };
每个Bulk_item对象包含四个数据成员:从Item_base继承的isbn和price,自己定义的min_qty和discount。
派生类一般会重定义所继承的虚函数,如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类型必须对想要重定义的每个继承成员进行声明。虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
【注释】
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做[但是建议这么做,可以提醒类的使用者该函数为virtual函数]。
- double Bulk_item::net_price(std::size_t cnt) const
- {
- if (cnt >= min_qty)
- {
- return cnt * (1 - discount) * price;
- }
- return cnt * price;
- }
因为每个派生类对象都有基类部分,类可以访问其基类的public和protected成员,就好像那些成员是自己的一样
用作基类的类必须是已经定义的
派生类的声明包含类名,而不包含派生类列表。
- class Bulk_item : public Item_base; //Error
- class Bulk_item; //OK
C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:
1)只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定。
2)必须通过基类类型的引用或指针进行函数调用。
因为每个派生类对象都包含基类部分,所以可以将基类类型的引用绑定到派生类对象的基类部分可以用指向基类的指针指向派生类对象:
- 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;
无论实际对象具有哪种类型,编译器都将它当做基类类型对象。将派生类对象当做基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即:任何在基类对象上执行的操作也可以通过派生类对象使用。
基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。
引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态的基石!
通过基类引用或指针调用基类中定义的函数时,我们并不知道执行函数的对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
【理解:】
只有通过引用或指针调用,虚函数才在运行时确定。
覆盖虚函数机制
如果希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,可以使用作用域操作符:
- Item_base *baseP = &derived;
- //显式调用Item_base中的版本,重载时确定
- double d = baseP -> Item_base::net_price(42);
【最佳实践】
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
覆盖虚函数机制常用在:派生类虚函数调用基类中的版本,在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作:
【小心地雷】
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了作用域操作符,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归!
对类所继承的成员的访问由基类中的成员访问级别和派生类列表中使用的访问标号共同控制。每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对继承的成员的访问!
基类本身指定对自身成员的最小访问控制。基类中的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是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。
7:、友元关系与继承
友元关系不能继承。基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
每个类控制对自己的成员的友元关系:
- 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
- }
- };
基类的友元对从该基类派生的类型没有特殊访问权限,同样,如果基类和派生类都需要访问一个类,那个类必须特定的将访问权限授予基类和每一个派生类。
8:继承与静态成员
如果基类定义了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();
- }