面向对象三大特性:
封装,继承,多态。
封装本质是一种更好的管理。
相对于C语言面向过程,数据和方法都放在类中进行管理,再通过访问限定符进行限制。
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特
性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,
体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
从类的设计角度,避免重复定义数据和方法,进行类角度的复用。
老师和学生都属于人,但是老师和学生负责的模块不同为了减少冗余,就引入了继承。
这里就能明显看出,我们没有给学生的定义name成员但是可以调用基类的成员,这就是继承。
class Student : public Person { public: void func() { //cout << _name << endl; // 不可见 cout << _age << endl; // protected } protected: int _stuid; // 学号 };
派生类是Student 继承方式是public 基类是person。
通过上图可以算出有九种组合方式。
public > protected > private
基类的private成员,在子类是不可见的
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
这里子类的成员是要比父类的成员不吻合的,所以当父类通过指针或者引用去指向子类的时候,就会进行切片,将访问的吻合的区域,给指针或者引用。
Person * p= &S;
1.子类对象可以赋值给父类对象/指针/引用
2.基类对象不能赋值给派生类对象
3.基类的指针可以通过强制类型转换赋值给派生类的指针
class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { cout << "func(int i)->" << i << endl; } }; void main() { B b; b.fun(10); b.A::fun(); };
这里我们就可以看出我们创建出B的对象b再调用去成员函数fun,此时调用的就是B中的fun,对A中的fun进行利隐藏当我们利用作用于调用A::这个的时候才会对其进行寻找。
后构造的先析构
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant
的对象中Person成员会有两份。
class Person { public: string _name; // 姓名 }; class Student : virtual public Person { protected: int _num; //学号 }; class Teacher : virtual public Person { protected: int _id; // 职工编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 }; void Test() { // 这样会有二义性无法明确知道访问的是哪一个 Assistant a; a._name = "peter"; // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
此时就可以在监视窗口中看出,一个a成员有很多个相同的名字,这就二义性无法明确知道访问的是哪一个
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承
Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
原理:
class A { public: int _a; }; // class B : public A class B : virtual public A { public: int _b; }; // class C : public A class C : virtual public A { public: int _c; }; class D : public B, public C { public: int _d; }; int main() { D d; cout << sizeof(d) << endl; d._a = 0; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
这里通过代码可以看到无论修改成员中哪一个继承的类,他们所指向都是同一个,在内存中可以看到,BC类如果修改A中的成员都会通过一个地址去指向那个成员去修改这也就解决了数据的冗余。
这里这张图其实是虚基表的简图,用白话说就是,每一个要修改基类的成员会先通过一个地址去指向修改的类的地址,然后通过地址和偏移量去找到修改内容然后进行统一修改。