以下内容为笔者手打,望读者珍惜,如有转载还请注明。
第五章 继承与派生
 $5.1 继承与派生的概念
 $5.1.1 基本概念
     在C++中,当定义一个新的类B时,如果发现类B拥有某个已经写好的类A的全部特点,此外还有
 类A所没有的特点,那么就不必从头写类B,而是可以把类A作为一个基类("父类"),把类B称作
 “基类A”的一个“派生类”("子类").这样,就可以说从类A“派生”出了类B,也可以说类B“继承”了
 类A.
     派生类的成员函数不能访问基类的私有成员.
 在C++中,从一个类派生出来另一个类的写法如下:
 class 派生类名:继承方式说明符 基类名
 {
     ……
 };
     继承方式说明符可以是public(公有继承),private(私有继承),protected(保护继承)
 一般都用public,private和protected很少用到.
     派生类对象占用的存储空间大小,等于基类对象占用的存储空间大小加上派生类对象自身
 成员变量占用的存储空间大小.派生类对象中包含基类对象,而且基类对象的存储位置位于
 派生类对象新增的成员变量之前.
     在基类A和派生类B有同名成员func()(可以是成员变量,也可以是成员函数)的情况下,在
 派生类的成员函数中访问同名成员,或者通过派生类对象访问同名成员,除非特别指明,访问的
 就是派生类的成员,这种情况叫“覆盖”.对于派生类对象b,b.func()默认调用B类的func函数,
 如果要访问A类的func函数,写 b.A::func();
 如果p是B类的指针,写p->A::func();
     在派生类的同名成员函数中,先调用基类的同名成员函数完成基类部分的功能,然后再执行
 自己的代码去完成派生类的功能,这种作法非常常见.
     派生类成员和基类有同名的成员函数很常见,但是一般不会在派生类中定义和基类同名的
 成员变量.
     在DevC++中sizeof(string)=4,而在VSCode中sizeof(string)=32,这是由于不同的
 编译器所提供的类库对于string类有不同的实现方法,因此sizeof(string)在不同编译器上的
 值是不同的.
     对于guowei:program5.1.2中的sizeof(CStudent)如果在DevC++中运行理论值是13但
 实际值是16.这是因为计算机内部在CPU和内存之间传输数据都是以4字节(对于32位计算机)或
 8字节(对于64位计算机)为单位进行的.处于传输效率的考虑,应该尽量使对象的成员变量的地址
 是4或8的整数倍,这叫做对齐.
     对于CStudent类,编译器为每一个CStudent对象的char类型成员变量gender补齐三个字节,
 使得age成员变量能够对齐,这样CStudent对象就变成了16字节.
     VScode里面对象的成员变量对齐的默认值是8.
     思考题:如何实现string类,可以使得sizeof(string)=4?这样实现的string类如何才能
 在常数时间内求得string对象中的字符串长度.
     string类里面放一个指针(四个字节),指向动态分配的用来放字符串的存储空间.该存储空间
 初始位置用来放字符串的长度,后面放字符串本身:如string对象内有指针str,字符串是"Hello\0"str指向的空间
 第一个位置放长度5,然后依次H,e,l,l,o,\0
$5.2正确处理类的复合关系和继承关系
     在C++中,类和类之间有两种基本关系:复合关系和继承关系
     复合关系也称“has a”关系或“有”的关系,表现为封闭类,即一个类以另一个类的对象作为
 成员变量.如program5.1.2中CStudent类的例子,每个CStudent对象都“有”一个string类的
 成员变量name,表示姓名.
     继承关系也是“is a”关系或“是”的关系,即一个派生类对象也是一个基类对象.如program
 5.1.2中CUndergraduateStudent类(代表本科生)继承了CStudent类(代表学生).因为本科生
 也是学生,因此可以说,每一个CUndergraduateStudent类对象也是一个CStudent类的对象.
     在设计两个类的时候要注意,并非两个类有共同点,就可以让它们成为继承关系.让类B继承
 类A,必须满足“类B所代表的事物也是类A所代表的事物”.
比如点类CPoint:
 class CPoint{
     double x,y;//点的坐标
 };
 圆类 CCircle:
 class CCircle:public CPoint{
     double radius;//半径
 };这样的写法就是不正确的,因为圆不是点.
 应该写成:class CCircle{
     CPoint center;//圆心
     double radius;//半径
 };这样从逻辑上来说,每一个“圆”对象都包含一个“点”对象,这个点是圆心,就非常合理.
    如果写一个CMan类代表男人,然后又发现需要一个CWoman类代表女人,我们不能因为
 二者有共同之处就让CWoman 类从CMan类派生出来,因为“女人不是男人”;当然,让CWoman类
 包含CMan类成员对象也不合理,正确做法应该是综合CMan类和CWoman类的共同特点,编写一个
 CHuman类,然后让CWoman类和CMan类都从CHuman类派生出来.
     有些时候,复合关系也不一定通过封闭类实现,尤其当类A中有类B,类B中又有类A的情况.
 要小心循环定义.避免循环定义的方法是在一个类中使用另外一个类的指针,而不是对象作为
 成员变量.
 以“人和狗”为例:一个人最多养十只狗,一只狗只有一个主人.
 [owner&dog1.0]
 class CDog;
 class CMaster{
     CDog* dogs[10];
     int dogNum; //狗的数量
 };
 class CDog{
     CMaster m;
 };
     [1.0]的写法在CMaster对象中定义了一个CDog类的指针数组作为CMaster类的成员对象.
 指针就是地址,大小固定为4个字节.所以编译器编译到此不需要知道CDog类是什么样子的.这种写法
 的思想是:当一个CMaster对象养了一条狗时,就用new运算符动态分配一个CDog类的对象,然后
 在dogs数组中找一个元素,让它指向动态分配的CDog对象.
     [1.0]的不足之处在于,CDog对象中包含了CMaster对象.在多条狗的主人相同时,多个CDog
 对象中CMaster对象都代表同一个主人,这就造成了没有必要的冗余————一个主人用一个CMaster
 类对象足矣,没有必要对应多个CMaster类对象.而且,在一对多的这种情况下,当主人的个人
 信息发生变化时,就需要将与其对应的,位于多个CDog对象中的CMaster成员变量m都找出来修改.
 这毫无必要,而且非常麻烦.
     正确的写法应该是为“狗”类设一个“主人”类的指针成员变量,为“主人”类设一个“狗”类的
 对象数组.
 [2.0]
 class CMaster;
 class CDog{
     CMaster* pm;
 };
 class CMaster{
     CDog* dogs[10];
     int dogNum;
 };
$5.3 protected 访问范围说明符
     类的成员可以是私有成员,公有成员,还有保护成员.保护成员的可访问范围比私有成员大,
 比公有成员小.能访问私有成员的地方都能访问保护成员.保护成员扩大的访问范围表现在:基类
 的保护成员可以在派生类的成员函数中被访问.引入保护成员的理由是:基类的成员本来就是派生
 类的成员,因此对于那些出于隐藏的目的不宜设为公有,但又确实需要在派生类的成员函数中经常
 访问的基类成员,将它们设置为保护成员.
     需要注意的是,派生类的成员函数只能访问所作用的那个对象(即this指针指向的对象)的基类
 保护成员,不能访问其他基类对象的基类保护成员.
 class CBase{
     private:int nPrivate;
     public:int nPublic;
     protected:int nProtected;
 };
 class CDerived:public CBase{
     void AccessBase(){
         nPublic=1;
         //nPrivate=1;错:派生类不能访问基类的私有成员
         nProtected=1;//派生类的成员函数内部可以访问基类的保护成员
         CBase f;
         /*f.nProtected=1;错:派生类的成员函数只能访问所作用的那个对象(即this指针
         所指向的对象)的基类保护成员,不能访问其他基类对象的基类保护成员.这里面f不是
         AccessBase函数所作用的对象,所以不能访问其基类的保护成员.*/
     }
 };
 类的成员函数外部,不能访问对象的私有成员和保护成员.
 在基类中,一般都将需要隐藏的成员声明为保护成员而非私有成员.
$5.4 派生类的构造函数和析构函数
     派生类对象中包含基类对象,因此派生类对象在创建时,除了要调用自身的构造函数进行
 初始化外,还要调用基类的构造函数初始化其包含的基类对象.因此,程序中任何能生成派生类
 对象的语句都要说明其包含的基类对象是如何初始化的.如果对此不做声明,则编译器认为基类
 对象要用无参构造函数初始化————如果基类没有无参构造函数,则会导致编译错误.
     在执行一个派生类的构造函数之前,总是先执行基类的构造函数.
     和封闭类说明成员对象如何初始化类似(参见本文档355~420行),派生类说明基类对象如何
 初始化,也需要在构造函数后面添加初始化列表.在初始化列表中,要指明调用基类构造函数的
 形式.
    构造函数名(参数表):基类名(基类构造函数列表){
         ……
     }
    在执行一个派生类的构造函数之前,总是先执行基类的构造函数.
     派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数.
     从初始化和析构的先后次序来看基类和封闭类有点像:都是先初始化,后析构
    如果一个派生类对象是用默认复制构造函数初始化的,那么它内部包含的基类对象也要用
 基类的复制构造函数初始化.
$5.6 包含成员对象的派生类
     在派生类也是封闭类的情况下,构造函数的初始化列表不但要指明基类对象的初始化方式,
 还要指明成员对象的初始化方式.派生类对象生成时,会引发一系列构造函数调用,顺序是:先
 从上至下执行所有基类的构造函数,再按照成员对象的定义顺序执行各个执行各个成员对象的构造
 函数,最后再执行自身的构造函数;派生类对象消亡时,先执行自身的析构函数,然后按与构造
 相反的次序依次执行所有成员对象的析构函数,最后再自底向上依次执行各个基类的析构函数.
$5.7 公有派生的赋值兼容规则
 在公有派生的情况下,有以下三条赋值兼容规则.
 (1)派生类对象可以赋值给基类对象
 (2)派生类对象可以用来初始化基类引用
 (3)派生类对象的地址可以赋值给基类指针,亦即派生类指针可以赋值给基类指针
 反过来就不成立.不能把基类对象赋值给派生类对象.
 class A{};
 class B:public A{};
 int main()
 {
     A a;
     B b;
     a = b;  //派生类对象赋值给基类对象
     A& r = b;   //派生类对象初始化基类引用
     A* pa = &b; //派生类对象的地址赋值给基类指针
     B* pb = &b; 
     pa=pb;      //派生类指针赋值给基类指针
     return 0;
 }
$5.8基类与派生类指针的相互转化(参见guowei :program5.8.cpp)
     在公有派生的情况下,派生类的指针可以直接赋值给基类指针.但即便基类指针指向的是一个
 派生类的对象,也不能通过基类指针访问基类所没有而派生类中有的成员.
     基类的指针不能赋值给派生类的指针.但是通过强制类型转化,也可以将基类指针强制转化
 成派生类指针后再赋值给派生类指针.只是在这种情况下,我们需要保证被转化的基类指针本来
 就指向一个派生类对象,这样才安全,否则很容易出错.
 class Complex{
     double real,imag;
     public:
         Complex(double r,double i):real(r),imag(i){}
         operator double (){return real;}//重载强制类型转化运算符double
 };
 int main()
 {
     Complex c(1.2,3.4);
     cout<<(double)c<<endl;//输出1.2
     double n=2+c;//等价于double n = 2+ c.operator double(c)
     cout<<n;//输出3.2
 }
 编译器看到是哪个类的指针,就会认为要通过它访问哪个类的成员,编译器不会分析基类指针
 指向的到底是基类对象还是派生类对象.
基类引用也可以强制转化为派生类引用.将基类指针强制转化为派生类指针,或将基类引用强制
 转化为派生类引用,都有安全隐患.
C++提供了dynamic_cast强制类型转化运算符来判断这种转化是否安全(参见课本p267~268)
$5.9 私有派生和保护派生(p101)
$5.10 派生类和赋值运算符(参见guowei: program5.10.cpp)
     派生类的默认复制构造函数会调用基类的复制构造函数,以对派生类对象中的基类对象进行初
 始化.如果基类重载了复制运算符“=”而派生类没有重载“=”,那么在派生类对象之间赋值时,或者
 用派生类对象对基类进行赋值时,其中基类部分的赋值操作是调用被基类重载的“=”完成的.
以上内容为笔者手打,望读者珍惜,如有转载还请注明。










