C++ Primer(十五) 面向对象编程

来源:互联网 发布:点对点网络测速工具 编辑:程序博客网 时间:2024/05/02 04:32

15.2定义基类和派生类

15.2.1 定义基类

1. 基类成员函数

成员函数默认为非虚函数,对非虚函数的调用将在编译时确定。

保留字virtual只在类内部的成员函数声明中出现,不能再类定义体外部出现在函数定义山。

基类通常将派生类需要重定义的任意函数定义为虚函数。

2. 访问控制和继承

在基类中,public和private标号具有普通含义:用户代码可以访问类的public成员而不能访问private成员,private成员只能由基类的成员和友元访问。派生类对基类的public和private成员的访问权限与程序中任意其他部分一样:它可以访问public成员,但是不能访问private成员。

有时作为基类的类具有一些成员,它希望允许派生类访问但是禁止其他用户访问这些成员。对于这样的成员应该使用受保护的访问标号。protected成员可以被派生类对象访问但不能被该类型的普通用户访问。

15.2.2 protected成员

可以认为protected访问标号是private和public的混合:
·像private成员一样,protected成员不能被类的用户访问

·像public成员一样,protected成员可以被类的派生类访问

·派生类只能通过派生类对象访问基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。

  1: void Bulk_item::memfcn(const Bule_item &d,const Item_base &b)
  2: {
  3: double ret=price;    //可以 通过this->price
  4: ret=d.price;         //可以  通过 派生类对象访问      
  5: ret=b.price;         //  错误  不可以通过基类对象访问   
  6: }

15.2.3 派生类

为了定义派生类,可以使用类派生列表指定基类。类派生列表指定了一个或多个基类,具有如下形式:
class classname: access-label  base-class

这里access-label是public、protected或private,base-class是已定义的类的名字。

派生类继承基类的成员并且可以自己定义成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员

2. 派生类和虚函数

如果派生类没有重定义某个虚函数,则使用基类中定义的版本。

派生类型必须对重定义的每个继承成员进行声明。

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

一旦函数在基类中声明为虚函数,它就一直是虚函数,派生类无法改变该函数为虚函数的事实。

3. 派生类对象包含基类对象作为子对象

派生类对象由多个部分组成:派生类本身定义的(非static)成员加上由基类(非static)成员组成的子对象。

C++语言不要求编译器将对象的基类部分和派生部分连续排列。

4. 派生类中的函数可以使用基类的成员

因为每个派生类对象都有基类部分,类可以访问基类的public和protected成员,就好像这些成员是自己的成员一样。

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

已定义的类才能用作基类。

这一限制的原因是:每个派生类包含并且可以访问其基类的成员,为了使用这些成员必须知道他们是什么。

7. 派生类声明

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

class Bulk_item :public Item_base;

正确的前向声明为:
class Bulk_item;

class Item_base;

15.2.4 virtual与其他成员函数

C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员才能进行动态绑定;第二,必须通过基类类型的引用或者指针进行函数调用。

1. 从派生类到基类的转换

使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型:基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象。无论实际对象具有哪种类型,编译器都将它作为基类类型对象。

任何可以在基类类型上执行的操作也可以通过派生类对象使用。

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

2.可以再运行时确定virtual函数的调用

对象的实际类型可能不同于对象引用或指针的静态类型,这是C++中动态绑定的关键。

通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。

引用和指针的静态类型和动态类型不同,这是C++用以支持多态性的基石。

如果调用非虚函数,则无论实际对象是什么类型,都将调用基类类型所定义的函数。如果调用虚函数,则知道编译时才能确定调用哪个函数。

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

3. 在编译时确定非virtual函数调用

非虚函数总是在编译时刻根据调用的对象、引用或指针的类型确定。

4. 覆盖虚函数机制

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

派生类虚函数调用基类版本时,必须显式使用作用域操作符。

5.虚函数与默认实参

虚函数也有默认实参。通常,如果有用在给定调用中的默认实参,该值在编译时确定。

如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。

通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。

在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。

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

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

每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。

如果成员在基类中为private,则只有基类和基类的友元可以访问该成员。派生类不能访问基类的private成员,也不能使自己的用户访问那些成员。如果基类为protected或public,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:
·如果是公有继承,基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。

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

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

无论派生列表中是什么访问标号,所有继承基类的派生类对积累成员都具有相同的访问。派生类的访问标号控制将控制派生类的用户对从基类继承来的成员的访问

2. 去除个别成员

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

可以使用using声明访问基类中的名字。

3. 默认继承保护级别

使用class保留字定义的派生类默认具有private继承,而用struct保留字定义的类默认具有public继承

 

15.2.6 友元关系与继承

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

15.2.7 继承与静态成员

如果基类定义了static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。

假定可以访问static成员,则既可以通过基类访问static成员,也可以通过派生类访问static成员

15.3 转换与继承

可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。

没有从基类引用(或基类指针)到派生类引用(或派生类指针)的转换。

虽然可以使用派生类对象对基类类型的对象进行初始化或赋值,但没有从派生类型对象到基类类型对象的直接转换。

1. 引用转换不同于转换对象

可以将派生类对象传给希望接受基类引用的函数,实际上没有对象的转换。事实上将对象传给希望接受基类引用的函数时,引用直接绑定到该对象。

将派生类对象传递给希望接受基类类型对象的函数时,情况不同。该派生类对象的基类部分被复制到形参。

一个是将派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值。

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

对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。

用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种基类显示定义了将派生类对象复制或赋值给基类对象的含义。

然而显示定义用派生类型对象对基类类型进行初始化和赋值并不常见,相反,基类一般定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参是基类类型的(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从可用于从派生类对象对基类对象进行初始化或赋值:

Item_base item;

Bulk_item bulk;

Item_base item(bulk);

item=bulk;

用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部分被忽略

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

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

派生类本身的成员和友元总是可以访问派生类到基类的转换。

15.3.2 基类到派生类的转换

从基类到派生类的自动转换是不存在的,需要派生类对象不能使用基类对象。

编译器确定转换是否合法,只看指针或引用的静态类型。

15.4 构造函数和复制控制

当构造、复制、赋值和撤销派生类型对象时,也会构造、复制、赋值和撤销这些基类子对象。

构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。

15.4.1 基类构造函数和复制控制

某些类需要只希望派生类使用特殊的构造函数,这样的构造函数应定义为protected。

15.4.2 派生类构造函数

每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。

1. 合成的派生类默认构造函数

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

3.向基类构造函数传递实参

派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反,派生类构造函数通过将基类包含在构造函数初始化列表中类间接初始化继承成员

  1: class Bulk_item:public Item_base{
  2: public:
  3: Bulk_item(const string &book,double sales_price,size_t qty=0,double disc_rate=0.0):
  4: Item_base(book,sales_price),min_qty(qty),discount(disc_rate){}
  5: };

这个构造函数使用两个形参的Item_base构造函数初始化基类子对象。

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

5. 只能初始化直接基类

15.4.3 复制控制和继承

合成操作对对象的基类部分连同派生部分的成员一起赋值、复制或撤销,使用基类的赋值操作符、复制构造函数或析构函数对基类部分进行复制、赋值或撤销。

只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作。

1. 定义派生类复制构造函数

如果派生类显示定义自己的复制构造函数和赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。

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

如果省略基类复制构造函数,则调用基类的默认构造函数初始化对象的基类部分。

2.派生类赋值操作符

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

3.派生类析构函数

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

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

15.4.4 虚析构函数

删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存前清除对象。

处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。

如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指向对象类型不同而不同。

基类几乎总是需要析构函数,从而将析构函数设为虚函数。如果基类为了将析构函数设为虚函数而具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。

构造函数和赋值操作符不是虚函数

15.4.5 构造函数和析构函数中的虚函数

构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。

撤销派生类对象时,首先撤销他的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。

在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。编译器将对象的类型视为在构造函数和析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对待。

构造或析构期间的对象类型对虚函数的绑定有影响。

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

15.5 继承情况下的作用域

每个类都保持着自己的作用域,在该作用域中定义了成员名字。在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。

15.5.1 名字查找在编译时发生

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

例如可以给Disc_item类增加一个成员,该成员返回一个保存最小数量和折扣价格的pair对象:

class Disc_item :public Item_base{

public:
pair<size_t ,double> discount_policy() const

{

……………………

………………………..

}

};

只能通过Disc_item类型或Disc_item派生类型的对象、指针或引用访问discount_policy;

Bulk_item bulk;

Bulk_item *bulkp=&bulk;

Item_base *itemp=&bulk;

bulk->discount_policy();//正确

itemp->discount_policy();//错误

通过itemp访问是错误的,因为基类类型的指针(引用或对象)只能访问对象的基类部分,而基类中没有要调用的成员函数。

15.5.2 名字冲突与继承

与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。

使用作用域操作符访问被屏蔽的成员

使用作用域操作符访问被屏蔽的基类成员

15.5.3 作用域与成员函数

在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。

一旦找到了名字,编译器就不再继续查找了。

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

重载函数

如果派生类中重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。

派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供using声明。一个using声明只能指定一个名字,不能指定形参表,因此,为基类成员函数名称而作的using声明将该函数的所有重载实例加到派生类的作用域。

15.5.4 虚函数与作用域

如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。会屏蔽该函数

名字查找与继承:

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

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

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

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

15.6 纯虚函数

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

class Disc_item :public Item_base{

public:
double net_price(size_t)const =0;

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

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

 

}

0 0
原创粉丝点击