c++之继承--单继承(多继承)、虚继承

继承

                                        --前人栽树,后人乘凉

这里写图片描述
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手
段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样
产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现
了由简单到复杂的认知过程。简单来说,继承就是在获取“父辈”东西的基础上,有选择的增添自己的东西。

比如手机功能的更迭:
继承图例

继承的格式
继承的格式

继承的方式:
继承方式
总结: 这里写图片描述

  • 基类private成员在派生类中是不能被访问的,如果基类成员不想在
    类外直接被访问,但需要在派生类中能访问,就定义为protected。
    可以看出保护成员限定符是因继承才出现的
  • public继承是一个接口继承,保持is-a原则,每个父类可用的成员对
    子类也可用,因为每个子类对象也都是一个父类对象
  • protected/private继承是一个实现继承,基类的部分成员并非完全
    成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下
    不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继
    承。私有继承意味着is-implemented-in-terms-of(是根据……实
    现的)。通常比组合(composition)更低级,但当一个派生类需要访
    问基类保护成员或需要重定义基类的虚函数时它就是合理的
  • 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保
    护成员,基类的私有成员存在但是在子类中不可见(不能访问)
  • 使用关键字class时默认的继承方式是private,使用struct时默认的
    继承方式是public
    ,不过最好显示的写出继承方式
  • 在实际运用中一般使用都是public继承,极少场景下才会使用
    protetced/private继承

派生类默认成员函数
继承体系下,派生类中如果没有显示定义这六个成员函数,编译器则会合
成这六个默认的成员函数
【派生类对象的构造与析构】
继承的分类
继承的分类

这里只把<有无重写>放到虚拟继承不够准确,因为重写的要求如下:
1. 基类中被重写的函数必须为虚函数(先知道,就是在函数前面加虚拟virtual关键字)
2. 基类和派生类中的虚函数的原型的原型必须保持一致(返回值类型,函数名(参数列表)),即完全一样。那么普通继承里,自然也可以有重写。

class father
{
public:
    virtual void func1()
    {
        cout<<"father::func1()"<<endl;
    }
private:
    int _a;
};

class child:public father
{
public:
    //子类重写了父类的func1函数
    virtual void func1()
    {
        cout<<"child::func1()"<<endl;
    }
private:
    int _b;
};

所以①②两个分类之间的继承方式,可以组合,比如有一下组合方式:
普通单继承无重写、普通单继承有重写、虚拟单继承有重写、虚拟单继承无重写、
普通多继承无重写、普通多继承有重写、虚拟多继承有重写、虚拟多继承无重写

先来说下普通继承吧:

class father
{
public:
    father()
    {
        _a=10;
    }
    void func1()
    {
        cout<<"father::func1()"<<endl;
    }
private:
    int _a;

};

class child:public father
{
public:
    child()
    {
        _b=20;
    }
    void func2()
    {
        cout<<"child::func2()"<<endl;
    }
private:
    int _b;
};
void test()
{
    father a;
    child b;
    cout<<"sizeof(father):"<<sizeof(father)<<endl;
    cout<<"sizeof(child):"<<sizeof(child)<<endl;
}

int main()
{
    test();
    return 0;
}

运行结果:
运行结果
可见子类继承了父类的成员,再加上自己的成员变量,一共8字节。

而如果将成员函数改成虚函数:

class father
{
public:
    father()
    {
        _a=10;
    }
    virtual void func1()
    {
        cout<<"father::func1()"<<endl;
    }
private:
    int _a;

};

class child:public father
{
public:
    child()
    {
        _b=20;
    }
    virtual void func2()
    {
        cout<<"child::func2()"<<endl;
    }
private:
    int _b;
};
void test()
{
    father a;
    child b;
    cout<<"sizeof(father):"<<sizeof(father)<<endl;
    cout<<"sizeof(child):"<<sizeof(child)<<endl;
}

int main()
{
    test();
    return 0;
}

运行结果:
普通继承有虚函数

扫描二维码关注公众号,回复: 1949198 查看本文章

父类和子类都多了4个字节,查看内存:
这里写图片描述

接下来,介绍一位关键人物:“菱形继承
它包含了单继承和多继承

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;
};
void test1()
{
    D d;
    d._b=1;      //<--------
    d._c1=3;
    d._c2=4;
    d._d=5;
}

int main()
{
    test1();
    return 0;
}

这时候会报错:
访问报错

类D继承自类C1和类C2,而C1类和C2类都继承自类B,则类D中会两次继承B。而直接对_b赋值,编译器会不知道是给从C1中继承的_b赋值,还是给从C2中继承的_b赋值。
所以,为了解决这个问题,有如下做法

void test1()
{
    D d;
    d.C1::_b=1;   //在变量前面加上作用域限定
    d.C2::_b=2;   //明确访问的变量
    d._c1=3;
    d._c2=4;
    d._d=5;
}

而更关键的是,为了节省空间,同时解决上面继承中的二义性的问题,我们可以将C1、C2对B的继承定义为虚拟继承,而B就成了虚拟基类。

关于虚拟继承,我参考的是这篇博客【C++】简单介绍虚拟继承

虚拟继承与普通继承的区别:

1.书写形式上:虚拟继承要加虚拟关键字virtual

2.对象模型区别:虚拟继承要多四个字节空间,多的四个字节为偏移量表格的地址

如下,“菱形虚拟继承”:

class B
{
public:
    virtual void Funtest1()
    {
        cout << "B::Funtest1()" << endl;
    }
    virtual void Funtest2()
    {
        cout << "B::Funtest2()" << endl;
    }
public:
    int _b;
};
class C1:virtual public B
{
public:
    virtual void Funtest1()
    {
        cout << "B::Funtest1()" << endl;
    }
    virtual void Funtest3()
    {
        cout << "C1::Funtest3()" << endl;
    }

public:
    int _c1;
};
class C2 :virtual public B  
{
public:
    virtual void Funtest2()
    {
        cout << "B::Funtest2()" << endl;
    }
    virtual void Funtest4()
    {
        cout << "C2::Funtest4()" << endl;
    }

public:
    int _c2;
};
class D : public C1, public C2
{
    virtual void Funtest8()
    {}
    virtual void Funtest7()
    {}
public:
    int _d;
};

插叙:狗头哈哈
关于虚表的生成:

  • 基类虚表的生成:
    将类中的虚函数按照在类中的声明的先后次序添加到虚函数表中
  • <单继承–派生类 >
    如果派生类重写了基类的某个虚函数,使用派生类自己的虚函数,替换派生类虚表中相同偏移量位置 基类虚函数 ,然后再按照派生类特有的虚函数的声明次序,将其增加到虚函数表的最后(有些绕,仔细理解下)。
  • 特别注意:<多继承> 将派生类新增加的虚函数放置到第一张虚表的最后
    这样会方便调用(见下菱形虚拟继承的对象模型

虚函数的调用原理:

  1. 从对象的前4个字节取虚表的地址
  2. 传递this指针
  3. 从虚表中取对应的虚函数 :虚表地址 + 偏移量
  4. 执行当前虚函数

正题:狗头哈哈

菱形虚拟继承的内存分析:
菱形虚拟继承内存分析

菱形虚拟继承的对象模型:
菱形虚拟继承的对象模型

可以看到,最后只继承了一次基类B,对成员变量_b的访问也明确了。

猜你喜欢

转载自blog.csdn.net/qq_33279168/article/details/80952806