一、继承的概念
1、概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用。
class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: string _name = "peter"; // 姓名 int _age = 18; // 年龄 }; class Student : public Person { protected: int _stuid; // 学号 }; class Teacher : public Person { protected: int _jobid; // 工号 };
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。
2、继承的定义
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
3、继承关系和访问限定符
我们知道访问限定符有 public、protected 和 private三种,而继承方式也是有三种。他们之间重要的关系如下:
注:不可见表示子类无法访问父类的私有成员。因此如果父类里有不想让子类访问的成员,就可以设为私有权限。
基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承。
二、基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。基类对象不能赋值给派生类对象。
class Person { protected : string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public : int _No ; // 学号 }; int main() { Student sobj ; // 子类对象可以赋值给父类对象/指针/引用 Person pobj = sobj ; Person* pp = &sobj; Person& rp = sobj; }
三、继承下的子类的成员函数
父类
class Person { public: Person(const char* name = "ZDL") : _name(name) { cout << "Person()" << endl; } Person(const Person& p) : _name(p._name) { cout << "Person(const Person& p)" << endl; } Person& operator=(const Person& p) { cout << "Person operator=(const Person& p)" << endl; if (this != &p) _name = p._name; return *this; } ~Person() { cout << "~Person()" << endl; } protected: string _name; };
子类
class Student : public Person { public: Student(const char* name, int num); Student(const Student& s); Student& operator=(const Student& s); ~Student(); protected: int _num; };
1、构造函数
子类自己的成员,跟类和对象一样。从父类继承过来的成员需要去调用父类的构造函数。
Student(const char* name,int num) :Person(name) //_name是由父类继承而来,需要去调用父类的构造函数 ,_num(num) //子类自己的成员自己初始化 {}
2、拷贝构造函数
子类自己的成员,跟类和对象一样(内置类型完成值拷贝,自定义类型调用它自己的拷贝构造)。从父类继承过来的成员需要去调用父类的拷贝构造函数。
Student(const Student& s) :Person::Person(s)//调用父类的拷贝构造 ,_num(s._num) {}
3、赋值运算符
Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s); _num = s._num; } return *this; }
4、析构函数
在继承的情况中,不需要显示调用父类的析构函数,编译器自动调用,先析构子类,再析构父类。只需要处理子类中的成员。
//子类的析构函数跟父类的析构函数构成隐藏 ~Student() { //处理子类自己的 }
四、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
五、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
六、菱形继承问题
1、单继承
一个子类只有一个直接父类时称这个继承关系为单继承。
2、多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承。
3、菱形继承
菱形继承是多继承的一种特殊情况。 如下图:
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和
Teacher的继承Person时使用虚拟继承,加上关键字 virtual 即可解决问题。
为了方便观察,我们使用下面的例子来看看菱形继承的问题:
class A { public: int _a; }; // class B : virtual public A class B : public A { public: int _b; }; // class C : virtual public A 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中A的成员_a有两份,有数据冗余的问题。接下来我们了看看虚拟继承下的D中的情况。
这下我们发现 A 的 _a 成员只有一份了(红色框起来的),而 B 类和 C类中也分别多出了一个指针。那么这指针是个什么的呢?下面我们来讲一讲。
从内存的结构看去,我们发现指针指向的内容为它到取到 _a 的位置的偏移量,这样我们就可以找到共用的_a。这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A(虚基类) 。
七、继承与组合
1、public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
2、组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
3、优先使用对象组合,而不是类继承。
4、实际尽量多去用组合。组合的耦合度低,代码维护性好。要实现多态,也必须要继承。