什么是继承
继承机制就是面向对象程序设计使得代码可以复用的重要手段,他允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,俗称派生类。
eg:
假设有三个种类,分别是人,老师学生
人:姓名,性别,年龄
学生:姓名,性别,年龄,学号
老师:姓名,性别,年龄,工号
老师与学生都有人的特性,三者都是单独的类,如果在将学生类中的特性全写过一遍的话,造成代码的重复,老师类也一样,所以如果我们可以让两个后者继承人类的性质然后再加上自己私有的性质,就可以让代码复用。
- class person
- {
- public:
-
-
- public:
- string _name;
- string _sex;
- int _age;
-
-
- };
- class student :public person
- {
- public:
- void func()
- {
- Print();
- //_age = 2;这是错误的,因为此时的_age在base类中是private
- }
- private:
- int _number;//学号
-
-
- };
- class teacher :public person
- {
-
- private:
- int _ID;//工号
-
- };
继承的规则
继承后base类的成员(成员函数+成员变量)都会变成derive类的一部分,体现了derive复用base类的成员。
即derive类可以“使用”base类的成员。
但并不是随便使用base类中成员。
继承方式:public/protected/private
访问限定符:public/protected/private
可以看到继承方式和访问限定符共有九种组合方式,不同组合方式导致derive类对base类成员的权限是不一样的,
但是private
1,基类private成员在derive类中无论以什么方式继承都是不可见的,不可见指的是基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问他们。
2,基类private成员在derive类中是不能被访问的,如基类成员不想在类外被直接访问,但是需要在derive类能被访问,就定义为protected,可以看出保护成员限定符是因继承才出现的
3,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,但是最好还是显示继承方式
4,在实际运用中一般使用的都是public继承,几乎很少使用protecte/private继承,因为protected/private继承下来的成员都只能在派生类的类里面使用,实际扩展维护性不强。
derive类和base类的赋值
派生类可以赋值基类的对象/基类的指针/基类的引用,这也叫做切片,寓意是把派生类中父亲的那部分切来赋值过去。(语法是天然支持的,不存在类型转换)
基类对象不能赋值给派生类对象
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针式指向派生类的才是安全的。
eg:
- void Test_inherit3()
- {
- //1,派生类对象可以赋值给基类对象/指针
- student s;
- person p1 = s;
- person* p2 = &s;
- person& p3 = s;
- //2,基类对象不能赋值给派生类对象
- //s = p1;会报错
- //3.基类的指针可以通过强制类型转换赋值给派生类的指针
- student* ptrs = (student*)p2;
- p2 = &p1;
- student* ptrs1 = (student*)p2;//这种情况转换时虽然可以,但是会存在越界访问
- p2 = &s;
- student& ptrs3 = (student&)(*p2);//不建议这样使用
}
作用域,派生类中默认成员函数,继承中的友元,继承中静态成员
1,作用域
1,在继承体系中基类和派生类都有独立的作用域
2,如果基类和派生类中有同名成员,那么派生类中的成员将屏蔽基类对同名成员进行直接访问,这种情况叫做隐藏也叫做重定义(在派生类成员函数中,如果想要访问基类的同名成员可以使用基类::基类成员 进行显示访问)
3,如果是成员函数名相同就会构成隐藏,不会看函数的参数是否相同
4,在实际的体系中最好不好构成隐藏
**注意:**上述的Print()构成的是隐藏,并不是构成重载。
2 默认成员函数
普通类都会有六个默认成员函数,如果我们不写,编译器会自动为我们生成的函数。
子类(派生类)可以将父类(基类)当作是一个自定义成员
子类构造函数规则:
a.先调用基类的构造函数初始化基类的那一部分构造函数,如果基类没有默认的构造函数,则必须在派生类的初始化列表中显示调用基类的构造函数
然后再参考普通类,其中拷贝构造和赋值运算符重载和构造函数都是一样的。
析构函数是子类先调用自己的析构函数,然后再调用基类的析构。
因为后续一些场景虚构函数需要重写,而重写的条件之一是函数名相同,那么编译器会对析构函数进行处理,会将析构函数名都进行特殊处理,处理成desturct(),**所以父函数不加virtual的情况下,子类析构和父类析构构成隐藏关系 这里指的是一个基类类型的指针 且基类的析构函数没有加virtual 然后直接delete 指针的情况 **
友元,静态成员
a.友元关系不能继承,也就是说基类的友元函数不能访问子类私有和保护成员。
b.如果基类定义了static静态成员,则整个继承体系里只有一个这样的成员,无论有多少个派生类,都是只有一个static成员。
我在基类中定义了一个静态成员变量,并且初始化为5
通过上述代码可以了解到,静态成员变量可以通过对象去访问,也可以通过基类或者子类的类域去访问,但无论如何访问,静态成员变量始终都是同一个。
如何实现不能被继承的类
只需要将基类的构造函数私有化,这样的话,子类就无法构造对象,但是引出一个问题,基类自身也不能调用构造函数了,既无法构造自身对象。如何解决?
如何构造子类对象:将子类变成父类的友元类即可
如何构造基类对象:将基类的构造函数用一个静态函数封装,如果不封装的话,就无法一开始就创造对象然后调用封装函数。
- class person
- {
- friend class student;
-
- public:
- static person creatObj()
- {
- return person();
- }
- private:
- person()
- {
- cout << "person构造" << endl;
- }
- };
- class student :public person
- {
- public:
- student()
-
- {
- cout << "student构造" << endl;
- }
- };
- int main()
- {
- student d;
-
- person q = person::creatObj();
- return 0;
- }
菱形继承
先了解一下单继承,多继承
根据上述可以发现,如上图所示,因为派生类student,和teacher都继承了person,而另外一个派生类又同时继承了他们两个,相当于其同时继承了基类person两次,这会导致数据的冗余和二义性问题,相当于继承了基类person两次,但在实际存储中如果采用菱形虚拟继承的话,person是会各自储存两次的,即使他们代表的意义是相同的。
二义性指的是无法明确知道访问的是哪一个类中的基类person。
- class person
- {
- public:
- string _name;
-
- };
- class student: public person
- {
- public:
- int _number;
- };
- class teacher : public person
- {
- public:
- int _ID;
- };
- class master :public student, public teacher
- {
- public:
- string _course;
- };
- int main()
- {
- master d;
- //d._name = "asas";//此时的_name有可能是student的,也有可能是teacher
- //具有二义性,虽然下列的显示访问能解决二义性问题
- //但数据的冗余性无法解决,因为此时共同继承的对象相当于是多储存了一份
- d.student::_name = "zjt";
- d.teacher::_name = "zh";
- return 0;
-
- }
虚拟继承可以解决菱形继承的二义性和数据冗余性的问题,如上面的继承关系,在student和Teacher继承person中使用虚拟继承也就是加一个virtual即可,需要注意的是,虚拟继承不要在其他地方使用!
菱形虚拟继承
为了更好的研究菱形虚拟继承原理,给出一个菱形继承继承体系,再借助内存窗口观察对象或成员的模型
上述中没有家virtual,公共元素既放在B中也放在C中,但是菱形虚拟继承,既没有将公共元素放在B中也没有将公共元素放在C中。
对于编译器而言,变复杂了为了解决菱形继承的问题,但是效率降低了,因为这样的话不是一次性找到地址,而是需要间接的通过虚基表寻找。
上述类定义的对象中,D不用找A但是B和C确实需要寻找A
可以看到ptr1和ptr2不知道自己压根不知道自己指向的是谁,这是一种切片,他们只管通过同样的方式先找到虚基表,然后通过虚基表中的偏移量找到A的位置,也可以说实际上D不用找A,但是B和C要找,因为不清楚B是切片的还是独立的,在D中找A直接通过切片的B寻找,但是独立的B要用自己的偏移量寻找
总结
无论是切片的B还是独立的B,寻找公共元素的方式都是一样的,都是通过虚基表指针找到虚基表,再计算其偏移量,找到公共元素!
继承与组合
摘自优先使用对象组合,而不是类继承
1,public继承是一种is-a关系,也就是说每个派生类对象都是一个基类对象
2,组合式一种has-a的关系。假设B组合了A,即每个对象中都有一个A对象
3,继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
4,对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
egstack queue priority_queue就是组合加泛型 只提接口,内部实现细节不提供 耦合度低
5实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关可以用继承,可以用组合,就用组合