多平台统一管理软件接口,如何实现多平台统一管理软件接口
404
2022-11-05
C++ 继承详解
写在前面
在谈着这个之前,我们需要先说说C++的几大特性,封装继承,多态…注意,实不置这三种,只不过他们是基础罢了,大家面试的时候注意一点.我们已经学过了封装,今天就开始继承吧,我们最好按照简单的学习来,这里的语法可能有点难,但是我们用的时候一定要偏简单一点.
继承
说实话,C++的那些大佬也考虑了很多方式,把继承搞得很复杂,管是继承方式就有三种,所以后面的语言尽力把这个知识点给简化了,我们学习C++确实需要些时间来思考.多的不说,现在看看继承究竟是什么.
什么是 继承
我查了一些资料,里面对继承的概念简述的还是比较详细的.
继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。(来源:维基百科).我举一个不太恰当的例子,当张三的父亲离世后,张三继承了父亲的遗产,这里张三就是"子类",其父亲便是"父类",张三拥有父亲的遗产,但是除此之外他可能也有自己的财富。
为何要 继承
我们可以举一个例子来帮助我们理解,假设我们要做一个学校人员管理系统,在一个学校里面是存在老师,学生…角色的,我们要是给每一个角色都封装一个类,想想每一个类里面都存在名字,年龄…等相同的成员变量,想想都头疼,假如我们把这个相同的属性拿出来,单独作为一个类,让其他的类继承它不就可以了吗.这就是继承的作用,代码复用,避免重复造轮子.
如何 继承
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;}
继承了父类的什么
这里我现给大家一个不恰当的结论,可以这么说,子类继承了父类的成员函数和成员变量.这里面也是存在很大的问题的.
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;}
继承方式
前面我就说了,看到C++的继承方式我就感到一镇头疼,要知道我们访问修饰限定符也是存在三种的,这一计算就是九种情况.这个我们很多人都带来了巨大的困难.
类成员/继承方式 | public继承 | protected继承 | private |
public 修饰 | 子类 public 成员 | 子类 protected成员 | 子类 private成员 |
protected 修饰 | 子类 protected成员 | 子类 protected成员 | 子类 private成员 |
private修饰 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
大家想不要慌,这九种情况还是很好区分的,我们可以得到下面的两条结论.
private 成员 无论是什么继承方式 在 子类种不可见其余的成员是 父类成员与继承方式相比较 权限较小的那一个 权限比较 public > protected > private
关于这个情况,我们不用担心,一般都是public继承,父类里面大多是protected成员,很少使用其他的.
不可见 VS 没有继承
我们需要看看究竟什么是不可见,什么是没有继承.没有继承可以了解,这里主要看看什么是不可见.
所谓的不可见是你在子类中无法直接访问这个类型,但是有确确实实的继承了.
那么这里就有一个问题了,我们是不是可以间接的去访问这个成员啊,是的,我们可以通过函数来访问,这里就不和大家分享了,下去有兴趣的话可以自己试试.
继承特性
对于继承,父子类之间具有很多的特性,其中比较关键的有两个。
切片隐藏
切片
我们可以这么认为,父类是可以接受子类的类型的,这就像一个水到渠成的事,不是什么类型的转化,类似一种父亲可以教导孩子,这也可以认为是向上调整.这也是后面多态的基础.
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去 .
我们可以看看下面的例子.
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;}
这里要和大家提一下,这个切片是有要求的,子类继承父类的方式必须是pubilic继承,其他的方式是不可以的.
赋值
我分别说一下它们的情况,里面有一点细节要来分享一下.
对于直接把子类赋值给父类,这是会调用父类的赋值函数,编译器会自动把从属父类的内容那一部分给切片了,回去赋值给父类对象.
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;}
引用
如果是用父类去引用子类的对象,就相当于给子类对象里面的属于父类的取了一个别名.
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;}
指针
指针更好理解,指针指向的就是子类当中属于父类的那些东西.
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;}
子类可以接受父类吗
这个普通的方法是不可以的,在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;}
成员函数
现在变量已经说完了,我们现在可以谈谈函数的隐藏了,再谈隐藏之前,我们先问问一个问题,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; }};
子类的默认成员函数
构造函数
子类实例化对象的时候,必须先把父类给默认构造函数给调用(或者显示调用)出来.这一点是我们需要知道的.
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;}
显示调用父类构造函数
如果我们想要调用父类的构造函数,我们该如何去做?
我们直接 在初始化列表里面去初始化父类的成员会怎么样? 看到答案是不行的
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; //学号};
class Student : public Person{public: Student(const char* name, int num) :_num(num) { _name = name; // 再次 赋值 cout << "Student()" << endl; }protected: int _num; //学号};
在初始化列表里面显示调用构造函数,这才是最正确的做法.
class Student : public Person{public: Student(const char* name, int num) :_num(num) ,Person(name) // 这 才是 最正确 的动作 { cout << "Student()" << endl; }protected: int _num; //学号};
父类是先构造的吗
class Student : public Person{public: Student(const char* name, int num) :_num(num) ,Person(name) // 这 才是 最正确 的动作 { cout << "Student()" << endl; }protected: int _num; //学号};
拷贝构造
谈完了构造函数,我们需要谈谈拷贝构造了,这个就比较简单了.这里我在强调一遍,父类看作一个自定义类型变量,他会调用自己的拷贝构造,而且是首先调用.这里唯一的问题是在初始化列表中,我们该如何传入父类拷贝构造的函数,我们该传什么类型呢?要知道,继承是可以切片的,传入子类就可以了.
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;}
赋值重载
赋值重载和拷贝构造差不多,不过这是在函数体内调用,因为赋值重载是没有初始化列表的,注意一点就可以了.
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;}
析构函数
这里析构函数需要有点问题,我们确实需要好好看看.按照我们上面的想法,不久是析构函数吗?可以,我先让父类析构,最后在析构子类的(这个顺序是不对的),我们也这么做.
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;}
这里我们就疑惑了,为何会报错?我们好象使用的方法很正确啊,子类和父类没有构成隐藏的函数啊,这是为啥?这里是因为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; //学号};
这里我们就可以析构了,现在又出来一个问题了,我们好象把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; //学号};
总结
我们需要对析构函数来进行一个总结,子类的析构函数不需要管父类,编译器会自动调用的.我们还要析构函数做一个总结,是先把子类的给析构,在把父类给析构的,这里符合先构造父类,在构造子类,符合栈的顺序.
友元和继承
我们如果把父类里面一个友元函数,这里需要说明一下,子类里面和这个函数是没有一点关系的,也就是友元是不能继承的.
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;}
继承和静态成员
基类定义了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;}
多继承
现实世界中,一个人可能是一个教师助教,也就是说他又两个身份,既是学生,有是老师,在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; // 主修课程};
代码冗余
我们是可以接受的,助教分别继承了teacher和student,它们两个有继承了Person,这就给代码造成了一定的冗余.
int main(){ Assistant a; return 0;}
如果说空间小点还好,要是出现很大的空间,这就造成了空间的浪费,我们这里给person里面加上一个很大的数组,这就又极大的空间浪费.
class Person{public: string _name; // 姓名 int arr[100000];};int main(){ cout << sizeof(Assistant) << endl; return 0;}
二义性
谈完了代码冗余,这里还有一个问题,这就是二义性,我们想问,如何访问继承person里面的那些成员变量呢?这里面存在两个_name,就造成了不确定性.
int main(){ Assistant a; a._name = "peter"; return 0;}
当然,如果我们实在是想要访问这里面的_name,也不是不可以,需要突破类域来访问.
int main(){ Assistant a; a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; return 0;}
虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在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;}
从这里我们可以看,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;}
我们需要看看代码的实际内存,也分析一下虚拟继承是怎么来的.通过这里我们就可以发现,虚继承重复的变量都被放在一起了,成了一份,这样就解决好了.
虚基表
如果说,你只是想认识一下C++,到上面的哪个层次就完事了,要是想要深入,这里还有一个问题.
这两个是一个指针,这两个指针叫虚基表指针,指向的叫做虚基表(图上错了).
这里我们就疑惑了,我们要两个虚继表干啥?大家仔细看,发现虚基表指针指向的地址的下一个位置(4个字节),一个 是 20,另一个 是 12,我们要谈的就是这两个数,这两个数,我们知道公共区域的偏移量,看看吧.这样的话,无论这个公共的区域跑到哪,我们有偏移量,就可以找到它.
虚继承是如何发生切片的
注意,只要是我们发生了虚继承,一定是存在这个续集表的,就是为了规则统一,利于编译器工作.到这里我们就知道了,编译器先找到这个虚继表指针,计算出偏移量,得到相应的内存,然后把和独属于自己的一起拿出来就可以了,这也是虚继表的作用.
int main(){ D d; B b = d; C c = d; return 0;}
继承总结
继承是一个语法很多的知识点,但是实际应用上比较简单,我们一般不用那些看着很复杂的代码,像多继承,我们很少使用,即使是单继承,我们一般也是public继承.
继承和组合
组合就是让自定义类型作为类的一个成员就可以了.
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
这里面我还要讲述一个特性,在计算机中,我们类和类之间的联系越少越好,这也是软件工程提出的"高内聚,低耦合",无关的代码不要,模块与模块之间要自由.
所以在一定的程度上,继承是破坏了封装,父子类之间关系太紧密,继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可.继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高.
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封.
我们对于那些即适合继承又适合组合的,优先使用组合.
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~