程序设计与算法三~C++面向对象程序设计~北大郭炜MOOC学习笔记~第五章:继承与派生(新标准C++程序设计)

以下内容为笔者手打,望读者珍惜,如有转载还请注明。

第五章 继承与派生
$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)
    派生类的默认复制构造函数会调用基类的复制构造函数,以对派生类对象中的基类对象进行初
始化.如果基类重载了复制运算符“=”而派生类没有重载“=”,那么在派生类对象之间赋值时,或者
用派生类对象对基类进行赋值时,其中基类部分的赋值操作是调用被基类重载的“=”完成的.

以上内容为笔者手打,望读者珍惜,如有转载还请注明。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值