C++——【继承】总结

C++语言三大特性:继承, 封装, 多态。
我们来看一下继承那点事儿。

概念——

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次 结构,体现了由简单到复杂的认知过程。

简单来说,继承最显著的特点就是:代码复用。比如有下面这样一个类:

class Student
{
public:
    void print()
    {
        cout << "number :" << _number << endl;
        cout << "name :" << _name << endl;
        cout << "score :" << _score << endl;
    }
protected:
    int _number;
    char* _name;
    int _score;
};

以上类可以用来打印学生的学号,姓名,成绩。那么如果现在我们要求在该类基础上同时打印学生专业,那可能有人就像我一样,重新写一个符合要求的类,然后再有要求再重新写,显然这样很麻烦。这个时候【继承】就派上用场了。

class UStudent:public Student
{
public:
    void print1()
    {
        cout << "major :" << _major << endl;
    }
protected:
    char * _major;
};

类UStudent继承自类Student,所以Student里的函数以及成员变量,都可以被UStudent调用,所以要实现专业类,在新类里添加上原类里没有的成员就可以了。

来看看继承的定义格式:

【继承】定义格式
class 派生类名:继承方式 基类名
{ 派生类新增的数据成员和成员函数 }

了解了定义格式,再来看继承类就简单啦。

在上面类名后面多了:public Student。其中public代表继承方式为公有继承,Student是基类名,它是派生类(子类)UStudent的基类(父类)。

定义格式

继承方式 访问限定符——

类成员的访问限定符有三种,即public, protected, private。同样的,继承关系限定符也有三种,即公有继承public, 保护继承protected, 私有继承private。
上面我们使用public来继承Student类以此来访问其变量 和函数,那么是不是这三种继承方式都可以随意的访问基类的成员呢?显然是不可以的,不然要一种继承方式不久ok了么。对于这几种访问关系我做了一个表格作为总结:

这里写图片描述

  • 看下面例子:
class Base
{
public:
    Base()
    {
        cout << "Baase()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void ShowBase()
    {
        cout << "_pri = " << b_pri << endl;
        cout << "_pro = " << b_pro << endl;
        cout << "_pub = " << b_pub << endl;
    }
private:
    int b_pri;
protected:
    int b_pro;
public:
    int b_pub;
};

class Derived : public Base
{
public:
    Derived()
    {
        cout << "Derived()" << endl;
    }
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
    void ShowDerived()
    {
        cout << "_d_pri = " << d_pri << endl;
        cout << "_d_pro = " << d_pro << endl;
        cout << "_d_pub = " << d_pub << endl;
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_pub;
};

这时候,我们在类外通过派生类Derived的对象只能访问基类Base中的公有成员变量b_pub和派生类自己的公有成员变量d_pub以及除构造函数以外的公有成员函数。
因为类的private成员是不能在类外直接访问的,同样的,如果派生类继承自基类,基类的私有成员被继承到派生类中,那么无论是在派生类中还是在类外它都是不能被访问的。
关于protected成员,我们之前的理解是和private成员一样,因为它也不能在类外直接访问。但是在派生类中基类的protected成员可以访问。比如,在ShowDerived()中打印基类b_pro的值,编译器就不会报错。但是如果想要打印b_pri的值编译器就会报错。

  • 需要注意的是:
  1. 使用关键字class继承时,系统默认的继承方式是private。使用关键字struct继承时,系统默认的继承方式是public。建议显式给出继承方式。
  2. 如果基类成员不想在类外直接被访问,而在派生类中被访问时,可将该成员定义为protected(保护成员)。可以说,保护成员就是为继承而生的。
  3. 不管哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,而基类的私有成员存在于派生类,但不可见,即不可访问。
  4. 实际应用中,多使用public继承,很少会使用protected和private。

同名隐藏——

如果派生类中的函数或变量名和基类中的相同,那么通过对象调用的时候,系统会优先调用派生类的,同时隐藏基类中对应的函数或变量。这种现象称为同名隐藏。同名隐藏与是否带有参数无关。例如,在上面的代码中,两个类的show函数都更名为Show(),其中基类的Show带有一个int类型的参数。那么 d.Show(10); 这行代码将会报错,因为派生类隐藏了基类中的Show(int),调用时只能调用派生类的Show();而派生类的函数是不接收参数的。

  • 一般不建议使用同名成员,容易混淆。
  • 继承体系中,基类和派生类是两个不同的作用域。

在派生关系中,如果派生类没有显式给出以下六个成员函数,系统会默认合成。

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值操作符重载
  • 取地址操作符重载
  • const修饰的取地址操作符重载

构造/析构函数调用顺序——

我们依旧拿上面例子来看

class Base
{
public:
    Base()
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
public:
    int b_pub;
};

class Derived : public  Base
{
public:
    Derived()
    {
        cout << "Derived()" << endl;
    }
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
public:
    int d_pub;
};

int main()
{
    Derived d;
    return 0;
}

这段代码运行结果如下:

Base()
Derived()
~Derived()
~Base()

那么是不是说明创建对象d的时候先调用基类的构造函数呢?其实不是的。我们来看看反汇编代码:

这里写图片描述

反汇编代码的倒数第二行call指令中可以看到,在派生类的构造函数里调用了基类的构造函数。这是因为派生类对象d包含有基类的成员,所以初始化时在派生类的构造函数的初始化列表调用了基类的构造函数对基类的成员进行初始化,执行完毕之后重新返回到派生类的构造函数继续执行。而销毁对象时,先调用派生类的析构函数,执行完毕之前调用基类的析构函数释放基类成员。

  • 派生类中到底要不要显式给出构造函数呢?

如果基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表。
如果基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
如果基类定义了带有形参表构造函数,派生类就一定定义构造函数。

也就是说决定权在基类啦,基类有,派生类就要显示给出。

对象模型——

即对象成员的布局模式。

看下面例子

class B
{
public:
    int _b;
};
class D:public B
{
public:
    int _d;
};

int main()
{
    D d;
    d._b = 1;
    d._d = 2;
    return 0;
}

当执行到return 0; ,在内存中查看对象d的地址会看到,d占了8个字节,其中前4个存放基类的成员_b,后4个存放自己的成员 _d,如下图;

对象模型图

  • 那么如果类中有多个成员的话,对象模型中各成员的顺序又是如何排列的呢?
class B
{
public:
    int _b;
    int _b1;
    int _b2;
};
class D :public B
{
public:
    int _d;
    int _d1;
    int _d2;
};

int main()
{
    D d;
    d._b = 1;
    d._b1 = 2;
    d._b2 = 3;
    d._d = 4;
    d._d1 = 5;
    d._d2 = 6;
    return 0;
}

直接来看结果吧!

这里写图片描述

这时候,我修改了各成员变量在主函数中的赋值顺序,发现结果和上面一毛一样~~~
于是,我又分别修改了基类和派生类中的成员变量的顺序,喏,看结果吧

这里写图片描述

结果说明,如果类中有多个成员的话,对象模型中变量的排列顺序按照变量在类中的声明顺序依次排列,基类在上派生类在下是大前提。与赋值先后顺序无关。

赋值兼容规则——(public继承)

基类对象不可以赋给派生类对象
派生类对象可以赋给基类对象
基类的指针/引用可以指向派生类的指针/引用
派生类的指针/引用不可以指向基类的指针/引用(可通过强制类型转换完成)

这里写图片描述

> 由图中可以看到,前两条赋值语句是ok的,而后两条语句,编译无法通过。因为当pd指向pb,pd对所指空间进行访问的时候,它并不知道此时所指空间已经缩小,所以当他访问某内存空间的时候,也许这个空间它是没有访问权限的。so...over. 语句`d = b;` 也是同样的道理。

友元函数能不能继承?

很显然不能。友元并不是类的成员函数,不能继承。即基类友元不能访问派生类的私有和保护成员。

继承的分类

这里写图片描述

以上所说的都是单继承。多继承和菱形继承虽然没有提到但基本思想都是一样的。

下面我们来看一个菱形继承的例子:

class B
{
public :
    int _b;
};
class C1:public B
{
public:
    int _c1;
};
class C2:public B
{
public :
    int _c2;
};
class D:public C1,public C2
{
public:
    int _d;
};

int main()
{
    D d;
    d.C1::_b = 1;
    d._c1 = 2;
    d.C2::_b = 3;
    d._c2 = 4;
    d._d = 5;
    cout << sizeof(d) << endl;
    return 0;
}

运行结果:对象d的大小是20 ,其中C1和C2都是8。
我们来看一下派生类D的对象模型。

这里写图片描述

由此可推出D的对象模型

这里写图片描述

  • 那么有一个问题,如果我们要通过对象d直接访问基类B的成员_b,类C1和C2中都有一个 _b, 访问的是哪一个呢?这就产生了二义性问题。系统会直接报错。

    • 解决这个问题的一个方法是把基类B的_b声明为static成员变量。再次求对象d的大小得到16。

基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

虚拟继承

虚拟继承是为了解决二义性问题和数据冗余而产生的。

  1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
  2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗

下面我们来看看虚拟继承的对象模型是什么样子的:

class B
{
public :
    int _b;
};
class C1 :virtual public B
{
public:
    int _c1;
};
class C2 :virtual public B
{
public :
    int _c2;
};
class D:public C1, public C2
{
public:
    int _d;
};

int main()
{
    D d;
    d._b = 1;
    d._c1 = 2;
    d._c2 = 4;
    d._d = 5;
    cout << sizeof(d) << endl;
    return 0;
}

上面代码中,对象d的大小为24。
这个数字也许有些出乎意料,来看看内存吧

这里写图片描述

观察发现,基类成员_b 只有一个且放在了最后4个字节。解决了二义性问题。而原来C1和C2中存放 _b 的空间分别用来放了一组像地址一样的数字。其实它就是地址啦。
C1和C2都继承自B,那么他们肯定是要能够访问基类B的成员 _b的,但此时这两个类中这个地址可以和 _b产生联系。在内存窗口中查看该地址的内容,如上图。C1中的地址里存放了数字0和20(图中为十六进制),C2存放了0和12。而C1和B正好相差20个字节,C2和B正好相差12字节。这样看来,这两组数字分别是相对于自身和基类成员的偏移量。
我通过给各个类中增加变量的方法来验证了一下这个猜想。果然……perfect

来看看它的对象模型

这里写图片描述

对比菱形继承的对象模型,我们可以发现,虚拟继承中,两个地址分别替换了_b,并且把 _b放到了最后4个字节。完美解决了菱形继承产生的二义性问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值