• 复杂的C++继承



    面向对象三大特性:封装继承和多态。封装在类和对象阶段我们已经学习过了,封装就是指不暴露底层实现细节在规范使用的前提下又方便了用户。本文将就继承这一特性展开讲解

    什么是继承

    继承这个词对我们来说应该不陌生,在现实生活中,你作为你父亲的继承者之一可以获得你父亲的财产,直接少奋斗N年。这里的继承也差不多是这个意思:在一个程序中有很多不同的类,但是这些类可能有共同的属性(成员变量或成员方法),为了避免多次对同样的成员方法和成员变量声明,C++就提出了继承。继承是类设计层次的代码复用。被继承的类就是父类或者基类,继承的类叫做子类或者派生类
    在这里插入图片描述

    当两者之间的关系是is a的关系时就可以使用继承,在上述用例中Person就是父类/基类,Student就是子类/派生类,在叫的时候最好搭配起来,不要叫基类和子类。

    public是公有继承方式,继承方式和访问限定符一样有三个:public,protected,private

    继承方式

    类成员/继承方式public继承protected继承private继承
    基类的public成员派生类的public成员派生类的protected 成员派生类的private 成员
    基类的protected 成员派生类的protected 成员派生类的protected 成员派生类的private 成员
    基类的private成员在派生类中不可见在派生类中不可见在派生类中不可

    父类的private成员在子类中是不可见的,不可见并不是因为没有被继承下来,只是不能被访问(除非使用共有函数)

    private和protected的主要区别就是在与继承,从有了继承以后我们说要尽量少用private(除非该成员是该类所特有的),因为继承的目的就是在于代码复用。

    在实际当中一般使用的都是公有继承,其他两种方式很少用。

    class定义的类默认继承是private,struct默认是public继承。但写明继承方式是一个好习惯

    赋值规则

    子类对象可以赋值给父类对象,父类的指针,父类的引用,并且中间没有临时变量的产生,这种赋值方式被称为切片/切割。(只能子类赋值给父类,而不能将父类赋值给子类,因为子类中有父类的那一块,但是父类中没有子类特有的成员。)

    在这里插入图片描述
    在这里插入图片描述

    切片仅限公有继承,因为如果是保活或者私有继承,父类中的成员权限在子类中改变了。

    继承中的作用域(隐藏)

    父类和子类都有各自独立的类域,如果它们有同名的成员函数(不是继承下来的),就会产生隐藏。所谓隐藏就是对在子类中只能看到子类自己定义的函数,在父类中也是一样。为什么不是函数重载?(函数重载要求在同一个作用域下)

    class Parent
    {
    public:
    	void test(double i)
    	{
    		cout << "you can not see me"<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述

    可以看到即使我传一个浮点数,也只能调用到子类中的test函数,因为b是一个子类对象。

    当然你需要通过子类对象调用父类的test函数也是可以的,因为子类继承了父类的test函数,只是被隐藏了,只要加上作用域限定符就可以调到父类的test函数:

    在这里插入图片描述

    不但同名的函数会隐藏,同名的成员变量也是会隐藏的

    class Child:public Parent
    {
    public:
    	void test()
    	{
    		cout << "这是一个隐藏示例" <<_a<< endl;
    	}
    private:
    	int _a = 3;
    	int _b;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ]

    如果子类中存在与父类同名的成员函数,会默认将父类的函数隐藏,除非使用域访问限定符指明类域访问。

    父类和子类的析构函数也会构成隐藏,因为底层将析构函数统一处理成了destructor函数

    父类和子类中不要定义同名的成员,因为这本身也容易让人混淆

    子类中的默认成员函数

    继承的子类与普通类不同的地方在于,子类中还有父类中的那一部分。

    构造函数和析构函数:对于内置类型不做处理,对于自定义类型调用它的构造和析构

    拷贝构造和赋值重载:对于内置类型按字节拷贝,对于自定义类型调用它的拷贝构造和赋值重载

    子类中父类的那一部分,要调用父类的默认成员函数来处理。

    class Child:public Parent
    {
    public:
    	Child(int y)
    		:Parent(_a)
    		,_b(y)
    	{}
        
        Child(const Child& c)
    		:Parent(c)//直接在初始化列表处传子类对象调用父类拷贝构造即可(切片)
    		, _b(c._b)
    	{}
        
        Child& operator=(const Child& d)
    	{
    		if (this != &d)
    		{
    			Parent::operator=(d);//这里必须要指明类域,否则会因为隐藏的原因造成死循环
    			_b = d._b;
    		}
    		return *this;
    	}
        
    private:
    	int _b;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    但是析构函数是一个例外,我们不能显示的调用父类的析构函数:

    class Parent
    {
    public:
    	Parent(int x)
    		:_a(x)
    	{}
    	~Parent()
    	{
    		cout << "~Person" << endl;
    	}
    protected:
    	int _a;
    };
    
    class Child:public Parent
    {
    public:
    	Child(int y)
    		:Parent(_a)
    		,_b(y)
    	{}
    
    	~Child()
    	{
    		Parent::~Parent();//前面有提到过:析构函数默认是隐藏,要调用父类的析构要指明类域
    		cout << "~Child" << endl;
    	}
    private:
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在这里插入图片描述

    这里父类的析构被调用了两次,因为这里没有清理资源否则是会报错的。

    父类的析构不用我们调用,在子类的析构结束时编译器会自动调用父类的析构,这里我们可以通过汇编来看一下:

    在这里插入图片描述

    需要自己写默认成员函数的情况

    1.父类没有默认构造函数,需要自己显示写构造

    2.子类存在浅拷贝,需要自己写赋值重载和拷贝构造

    3.子类存在资源要释放,需要显示写析构函数

    继承与友元及静态成员

    1.友元关系不能被继承

    2.静态成员虽然可以被继承,但是因为静态成员放在公共代码段,所以子类和父类共享静态的成员

    多继承

    一个子类继承一个父类是单继承,如果一个子类继承多个父类就是多继承。多继承本身没有错,因为一个类继承多个类也是一件很合理的事情。但是多继承给了别人犯错的机会,这个犯错就是菱形继承

    菱形继承

    在这里插入图片描述

    可以看到这个继承关系就像一个菱形一样,所以就叫菱形继承

    菱形继承的问题

    因为StudentTeacher都是Person的子类,也就是说这两个类中都有一份Person的成员变量;而Assistant作为这两个类的共同子类,也就继承了两份Person的成员,这会有数据冗余的问题,此外还会导致调用时的二义性(不知道你是要调用Student类中的Person成员,还是Teacher类中的Person成员)。

    void Test()
    {
    	// 这样会有二义性无法明确知道访问的是哪一个
    	Assistant a;
    	a._name = "peter";
    	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
    	a.Student::_name = "xxx";
    	a.Teacher::_name = "yyy";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    下面通过内存窗口来看一下菱形继承的内存结构:

    class A
    {
    public:
    	int _a;
    };
    
    class B :public A
    {
    public:
    	int _b;
    };
    
    class C :public A
    {
    public:
    	int _c;
    };
    
    class D :public B, public C
    {
    public:
    	int _d;
    };
    
    int main()
    {
    	D d;
    	
    	d._b = 1;
    	d._d = 2;
    	d.B::_a = 3;
    	d.C::_a =4;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在这里插入图片描述

    菱形虚拟继承

    存在数据冗余和二义性问题的根本原因就是同样的一份Person成员数据在Assistant中存在了两份,只要在继承的时候加上virtual关键字就可以解决这个问题。下面通过内存窗口来看一下菱形虚拟继承的内存结构:

    class A
    {
    public:
    	int _a;
    };
    
    class B :virtual public A
    {
    public:
    	int _b;
    };
    
    class C :virtual public A
    {
    public:
    	int _c;
    };
    
    class D :public B, public C
    {
    public:
    	int _d;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    使用虚拟继承以后,_a只存在一份,这也就没有了二义性的问题,但是继承于B类和C类的_b_c上方多了一串地址,再次要通过内存查找这串地址,发现这串地址之后的位置存放一个数字0x14,这个数字就是继承于B的成员到_a的偏移量,通过这个偏移量,对象d便能到d.B::_a。这样就解决了菱形继承成员冗余的问题。

    此时A被称为虚基类,继承的B和C类要找到A中的成员,就要通过虚基表中的偏移量来计算。

    在实际使用的时候,不要设计菱形继承,因为这是C++的一个大坑,跳进去就基本上爬不出来了哦。

    继承和组合

    继承是一个is a的关系,也就是说每一个子类都是一个父类;组合是has a的关系,也就说每一个B类中都有一个A类对象。下面是一个组合示例:

    // Car和BMW Car和Benz构成is-a的关系
    class Car
    {
    protected:
        string _colour = "白色"; // 颜色
        string _num = "陕ABIT00"; // 车牌号
    };
    class BMW : public Car
    {
    public:
    	void Drive() {cout << "好开-操控" << endl;}
    };
    class Benz : public Car
    {
    public:
    	void Drive() {cout << "好坐-舒适" << endl;}
    };
    // Tire和Car构成has-a的关系
    class Tire
    {
    protected:
        string _brand = "Michelin"; // 品牌
        size_t _size = 17; // 尺寸
    };
    class Car
    {
    protected:
        string _colour = "白色"; // 颜色
        string _num = "陕ABIT00"; // 车牌号
        Tire _t; // 轮胎
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    继承是一种白盒复用,子类对父类的实现细节可见,而且父类和子类的耦合性高,一旦父类做了修改就可能影响到子类的正常使用;组合是一种黑盒复用,无法窥探其内部实现的细节,且组合的耦合度低,只有Car的公有成员被修改才会影响到BMW的使用。

    如果一个类既是is a的关系又是has a的关系,优先使用组合。

  • 相关阅读:
    由浅入深理解latent diffusion/stable diffusion(3):一步一步搭建自己的stable diffusion models
    矢量图形编辑软件Boxy SVG mac中文版软件特点
    【已解决】微信小程序-苹果手机日期解析异常
    Redis6 十:使用Jedis连接Redis、使用redis完成手机验证码功能案例
    如何快速提升教育直播间人气
    帆软的数知鸟是一个什么东西
    三维扫描体数据的VTK体绘制程序设计
    ubuntu20.04安装repo
    认识网线上的各种参数标号
    Kotlin高仿微信-第29篇-朋友圈-发布作品(图片)
  • 原文地址:https://blog.csdn.net/m0_62633482/article/details/130848025