• C++继承



    什么是继承?

    继承是代码复用的一种体现。在已有类的基础上进行扩展。
    那么,继承的形式是什么样的呢?

    class son:public father
    {}
    
    • 1
    • 2

    son叫做派生类/子类,father叫做基类/父类;public是继承方式。
    继承方式有3种,公有继承public,保护继承protected,私有继承private
    当没有写是什么方式继承的时候为私有继承。

    不同继承方式的访问限定

    我们知道类也有三种访问限定:公有public,继承protected,继承private
    那么基类不同的限定访问,在不同方式的继承之后,派生类会出现怎么样的访问限定。

    结论:

    • 基类的私有成员,无论是哪一种形式的继承,继承之后在派生类中也不能访问。
    • 基类任意一种访问限定符限定的成员,当为私有继承的时候,在派生类外面都不能进行访问。
    • 我们之前在讲类的时候说,类的protectedprivate的访问限定是一样的,在类外面都不能访问。而继承就体现了它用法,当为protected继承的时候,在派生类的里面是可以访问到基类的成员的。
    • 大多数的情况下都是公共继承的,因为继承之后在派生类的外面是可以访问的。
    基类 / 派生类public继承protected继承private继承
    public成员派生类public成员派生类protected成员派生类private私有成员
    protected成员派生类protected成员派生类protected成员派生类private私有成员
    private成员不可访问不可访问不可访问

    派生类给基类赋值

    • 派生类是可以给基类赋值的,可以是赋值给基类的指针,基类的引用,基类的对象。外面把这种操作叫做切片操作。
    • 基类不可以给派生类继承。

    下面来解释一下赋值的底层:

    • 测试的两个类
    class father
    {
    public:
    	int _a;
    };
    
    class son :public father
    {
    public:
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 赋值给对象
    int main()
    {
    	son x;
    	x._a = 1;
    	x._b = 2;
    
    	father y = x;
    	y._a = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    • 赋值给指针
    int main()
    {
    	son x;
    	x._a = 1;
    	x._b = 2;
    
    	father* y = &x;
    	y->_a = 0;//此时派生类中继承的基类的成员的数据会改变。
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 赋值给引用
    int main()
    {
    	son x;
    	x._a = 1;
    	x._b = 2;
    
    	father y = &x;
    	y._a = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image.png

    继承中的作用域

    不管是基类还是派生类它都有独立的作用域,都在该类域里面。

    • 当基类中的成员和派生类中的成员名相同的时候,此时基类中的该成员隐藏。要显示基类的类域才可以访问,没有显示类域默认访问派生类中的。
    • 注意:只要名字一样就构成隐藏。

    成员变量构成的隐藏

    class A
    {
    public:
    	int _a=10;
    };
    class B :public A
    {
    public:
    	int _a=20;
    };
    int main()
    {
    	B x;
    	cout <<"B中:" <<x._a << endl;
    	cout << "A中:" << x.A::_a << endl;//访问A中的_a要指定类域
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image.png
    成员函数构成的隐藏

    class A
    {
    public:
    	void f()
    	{
    		cout << "A" << endl;
    	}
    public:
    	int _a=10;
    };
    class B :public A
    {
    public:
    	void f()
    	{
    		cout << "B" << endl;
    	}
    public:
    	int _a=20;
    };
    int main()
    {
    	B x;
    	x.f();
    	x.A::f();
    	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

    image.png

    派生类中的默认成员函数

    在基类中,如果没有默认成员函数,我们必须在派生类中显示的写出。如果基类中有默认成员函数,当派生类中不显示调用的时候,会自动调用。

    • 对于构造函数,都会在初始化列表的时候自动调用基类的构造函数。
    • 对于析构函数,在对象销毁的时候,会自动调用基类的析构函数,在调用自身的析构函数,所以,不需要自己在析构函数里面显示的调用基类的析构函数。这样才能保证先析构派生类,再析构基类

    构造函数

    • 1.当基类有默认的构造函数的时候,可以只初始化派生类新的成员变量,也可以自己调用基类的默认构造,看自己的心情。
    • 2.当基类没有默认的构造函数的时候,必须自己要写构造函数调用基类的
    class A
    {
    	int _a;
    public:
    	A()
    	{
    		_a = 10;
    	}
    };
    class B :public A
    {
    	int _b;
    public:
    	B()
    	{
    		_b = 20;
    	}
    };
    int main()
    {
    	B x;
    	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
    class A
    {
    	int _a;
    public:
    	A(int a)
    	{
    		_a = 10;
    	}
    };
    //把A的构造函数改成不是默认构造的时候,那么上面的代码就会报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image.png
    此时B就应该在初始化列表显示的调用A中的构造函数。

    class B :public A
    {
    	int _b;
    public:
    	B()
    		:A(0)
    	{
    		_b = 20;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    拷贝构造,赋值运算符重载

    必须调用基类的。派生类自己的成员还是和类的拷贝构造,赋值运算符重载一样。
    赋值运算符要指定一下域

    class A
    {
    	int _a;
    public:
    	A(int a=0)
    	{
    		_a = a;
    	}
    	A(const A& x)
    	{
    		_a = x._a;
    	}
    	A& operator=(const A& x)
    	{
    		_a = x._a;
    		return *this;
    	}
    };
    class B :public A
    {
    	int _b;
    public:
    	B()
    		:A(0)
    	{
    		_b = 20;
    	}
    	B(const B& x)
    		:A(x)//把参数X传给A的拷贝构造,派生类可以传给基类,上面讲了。也可以传其他值哦。
    	{
    		_b = x._b;
    	}
    	B& operator=(const B& x)
    	{
    		A::operator=(x);//指明域
    		_b = x._b;
    		return *this;
    	}
    };
    int main()
    {
    	B x;
    	B y(x);
    	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
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    析构函数

    派生类的析构函数在对象销毁的时候,会自动调用基类的析构函数,所有不需要自己再手动调用基类的析构函数。

    class A
    {
    public:
    	~A()
    	{
    		cout << "A析构" << endl;
    	}
    };
    class B :public A
    {
    public:
    	~B()
    	{
    		cout << "B析构" << endl;
    	}
    };
    int main()
    {
    	B x;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    image.png

    如果在B的析构函数里面手动析构A,A::~A(),会把A析构两次,如果A中的成员是动态开辟的,将会产生野指针。
    image.png

    继承与友元

    友元是不能继承的——基类友元不能访问派生类的私有和保护成员。

    class B;
    class A
    {
    	friend void print(const A& x, const B& y)
    	{
    		cout << x._a << y._b << endl;
    	}
    protected:
    	int _a;
    };
    class B :public A
    {
    protected:
    	int _b;
    };
    int main()
    {
    	print(A(), B());
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image.png

    继承与静态成员

    对于基类的静态成员,无论它派生出多个派生类,所有继承体只要这么应该静态成员。

    菱形继承与菱形虚拟继承

    菱形继承

    • 什么是菱形继承?

    如下图:B继承了A,C也继承了A,D既继承了B也继承了C。这种继承关系就是菱形继承

    • 为什么会出现菱形继承?

    C++支持多继承

    • 菱形继承有什么缺点?

    二义性和数据冗余

    • 二义性

    如上图:对于D来说,含有2个A,当对他们访问的时候,不知道是哪一个

    • 数据冗余

    2个A都需要占用内存

    代码验证

    • 菱形继承
    class father
    {
    public:
    	int _f;
    };
    class son1:public father
    {
    public:
    	int _s1;
    };
    class son2 :public father
    {
    public:
    	int _s2;
    };
    class kunkun :public son1, public son2
    {
    public:
    	int _kk;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    测试二义性
    int main()
    {
    	kunkun x;
    	x._f = 1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们会发现它就会报错,因为存在二义性

    image.png

    冗余

    看它的内存分布:从它的内存分布上可以看出,father有两份,所有会冗余,也是当kunkun木有指定类域的时候不知道访问的哪一个。
    image.png

    菱形虚拟继承

    关键子virtual,可以解决二义性和冗余的问题。只需要son1``son2虚拟继承father即可。

    • 下面解释一下解决这两个问题的原理
    class father
    {
    public:
    	int _f;
    };
    class son1:virtual public father
    {
    public:
    	int _s1;
    };
    class son2 :virtual public father
    {
    public:
    	int _s2;
    };
    class kunkun :public son1, public son2
    {
    public:
    	int _kk;
    };
    int main()
    {
    	kunkun x;
    	x._s1 = 1;
    	x._s2 = 2;
    	x._f = 0;
    	x._kk = 3;
    	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

    看它的内存分布:
    image.png

    我们通过观察他们在内存中的存储,可以看出father的成员_f只有一份,放在了如图所示的地方。

    • 现在有个问题,对于son1,son2他们怎么找到_f在哪里的?

    通过观察上图,会发现son1,son2里面存了两个地址,0x005f7bdc,0x005f7be4
    我们看看地址指向哪里:
    image.png
    image.png
    16进制的14为20,这里的20是偏移量,相对_s1的偏移量,加上20,此时就是指向_f,下面那个含义也是一样的。

    继承与组合

    继承是is-a的关系——派生类是基类
    组合是has-a的关系——A类中有B类

    • 在既可以用继承,也可以用组合的地方,选择用组合,因为继承的情况下,基类的成员是完成暴露给派生类的,而组合只会提供相应的方法,不会暴露底层的实现。
  • 相关阅读:
    OpenCV从入门到精通实战(七)——探索图像处理:自定义滤波与OpenCV卷积核
    AI绘画API:提升艺术创作的效率和品质
    JS——常用内置对象
    使用docker安装db2
    SPI协议
    自动化测试的生命周期是什么?
    ML307R OpenCPU UDP使用
    数据仓库实验二:关联规则挖掘实验
    这应该是最全的机器学习模型可解释性的综述
    云计算认证有哪些?认证考了有什么用?
  • 原文地址:https://blog.csdn.net/m0_60598323/article/details/127969634