转换与继承
派生类到基类到自动转换
派生类指针 ----> 基类指针 --/-> 派生类指针
派生类对象 --/-> 基类对象 --/-> 派生类对象
派生类对象的引用或指针
可以自动转换
为基类
子对象的引用或指针。(因为派生类对象也是基类对象)没有
从基类引用或指针
到派生类
引用或指针的自动转换
。(一个基类对象可能是也可能不是一个派生类对象的部分)- 虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但对象转换的情况更为复杂,
没有从派生类型对象
到基类
类型对象的直接转换
。 - 可以使用
派生类对象的地址对基类类型的指针进行赋值或初始化。
- 可以使用
派生类型的引用或对象初始化基类类型的引用。
引用转换不同于转换对象
- 将对象传给希望接受
引用
的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制,并且转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象
。 - 将派生类对象传给希望接受基
类类型对象
(而不是引用)的函数时,形参的类型是固定的,在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参
。 - 一个是
派生类对象转换为基类类型引用
,一个是用派生类对象对基类对象进行初始化或赋值
,理解它们之间的区别很重要。
- 将对象传给希望接受
用派生类对象对基类对象进行初始化或赋值
- 对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符
- 用派生类对象对基类对象进行初始化或赋值时,有两种可能性。
-
基类显式定义了将派生类型对象复制或赋值给基类对象的含义,然而这种情况并不常见。[Code1]
-
基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符
,这些成员接受一个形参,该形参是基类类型的const引用
。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值。[Code2]- 将Bulk_item对象转换为Item_base引用,这仅仅意味着将一个Item_base引用绑定到Bulk_item对象。
- 将该引用作为实参传给复制构造函数或赋值操作符。
- 那些操作符使用Bulk_item的Item_base部分分别对调用构造函数或赋值的Item_base对象的成员进行初始化或赋值。
- 一旦操作符执行完毕,对象即为Item_base。它包含Bulk_item的Item_base部分的副本,但实参的 Bulk_item部分被忽略。
-
派生类到基类转换到可访问性
- 像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号。
- 如果是public继承,则用户代码和后代类都可以使用派生类到基类的转换。
- 如果类是使用private或protected继承派生的,则用户代码不能将派生类型对象转换为基类对象。
- 如果是private继承,则从private继承类派生的类不能转换为基类。
- 如果是protected继承,则后续派生类的成员可以转换为基类类型。
- 要确定到基类的转换是否可访问,可以考虑
基类的public成员是否访问
,如果可以,转换是可访问的,否则,转换是不可访问的。 - 无论是什么派生访问标号,派生类本身都可以访问基类的public成员,因此,派生类本身的成员和友元总是可以访问派生类到基类的转换。
详解
派生类到基类的转换
- 从
基类到派生类的自动转换是不存在的
- 原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那 么就可以试图使用该派生类对象访问不存在的成员。
基类指针或引用
实际绑定到绑定到派生类对象时,从基类到派生类的转换也存在限制- 编译器在编译时无法知道特定转换在运行时实际上是安全的。编译器确定转换是否合法,只看指针或引用的静态类型
- 如果
知道从基类到派生类的转换是安全的
,就可以使用static_cast
强制编译器进行转换。或者,可以用dynamic_cast
申请在运行时进行。
构造函数和复制控制
- 每个派生类对象由派生类中定义的
(非static)成员加上一个或多个基类子对象构成
,这一事实影响着派生类型对象的构造、复制、赋值和撤销。 - 当构造、复制、赋值和撤销派生类对象时,
也会构造、复制、赋值和撤销这些基类当子对象
。 - 构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。如果类
不定义自己的默认构造函数和复制控制成员,就将使用合成版本
。 - 只希望派生类使用的特殊构造函数,基类当构造函数应定义为protected。
派生类的构造函数
-
合成的派生类默认构造函数
- 派生类的合成默认构造函数与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
- 对于Bulk_item 类,
合成的默认构造函数
会这样执行:- 调用
Item_base的默认构造函数
,将isbn成员初始化空串,将price成员初始化为 0。 - 用常规变量初始化规则
初始化Bulk_item的成员
,也就是说,qty和discount成员会是未初始化的。
- 调用
-
定义默认构造函数[Code3]
- Code3中隐式调用Item_base的默认构造函数初始化对象的基类部分,再初始化Bulk_item部分的成员并执行构造函数的函数体。
- 向基类构造函数传递实参[Code4]
- 构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。
首先初始化基类,然后根据声明次序初始化派生类的成员。
- 构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。
- 在派生类构造函数中使用默认实参。[Code5]
只能初始化直接基类
复制控制和继承
- 如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。
被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值
。 - 如果派生类定义了自己的复制构造函数,该复制构造函数一般应
显式使用基类复制构造函数
初始化对象的基类部分,否则运行基类的默认构造函数,则新构造的对象将具有奇怪的配置:它的Base部分将保存默认值,而它的 Derived成员是另一对象的副本。[Code6] - 如果派生类定义了自己的
赋值操作符
,则该操作符必须对基类部分进行显式赋值
。[Code7] - 析构函数的工作与复制构造函数和赋值操作符不同,
派生类析构函数不负责撤销基类对象的成员。
继承情况下的类作用域
- 在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。
对象、引用或指针的静态类型决定了对象能够完成的行为。
- 当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的, 静态类型仍然决定着可以使用什么成员。[Code8]
- 与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
- 可以使用作用域操作符访问被屏蔽的基类成员,例如:
Base::mem
- 设计派生类时,只要可能,最好避免与基类成员的名字冲突。
- 可以使用作用域操作符访问被屏蔽的基类成员,例如:
- 基类和派生类中使用同一名字的成员函数,在派生类作用域中派生类成员将
屏蔽基类成员
。即使函数原型不同
,基类成员也会被屏蔽。[Code9]- 局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不重载基类中定义的成员。
- 通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。
- 派生类可以重定义所继承的0个或多个版本。
- 如果派生类重定义了重载成员,则通过派生类型
只能访问派生类中重定义的那些成员。
- 如果派生类想通过自身类型使用的重载版本,则派生类必须要么
重定义所有重载版本,要么一个也不重定义。
- 有时类需要仅仅重定义一个重载集中某些版本的行为,并且想要继承其他版本的含义,可以为重载成员提供
using声明
- 一个 using 声明
只能指定一个名字,不能指定形参表
,因此,为基类成员函数名称而作的using声明将该函数的所有重载实例加到派生类的作用域
。将所有名字加入作用域之后,派生类只需要重定义本类型确实必 须定义的那些函数,对其他版本可以使用继承的定义。
- 一个 using 声明
- 如果派生类重定义了重载成员,则通过派生类型
容器与继承
- 希望使用容器(或内置数组)保存因继承而相关联的对象。但是,对象不是多态的,这一事实对将容器用于继承层次中的类型有影响。[Code10]
- 若是基类容器则派生类会被截断,如是派生类容器则不能放入基类
- 唯一可行的选择可能是
使用容器保存对象的指针
。但代价是需要用户面对管理对象和指针的问题,用户必须保证只要容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证在容器消失时适当地释放对象。