• C++ 继承详解


    写在前面

    在谈着这个之前,我们需要先说说C++的几大特性,封装继承,多态…注意,实不置这三种,只不过他们是基础罢了,大家面试的时候注意一点.我们已经学过了封装,今天就开始继承吧,我们最好按照简单的学习来,这里的语法可能有点难,但是我们用的时候一定要偏简单一点.


    继承

    说实话,C++的那些大佬也考虑了很多方式,把继承搞得很复杂,管是继承方式就有三种,所以后面的语言尽力把这个知识点给简化了,我们学习C++确实需要些时间来思考.多的不说,现在看看继承究竟是什么.

    什么是 继承

    我查了一些资料,里面对继承的概念简述的还是比较详细的.

    继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。(来源:维基百科).

    我举一个不太恰当的例子,当张三的父亲离世后,张三继承了父亲的遗产,这里张三就是"子类",其父亲便是"父类",张三拥有父亲的遗产,但是除此之外他可能也有自己的财富。

    为何要 继承

    我们可以举一个例子来帮助我们理解,假设我们要做一个学校人员管理系统,在一个学校里面是存在老师,学生…角色的,我们要是给每一个角色都封装一个类,想想每一个类里面都存在名字,年龄…等相同的成员变量,想想都头疼,假如我们把这个相同的属性拿出来,单独作为一个类,让其他的类继承它不就可以了吗.这就是继承的作用,代码复用,避免重复造轮子.

    image-20220718154200231

    如何 继承

    C++的继承可以说是让人头疼,大佬考虑的是在是太复杂了,继承方式有3种,每一种继承方式对于不同访问修饰限定符有不一样.我们需要来仔细看看.我们先等会再说继承的方式,先来看看.

    class Person
    {
    public:
    	void Print()
    	{
    		cout << "name:" << _name << endl;
    		cout << "age:" << _age << endl;
    	}
    protectedd:
    	string _name = "peter"; // 姓名
    	int _age = 18; // 年龄
    };
    
    class Student : public Person
    {
    protectedd:
    	int _stuid; // 学号
    };
    
    int main()
    {
    	Student stu;
    	stu.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

    image-20220718154620199

    继承了父类的什么

    这里我现给大家一个不恰当的结论,可以这么说,子类继承了父类的成员函数和成员变量.这里面也是存在很大的问题的.

    class Person
    {
    public:
    	void Print()
    	{
    		cout << "name:" << _name << endl;
    		cout << "age:" << _age << endl;
    	}
    public:
    	string _name = "peter"; // 姓名
    	int _age = 18; // 年龄
    };
    
    class Student : public Person
    {
    protectedd:
    	int _stuid; // 学号
    };
    
    int main()
    {
    	Student stu;
    	stu._name = "张三";
    	stu._age = 20;
    	stu.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

    image-20220718155037422

    继承方式

    前面我就说了,看到C++的继承方式我就感到一镇头疼,要知道我们访问修饰限定符也是存在三种的,这一计算就是九种情况.这个我们很多人都带来了巨大的困难.

    image-20220718160857629

    image-20220718160419992

    类成员/继承方式public继承protected继承private
    public 修饰子类 public 成员子类 protected成员子类 private成员
    protected 修饰子类 protected成员子类 protected成员子类 private成员
    private修饰在派生类中不可见在派生类中不可见在派生类中不可见

    大家想不要慌,这九种情况还是很好区分的,我们可以得到下面的两条结论.

    • private 成员 无论是什么继承方式 在 子类种不可见
    • 其余的成员是 父类成员与继承方式相比较 权限较小的那一个 权限比较 public > protected > private

    关于这个情况,我们不用担心,一般都是public继承,父类里面大多是protected成员,很少使用其他的.

    不可见 VS 没有继承

    我们需要看看究竟什么是不可见,什么是没有继承.没有继承可以了解,这里主要看看什么是不可见.

    所谓的不可见是你在子类中无法直接访问这个类型,但是有确确实实的继承了.

    image-20220718164959068

    image-20220718164841142

    那么这里就有一个问题了,我们是不是可以间接的去访问这个成员啊,是的,我们可以通过函数来访问,这里就不和大家分享了,下去有兴趣的话可以自己试试.

    继承特性

    对于继承,父子类之间具有很多的特性,其中比较关键的有两个。

    • 切片
    • 隐藏

    切片

    我们可以这么认为,父类是可以接受子类的类型的,这就像一个水到渠成的事,不是什么类型的转化,类似一种父亲可以教导孩子,这也可以认为是向上调整.这也是后面多态的基础.

    派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去 .

    我们可以看看下面的例子.

    image-20220719105058340

    class Person
    {
    protected:
    	string _name; // 姓名
    	string _sex; // 性别
    	int _age; // 年龄
    };
    class Student : public Person
    {
    public:
    	int _No; // 学号
    };
    
    int main()
    {
    	Person per;
    	Student stu;
    	per = stu;  
    
    	Person& p = stu;
    	Person* p = &stu;
    	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

    这里要和大家提一下,这个切片是有要求的,子类继承父类的方式必须是pubilic继承,其他的方式是不可以的.

    image-20220719111702923

    赋值

    我分别说一下它们的情况,里面有一点细节要来分享一下.

    对于直接把子类赋值给父类,这是会调用父类的赋值函数,编译器会自动把从属父类的内容那一部分给切片了,回去赋值给父类对象.

    class Person
    {
    public:
    	void operator=(Person& per)
    	{
    		cout << "void operator=()" << endl;
    	}
    protected:
    	string _name; // 姓名
    	string _sex; // 性别
    	int _age; // 年龄
    };
    class Student : public Person
    {
    public:
    	int _No; // 学号
    };
    
    int main()
    {
    	Person per;
    	Student stu;
    	per = stu;
    	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

    image-20220719105632061

    引用

    如果是用父类去引用子类的对象,就相当于给子类对象里面的属于父类的取了一个别名.

    class Person
    {
    public:
    	string _name = "张三"; // 姓名
    	string _sex = "男"; // 性别
    	int _age = 18; // 年龄
    };
    class Student : public Person
    {
    public:
    	int _No; // 学号
    };
    
    int main()
    {
    	Student stu;
    	Person& per = stu;
    	per._age++;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220719110920372

    指针

    指针更好理解,指针指向的就是子类当中属于父类的那些东西.

    int main()
    {
    	Student stu;
    	Person& per = stu;
    	Person* p = &stu;
    
    	cout << stu._age << endl;
    
    	per._age++;
    	cout << stu._age << endl;
    
    	p->_age++;
    	cout << stu._age << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220719111321474

    子类可以接受父类吗

    这个普通的方法是不可以的,在Java中这叫线下转型,但是C++里面还不太性,但是通过指针可以,这里先按下不表,后面多态的时候再和大家分享.

    隐藏

    隐藏也是C++里面一个重要的概念,大家都知道语言里面是有作用域这个概念的,类也有类域,同一个类里面不能定义同名的成员变量,那么父类和子类是两个个类域,这里就可以定义一样的变量了,编译器优先使用子类的变量,这就是隐藏,其中成员函数也是如此.

    子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定
    义。
    (在子类成员函数中,可以使用 基类::基类成员 显示访问

    这里给几个结论

    • 子类成员将屏蔽父类对同名成员的直接访问
    • 成员函数的隐藏,只需要函数名相同就构成隐藏

    成员变量

    我们先来看看成员变量,主要看看如何访问父类的同名的变量该如何访问.

    class Person
    {
    protected:
    	string _name = "小李子"; // 姓名
    	int _num = 111; // 身份证号
    };
    
    class Student : public Person
    {
    public:
    	void Print()
    	{
    		cout << " 姓名:" << _name << endl;
    		cout << " 身份证号:" << Person::_num << endl;  // 调用  父类 里面的隐藏的 变量
    		cout << " 学号:" << _num << endl;
    	}
    protected:
    	int _num = 999; // 学号
    };
    
    int main()
    {
    	Student stu;
    	stu.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

    image-20220719112900524

    这里我也给一个结论,如果我们要访问一个成员变量,编译器优先调用子类里面的,如果子类里面不存在,那么就去父类里面找,要是我们确实想想用父类里面的,就直接在用类域来声明.

    成员函数

    现在变量已经说完了,我们现在可以谈谈函数的隐藏了,再谈隐藏之前,我们先问问一个问题,fun()和fun(int i)构成什么关系?记住这个一定不是函数重载,函数重载是需要在同一个作用域的.这是构成了隐藏,成员函数的隐藏,只需要函数名相同就构成隐藏.

    class A
    {
    public:
    	void fun()
    	{
    		cout << "func()" << endl;
    	}
    };
    class B : public A
    {
    public:
    	void fun(int i)
    	{
    		cout << "func(int i)->" << i << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20220719114019643

    子类的默认成员函数

    6个默认成员函数, **”**的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?这些默认函数确实有点问题,里面是比较困难的.这里我们先来和大家提出一个易于下面的理解的想法.我们把子类里面继承的父类作为一个成员变量,而且是第一个首先声明的成员变量.这个可能会帮助大家理解.这里面还是主要分享4个比较重要的函数.

    构造函数

    子类实例化对象的时候,必须先把父类给默认构造函数给调用(或者显示调用)出来.这一点是我们需要知道的.

    class Person
    {
    public:
    	Person(const char* name = "peter")
    		: _name(name)
    	{
    		cout << "Person()" << endl;
    	}
    protected:
    	string _name; // 姓名
    };
    
    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    	{
    		cout << "Student()" << endl;
    	}
    protected:
    	int _num; //学号
    };
    
    int main()
    {
    	Student s1("jack", 18);
    	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-20220719115254652

    显示调用父类构造函数

    如果我们想要调用父类的构造函数,我们该如何去做?

    我们直接 在初始化列表里面去初始化父类的成员会怎么样? 看到答案是不行的

    class Person
    {
    public:
    	Person(const char* name = "peter")
    		:_name(name)
    	{
    		cout << "Person()" << endl;
    	}
    protected:
    	string _name; // 姓名
    };
    
    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_name(name)
    		,_num(num)
    	{
    		cout << "Student()" << endl;
    	}
    protected:
    	int _num; //学号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    image-20220719120138506

    我们把父类的成员在构造函数体内进行再次赋值,我们发现这是可以的,但是这里我们需要知道原理,初始化列表是声明和定义的地方,函数体内是再次赋值结果也证明了,在初始化列表里面是调用了父类的默认构造函数的.

    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    	{
    		_name = name;   //  再次 赋值
    		cout << "Student()" << endl;
    	}
    protected:
    	int _num; //学号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20220719120437301

    在初始化列表里面显示调用构造函数,这才是最正确的做法.

    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		,Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    protected:
    	int _num; //学号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20220719121255636

    父类是先构造的吗

    是的,我们可以认为,在子类中,父类当作一个成员变量,而且是首先声明的变量,是先需要帮助父类构造,才构造子类的.

    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		,Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    protected:
    	int _num; //学号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20220719121656410

    拷贝构造

    谈完了构造函数,我们需要谈谈拷贝构造了,这个就比较简单了.这里我在强调一遍,父类看作一个自定义类型变量,他会调用自己的拷贝构造,而且是首先调用.这里唯一的问题是在初始化列表中,我们该如何传入父类拷贝构造的函数,我们该传什么类型呢?要知道,继承是可以切片的,传入子类就可以了.

    class Person
    {
    public:
    	Person(const char* name = "peter")
    		:_name(name)
    	{
    		cout << "Person()" << endl;
    	}
    
    	Person(const Person& p)
    		: _name(p._name)
    	{
    		cout << "Person(const Person& p)" << endl;
    	}
    
    protected:
    	string _name; // 姓名
    };
    
    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		,Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    
    	Student(const Student& s)
    		: _num(s._num)
    		, Person(s) //  会 发生 切片
    	{
    		cout << "Student(const Student& s)" << endl;
    	}
    protected:
    	int _num; //学号
    };
    
    int main()
    {
    	Student s1("jack", 18);
    	Student s2(s1);
    
    	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

    image-20220719131404014

    赋值重载

    赋值重载和拷贝构造差不多,不过这是在函数体内调用,因为赋值重载是没有初始化列表的,注意一点就可以了.

    class Person
    {
    public:
    	Person(const char* name = "peter")
    		:_name(name)
    	{
    		cout << "Person()" << endl;
    	}
    
    	Person& operator=(const Person& p)
    	{
    		cout << "Person operator=(const Person& p)" << endl;
    		if (this != &p)
    			_name = p._name;
    		return *this;
    	}
    
    protected:
    	string _name; // 姓名
    };
    
    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		, Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    
    	Student& operator=(const Student& s)
    	{
    		cout << "Student& operator= (const Student& s)" << endl;
    		if (this != &s)
    		{
    			Person::operator =(s);  // 会 发生 隐藏   突破类域
    			_num = s._num;
    		}
    		return *this;
    	}
    
    protected:
    	int _num; //学号
    };
    
    int main()
    {
    	Student s1("jack", 18);
    	Student s2("joker", 18);
    
    	s2 = s1;
    
    	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

    image-20220719131846070

    析构函数

    这里析构函数需要有点问题,我们确实需要好好看看.按照我们上面的想法,不久是析构函数吗?可以,我先让父类析构,最后在析构子类的(这个顺序是不对的),我们也这么做.

    class Person
    {
    public:
    	Person(const char* name = "peter")
    		:_name(name)
    	{
    		cout << "Person()" << endl;
    	}
    
    	~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    protected:
    	string _name; // 姓名
    };
    
    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		, Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    
    	~Student()
    	{
    		~Person();
    		cout << "~Student()" << endl;
    	}
    
    protected:
    	int _num; //学号
    };
    
    int main()
    {
    	Student stu("jack", 18);
    
    	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

    image-20220719132853911

    这里我们就疑惑了,为何会报错?我们好象使用的方法很正确啊,子类和父类没有构成隐藏的函数啊,这是为啥?这里是因为C++在设计析构函数的,函数名有点问题.记住父子类的析构函数构成构成隐藏关系,这是由于析构函数被编译器统一处理为destructor(),这是为了多态的需要,我们需要突破类域.

    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		, Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    
    	~Student()
    	{
    		Person::~Person();
    		cout << "~Student()" << endl;
    	}
    
    protected:
    	int _num; //学号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20220719133551132

    这里我们就可以析构了,现在又出来一个问题了,我们好象把Person给析构了两次,幸亏我们父类里面没有使用delete,要不让绝对会delete两次,编译器绝对会报错.

    那么这里我们就开始疑惑了,我们这不行那不行,到底怎么才是可以?我们什么都不做,这就可以了.

    class Student : public Person
    {
    public:
    	Student(const char* name, int num)
    		:_num(num)
    		, Person(name)  //  这 才是  最正确  的动作
    	{
    		cout << "Student()" << endl;
    	}
    
    	~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    
    protected:
    	int _num; //学号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image-20220719134059168

    总结

    我们需要对析构函数来进行一个总结,子类的析构函数不需要管父类,编译器会自动调用的.我们还要析构函数做一个总结,是先把子类的给析构,在把父类给析构的,这里符合先构造父类,在构造子类,符合栈的顺序.

    友元和继承

    我们如果把父类里面一个友元函数,这里需要说明一下,子类里面和这个函数是没有一点关系的,也就是友元是不能继承的.

    class Student;
    class Person
    {
    public:
    	friend void Display(const Person& p, const Student& s);
    protected:
    	string _name; // 姓名
    };
    class Student : public Person
    {
    protected:
    	int _stuNum; // 学号
    };
    void Display(const Person& p, const Student& s)
    {
    	cout << p._name << endl;
    	cout << s._stuNum << endl;
    }
    int main()
    {
    	Person p;
    	Student s;
    	Display(p, s);
    	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

    image-20220719135208207

    继承和静态成员

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

    class Person
    {
    public:
    	static int _count; // 统计人的个数。
    };
    int Person::_count = 0;
    class Student : public Person
    {
    };
    class Graduate : public Student
    {
    };
    int main()
    {
    	cout << &Person::_count << endl;
    	cout << &Student::_count << endl;
    	cout << &Graduate::_count << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20220719203921442

    多继承

    现实世界中,一个人可能是一个教师助教,也就是说他又两个身份,既是学生,有是老师,在C++ 中也支持这样的情况.这是C++最让我感到痛苦的地方,C++支持多继承,好家伙,多继承一出来,我们讨论的的难度要提上一个台阶.多继承里面存在很多问题,例如二义性和代码冗余,后面的很多语言都舍弃了多继承,例如Java,只允许单继承.

    菱形继承是多继承的一种特殊情况.这里我们就以菱形继承来举列子.

    class Person
    {
    public:
    	string _name; // 姓名
    };
    
    class Student : public Person
    {
    protected:
    	int _num; //学号
    };
    class Teacher : public Person
    {
    protected:
    	int _id; // 职工编号
    };
    class Assistant : public Student, public Teacher
    {
    protected:
    	string _majorCourse; // 主修课程
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    image-20220719190100181

    代码冗余

    我们是可以接受的,助教分别继承了teacher和student,它们两个有继承了Person,这就给代码造成了一定的冗余.

    image-20220719191650317

    int main()
    {
    	Assistant a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220719191825978

    如果说空间小点还好,要是出现很大的空间,这就造成了空间的浪费,我们这里给person里面加上一个很大的数组,这就又极大的空间浪费.

    class Person
    {
    public:
    	string _name; // 姓名
    	int arr[100000];
    };
    int main()
    {
    	cout << sizeof(Assistant) << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image-20220719192525679

    二义性

    谈完了代码冗余,这里还有一个问题,这就是二义性,我们想问,如何访问继承person里面的那些成员变量呢?这里面存在两个_name,就造成了不确定性.

    int main()
    {
    	Assistant a;
    	a._name = "peter";
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    image-20220719192754297

    当然,如果我们实在是想要访问这里面的_name,也不是不可以,需要突破类域来访问.

    int main()
    {
    	Assistant a;
    
    	a.Student::_name = "xxx";
    	a.Teacher::_name = "yyy";
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20220719193252729

    虚拟继承

    虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用.

    我们先来实际看看多继承代码的冗余,这里我看看实际的内存,注意这里不要看什么监视窗口了,VS的监视窗口时经过美化过的.

    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::_a = 1;
    	d.C::_a = 2;
    
    	d._b = 3;
    	d._c = 4;
    
    	d._d = 5;
    	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

    image-20220719195557999

    从这里我们可以看,D是先继承B的,所以这里先给B开辟空间,后面才是A的,我们也可以看出,这里面确实多开辟一些不必要的空间,例如_a 被开辟了两次.

    虚继承

    现在我们就可以谈谈虚继承了,虚继承是把重复的类里面的内容只保留一份,解决代码的冗余和二义性.

    class A
    {
    public:
    	int _a;
        int arr[100000];
    };
    
    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;
    };
    int main()
    {
    	cout << sizeof(D) << 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
    • 27
    • 28

    image-20220719200247994

    我们需要看看代码的实际内存,也分析一下虚拟继承是怎么来的.通过这里我们就可以发现,虚继承重复的变量都被放在一起了,成了一份,这样就解决好了.

    虚拟继承

    虚基表

    如果说,你只是想认识一下C++,到上面的哪个层次就完事了,要是想要深入,这里还有一个问题.

    image-20220719201452392

    这两个是一个指针,这两个指针叫虚基表指针,指向的叫做虚基表(图上错了).

    image-20220719201938916

    这里我们就疑惑了,我们要两个虚继表干啥?大家仔细看,发现虚基表指针指向的地址的下一个位置(4个字节),一个 是 20,另一个 是 12,我们要谈的就是这两个数,这两个数,我们知道公共区域的偏移量,看看吧.这样的话,无论这个公共的区域跑到哪,我们有偏移量,就可以找到它.

    偏移量

    虚继承是如何发生切片的

    注意,只要是我们发生了虚继承,一定是存在这个续集表的,就是为了规则统一,利于编译器工作.到这里我们就知道了,编译器先找到这个虚继表指针,计算出偏移量,得到相应的内存,然后把和独属于自己的一起拿出来就可以了,这也是虚继表的作用.

    int main()
    {
    	D d;
    	B b = d;  
    	C c = d;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    继承总结

    继承是一个语法很多的知识点,但是实际应用上比较简单,我们一般不用那些看着很复杂的代码,像多继承,我们很少使用,即使是单继承,我们一般也是public继承.

    继承和组合

    组合就是让自定义类型作为类的一个成员就可以了.

    • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
    • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象

    这里面我还要讲述一个特性,在计算机中,我们类和类之间的联系越少越好,这也是软件工程提出的"高内聚,低耦合",无关的代码不要,模块与模块之间要自由.

    所以在一定的程度上,继承是破坏了封装,父子类之间关系太紧密,继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可.继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高.

    对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封.

    我们对于那些即适合继承又适合组合的,优先使用组合.

  • 相关阅读:
    Clickhouse冷热配置
    学习Java框架的重要性及步骤
    AVL部分功能实现和了解
    《golang设计模式》第三部分·行为型模式-06-备忘录模式(Memento)
    C语言用筛选法求 100 之内的素数(挖去 1,被除数平方根)
    MySQL压缩包方式安装,傻瓜式教学
    MPEG算法及代码例程
    「Redis原理」Redis 网络模型 -- 阻塞非阻塞IO、IO多路复用、epoll详解
    计算机毕业设计Java哈尔滨旅游项目推荐平台演示录像2020(源码+系统+mysql数据库+Lw文档)
    opensips开启python支持
  • 原文地址:https://blog.csdn.net/m0_61334618/article/details/125880983