• C++ —— 继承



    前言: C++继承是很重要的,笔试面试都会常考。复用是C++中常用的,继承就是类层次上的复用。基类,派生类。派生类继承了基类,然后再进行扩展。这个过程承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。本文详细的剖析继承,灰常细节。


    1. 继承的定义

    1.1 继承的定义格式

    简单的举一个例子:

    • 学生的信息包括:姓名 ,年龄,学号 ;
    • 老师的信息包括:姓名,年龄,工号;

    发现两者有共同的部分:姓名,年龄

    所以我们可以定义一个基类包含姓名,年龄;使得学生类和教师类都继承此基类。

    #include
    #include
    using namespace std;
    //基类
    class person
    {
    public :
    	person(const char *name="zhangsan", int age = 0)
    		:_name(name),
    		_age(age)
    
    	{
    	}
    
    	string _name;
    	int _age;
    };
    // 派生类
    class student :public person
    {
    public:
    	student(const char* name="zhangsan", int age=0, int id=0)
    		:person(name,age),
    		_stu_id(id)
    	{
    	}
    
    private:
    	int _stu_id;
    };
    // 派生类
    class teacher :public person
    {
    public:
    	teacher(const char* name="zhangsan", int age = 0, int id = 0)
    		:person(name, age),
    		_tea_id(id)
    	{
    	}
    private:
    	int _tea_id;
    };
    
    • 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

    格式就是:
    在这里插入图片描述
    所以只需要有:

    • 冒号
    • 继承方式:public,protected,private
    • 继承的基类名

    1.2 继承的方式

    在这里插入图片描述
    派生类继承基类的方式有:public,protected,private。但是基类中又有public,protected,private的权限限制。所以事情就搞复杂了。比如:用public方式继承,对于基类中的public,protected,private成员,是如何继承的?是有对应的关系的。

    (1) 先看看public继承:

    class A
    {
    
    public:
    
    	void Print_pubilc()
    	{
    		cout << "pubilc ok" << endl;
    	}
    protected:
    	void Print_protected()
    	{
    		cout << "protected ok" << endl;
    	}
    private:
    	void Print_private()
    	{
    		cout << "private ok" << endl;
    	}
    
    	int _private_A;
    };
    
    class B :public A
    {	
    public:
    };
    
    • 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

    在main()函数中,调用看看情况:

    int main()
    {
    	B hh;
    	hh.Print_pubilc();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    可以看到pubilc继承,声明的对象可以pubic权限来访问继承的继承;protect继承下来的如何使用呢?可以在派生类中通过函数来使用,所以在上面的接口中加入一个函数:

    class B :public A
    {	
    public:
    	void Bprint_protected()
    	{
    		Print_protected();
    	}
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    再在main()函数中使用:

    int main()
    {
    	B hh;
    	hh.Print_pubilc();
    	hh.Bprint_protected();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    还有一个private成员,pubilc方式继承,会是什么情况?发现根本看不到继承下来的private成员,也就是不可见。


    (2) protected方式继承

    class B :protected A
    {	
    public:
    };
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    可以看到之前可以直接使用继承下来的public不能通过派生类对象直接使用了。那么protected方式继承后,基类的public成员会变为派生类的何种权限成员呢?猜测是protected权限,我们在派生类中使用函数看看是否能访问。

    class B :protected A
    {	
    public:
    	void Bprint_protected()
    	{
    		Print_protected();
    		Print_pubilc();
    	}
    };
    
    int main()
    {
    	B hh;
    	hh.Bprint_protected();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述
    可以看到,确实是变为protected权限了。基类的private成员还是不可见。


    (3) private方式继承

    派生类以private的方式继承了基类的public,protected权限成员后,它俩的权限都会变成private权限。当然在派生类中,依旧可以调用继承下来的这两种权限的成员,貌似和protected继承的区别不大。但是如果有孙子呢?再来一个派生类去继承上面的派生类,是不是就会导致从爷爷那里继承的成员变成不可见,无论以何种方式继承。


    (4) 继承方式的总结
    在这里插入图片描述

    • 基类的private成员,派生类无论以何种方式继承都是不可见
    • 基类的public,protected成员 :派生类的继承方式会和基类的成员权限进行比较,取低的那个权限进行保留(public>protected>private)。

    注意:实际上,一般使用public方式继承,那俩种继承方式很少用


    2. 基类和派生类的赋值转换

    2.1 赋值转换的使用

    基类和派生类可以互相赋值吗?其实也可以,派生类可以赋值给基类,但是基类不能够赋值给派生类。这个也好理解,派生类一般大于基类,兼容基类的内容,所以可以完成赋值;基类不能够完全兼容派生类的内容,所以不能够赋值给派生类。

    可以举个例子:

    用上面的student类举例:

    int main()
    {
    	person hh;
    	student ly("张三",21,2002040207);
    	
    	hh = ly;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们再可以试试 将hh赋值给ly;

    ly =hh; 
    
    • 1

    报错了:
    在这里插入图片描述

    如果是指针和引用呢?

    • 子类对象可以赋值给父类对象/指针/引用
    • 父类对象可以赋值给子类对象指针,不过需要进行强转;
    int main()
    {
    	person ll;
    	student ww("小李子", 20, 200202202);
    
    	// 子类给父类
    	person* ptr = &ww;
    	person& tr = ww;
    	/
    	// 父类给子类
    	student* str = (student*)&ll;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    2.2 赋值转换的原理

    上面的内容讲如何使用,接下来,来揭晓一下赋值转换的原理:

    子类对象可以赋值给父类对象/指针/引用,这种叫做切片或者切割。
    在这里插入图片描述

    • 用person指针去指向student,会切片,也就是只会指向继承了person的那一部分。
      在这里插入图片描述

    • person引用,也只会引用继承了的那部分。

    • student指针指向person也是可以的,不过需要强制,还有越界的风险。
      在这里插入图片描述
      这个用法也是会被用到的,不过用的少,毕竟有风险嘛。


    3. 构成隐藏,重写

    • 隐藏:基类和派生类,函数名相同就构成隐藏
    • 重写:加上virtual 前缀,函数的返回值,参数,函数名都相同,就构成重写

    在讲之前,先聊聊作用域:基类和派生类的作用域是不同的。所以基类和派生类中函数名相同,并不构成重载,而是构成隐藏,作用域不同。

    3.1 隐藏的使用

    隐藏并不是没有继承,还是继承下来的,我们都知道作用域中有个原则叫做就近原则。
    比如:全局变量,局部变量

    #include
    using namespace std;
    int a = 10;
    
    int main()
    {
    	int a = 0;
    	cout << a << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    认为是打印出哪个值呢?
    在这里插入图片描述
    很明显打印的是局部变量 a的值,这就是就近原则,当然隐藏也是同理,我们在调用子类的同名函数时默认调用的时子类的函数,而不是父类的函数,如果想要调用父类的函数需要指定作用域。回到上面的程序,想要打印全局变量a的值,指定作用域就好了。

    int main()
    {
    	int a = 0;
    	cout << :: a << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述


    我们来举一个 类 的例子:

    #include
    using namespace std;
    class A
    {
    public: 
    	void Print()
    	{
    		cout << "i am father" << endl;
    	}
    };
    class B : public A
    {
    public:
    	void Print()
    	{
    		cout << "i am child" << endl;
    	}
    };
    int main()
    {
    	B s;
    	s.Print();
    	
    	//指定域名就好了
        s.A::Print();
    	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

    在这里插入图片描述


    3.2 重写的使用条件

    重写就是对继承下来的函数进行了覆盖,也就说:子类重写实现的功能和父类的功能完全不一样,这个多用于多态。

    重写满足的条件有两个:

    1. 加上virtual 前缀
    2. 基类和子类的函数的返回值,参数,函数名都相同

    4. 默认成员函数的继承关系

    基类有默认成员函数,关键的是:构造,析构,拷贝构造,赋值重载。子类会继承父类的默认成员函数,所以一般情况下,子类是不需要管父类继承下来的成员的。

    4.1 子类需要写默认成员函数的情况

    什么情况下必须自己写?

    • 1、父类没有默认构造,需要我们自己显示写构造
    • 2、如果子类有资源需要释放,就需要自己显示写析构
    • 3、如果子类存在深拷贝问题,就需要自己实现拷贝构造和赋值解决深拷贝问题
    4.2 如何写默认成员函数
    • 父类成员调用父类的对应构造、拷贝构造、operator=和析构处理
    • 自己成员按普通类处理。

    我可以举一个例子:

    #include
    using namespace std;
    class A
    {
    public: 
    	A(int a=0)
    		:_a(a)
    	{}
    
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    
    	int _a;
    	
    };
    class B:public A
    {
    public:
     B(int b=0)
    	 :_b(b)
     {}
    
     ~B()
     {
    	 cout << "~B()" << endl;
     }
     int _b;
    	
    };
    int main()
    {
    	B nb;
    	
    	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

    我创建了一个B类型的对象,继承了A类型,但是A类型有默认的构造函数。不过我们来查看一下,析构的情况。看到了,会自动析构父类的资源。
    在这里插入图片描述

    (1) 第一种情况:父类没有默认的构造函数。

    #include
    using namespace std;
    class A
    {
    public: 
    	A(int a)
    		:_a(a)
    	{}
    
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    
    	int _a;
    	
    };
    class B:public A
    {
    public:
     B(int a,int b=0)
    	 :A(a),
    	 _b(b)
     {}
    
     ~B()
     {
    	 cout << "~B()" << endl;
     }
     int _b;
    	
    };
    int main()
    {
    	B nb(2);
    	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

    可以看到上面的A类没有默认的构造函数,需要显示的传参,所以子类B的构造,必须包括给A的传参。
    在这里插入图片描述
    (2) 第二种情况:子类有资源需要释放,就需要自己显示写析构

    子类有资源需要释放,当然需要手动的写析构,但我有个疑问,父类的析构需要我们显示的调用嘛?

    #include
    using namespace std;
    class A
    {
    public: 
    	A(int a)
    		:_a(a)
    	{}
    
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    
    	int _a;
    	
    };
    class B:public A
    {
    public:
     B(int a,int b=0)
    	 :A(a),
    	 _b(b)
     {
    	 arry = new int[a];
     }
    
     ~B()
     {
    	 delete[] arry;
    	 A::~A();
    	 cout << "~B()" << endl;
     }
     int _b;
    private:
    	int * arry;
    	
    };
    int main()
    {
    	B nb(2);
    
    	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

    我在B类中加了一个私有成员,它需要开辟空间,所以也需要手动写析构函数,于是我还显示的调用了父类的析构函数,但是有一个细节A::~A();,我指定了作用域才显示的调用了~A(),难道它是构成了隐藏嘛?

    看一看结果:
    在这里插入图片描述
    父类被析构了两次,这是明显错误的,所以我们根本不需要手动的调用父类的析构函数,我们在子类的析构之后会默认的调用的父类析构函数,这是c++的优化,为了控制析构的顺序,先析构子类的独有资源,再析构父类的资源(方式就是调用父类的析构函数)。

    解释一下:上面的细节,我指定了类域才能显示的调用父类的析构,这确实构成了隐藏,虽然父类和子类的析构名不同,但是编译时,都会被看作成一个函数名为delete(),这也是C++大佬的设计思想。

    (3) 第三种情况:子类存在深拷贝问题,就需要自己实现拷贝构造和赋值解决深拷贝问题

    对于深拷贝问题,只需要子类中,处理自己的深拷贝问题就可以了,父类去调用自己拷贝构造和赋值构造即可。

    #include
    using namespace std;
    class A
    {
    public: 
    	A(int a)
    		:_a(a)
    	{}
    
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    	A& operator =(const A& nb)
    	{
    		this->_a = nb._a;
    	}
    	int _a;
    	
    };
    class B:public A
    {
    public:
     B(int a,int b=0)
    	 :A(a),
    	 _b(b)
     {
    	 arry = new int[a];
     }
    
     ~B()
     {
    	 delete[] arry;
    	 cout << "~B()" << endl;
     }
    
     B(const B& nb)
    	 :A(nb),
    	 _b(nb._b)
     {
    	 arry = new int[nb._a];
         memcmp(arry, nb.arry, sizeof(nb.arry));
     }  
     B& operator =(const B& nb)
     {
    	 if (this != &nb)
    	 {
    		 A::operator=(nb);
    		 _b = nb._b;
    		 delete[] arry;
    		 arry = new int[nb._a];
    		 memcmp(arry, nb.arry, sizeof(nb.arry));	 
    	 }
     }
    
     int _b;
     
    private:
    	int * arry;
    	
    };
    int main()
    {
    	B nb(2);
    	B wb(nb);
    	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
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    在这里插入图片描述


    总结: 综上就一个原则: 继承父类的资源由父类管,子类的资源由子类管。


    5. 继承的友元函数

    这里简单的说一下:父类的友元函数不是子类的友元函数。
    所以父亲的朋友不是儿子的朋友,简单记一下就ok了。


    6. 继承的静态成员

    类的静态成员属于类,不是属于具体某个对象的,所以派生类也可以继承父类的静态成员,也可以修改静态成员,因为静态成员属于所有类对象以及继承了静态成员的派生类,都共用一份。

    静态成员的初始化,需要在类外,而且是基类来初始化,派生类是做不到初始化的。

    如果想要修改静态成员的值,可以封装到函数中进行修改。

    举个例子:

    #include
    using namespace std;
    class A
    {
    public:
    	static int _sta;
    	A()
    	{
    		_sta++;
    	}
    };
    int A::_sta = 0;
    
    class B :public A
    {
    };
    int main()
    {
    	B _a;
    	cout << _a._sta << endl;
    	B _b;
    	cout << _b._sta << endl;
    	B _c;
    	cout << _c._sta << endl;
    	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

    在这里插入图片描述


    7. 菱形继承

    菱形继承是什么?其实我们应该避免设计出菱形继承,这真是很复杂。但是面试会问,笔试会考就来讲讲。

    7.1 继承的方式

    (1) 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
    在这里插入图片描述
    (2) 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
    在这里插入图片描述
    (3) 菱形继承:菱形继承是多继承的一种特殊情况。
    在这里插入图片描述

    7.2 菱形继承的问题
    • 数据冗余:代码中继承了两份爷爷对吧,我只想要继承一份,但是却继承了两份,这是数据冗余

    • 二义性:这是因为继承了两份爷爷里面的内容,导致内容重名了。

    可以举个例子:

    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这就是个简单的菱形继承,我们来使用一下吧!

    (1) 二义性的测试

    int main()
    {
    	D dd;
    	dd._a = 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    报错了,对_a的访问不明确,不知道是在访问谁的_a,B和C都继承了A中的_a,然后由D全继承下来。
    在这里插入图片描述
    在这里插入图片描述
    解决方案: 可以指定作用域对吧,当然还有一个解决方案,一会讲。

    int main()
    {
    	D dd;
    	dd.B::_a=1;
    	dd.C::_a = 2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (2) 数据冗余

    为了展示,我将类A中的成员,变成一个数组:

    class A
    {
    public :
    	int _a[100000];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试一下大小:

    int main()
    {
    	D dd;
    	cout << sizeof(dd) << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到相当的大,我只想要一份的,但是给了两份(也有可能想要两份):
    在这里插入图片描述
    解决方案: 虚拟继承就可以解决数据冗余,在腰部也就是直接和爷爷继承的地方,搞成虚拟继承。
    在这里插入图片描述
    通过内存窗口以及监视窗口,我们来具体的观察一下这个实现过程:
    将类A还原:

    class A
    {
    public :
    	int _a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    int main()
    {
    	D dd;
    
    	dd.B::_a = 2;
    	dd.C::_a = 4;
    
    	dd._c = 1;
    	dd._b = 3;
    
    	dd._d = 16;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    取到D对象dd的地址,我们来查看一下:
    在这里插入图片描述
    在这里插入图片描述
    可以看到,这个菱形继承是这样继承的,以及储存的。
    先继承的B,所以B在上面;如果想让上面是C的话,可以调整一下继承的顺序。然后D的数据在最下面。
    在这里插入图片描述


    有了这个理解后,我们来正式解决问题:用虚继承,其余不变,方便查看

    class B : virtual public A
    {
    public:
    	int _b;
    };
    
    class C :  virtual public A
    {
    public:
    	int _c;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    我们来看看,虚继承之后的结果:
    在这里插入图片描述
    虚继承下来后,从爷爷A那里继承的数据,放到了最底部,本来在B处存_a,以及C处存的_a变成了不知道的数据。这样就解决了数据冗余。只保留了一份。

    那么现在就只剩下一个问题:本来在B处存_a,以及C处存的_a的位置,存的是什么?看的像一个地址,我们来试一试:
    在这里插入图片描述
    在这里插入图片描述

    可以看到:一个是12,一个是20,这是十六进制。保存的就是B和C距离_a偏移量,可以算算两地址的差值,发现就是一个是12。一个是20。


    8. 继承和组合

    多继承真的有的复杂,导致后面出来的语言都砍掉了多继承这个功能,现在来讲讲继承和组合的优缺点。

    • 组合:组合就是在类中直接复用某个类,直接包括此类的一个对象就行了
    • 继承:三种方式,九种组合来继承父类。

    一般情况下,优先考虑组合,因为组合对类的封装保护的很好,继承的话一定程度上破环了父类的封装。所以记住这个原则:能用继承也能用组合,那就用组合。


  • 相关阅读:
    Python模块导入出现ModuleNotFoundError: No module named ‘***’解决方法
    【ARKUI】HarmonyOS 如何设置桌面壁纸
    SDL2 简单介绍以及Windows开发环境搭建
    MySQL数据库介绍
    水质查询接口
    FreeRTOS学习 -- 任务
    The Sandbox Alpha 第三季游戏体验推荐|《爱是永恒》
    全志A33使用主线U-Boot方法
    调研主板,树莓派 VS RK3288板子,还是 RK的主板香,但是只支持 anrdoid 7系统,估计也有刷机成 armbian或者
    【CSS常见的选择器】介绍
  • 原文地址:https://blog.csdn.net/lyzzs222/article/details/126888898