继承(inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
对于下面的学生类和老师类,我们发现他们具有很多同样的信息
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中都有变量_name和_age和Print()函数并且独立的有自己的_stuid / _jobid.
基类中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。又组合知识得出共有9种结果。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
对上表我们稍作分析就可以观察到:
1、在基类当中的访问方式为private的成员,在派生类当中都是不可见的(无论以任何方式继承)。
2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3、在基类当中的访问方式为public或protected的成员,在派生类当中的访问方式变为:Min(成员在基类的访问方式,继承方式)。其中三种访问限定符的权限大小为:public > protected > private
4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
double d = 1.1; int i = d; //int &r = d;//报错 //因为该语句会先将d赋值给临时变量,而临时变量是const的,无法直接赋值r,因为这是权限的放大,不合法。而下面这段代码是正确的 const int& r = d;//d转换到r要产生临时变量 string s = "abcd"; const string& rs = "abcd";
Student s;//子类 Person p = s;//父类 Perspon& rp = s;//子类对象赋值个基类引用而在public继承中,父类和子类是一个is - a 的关系,子类对象 可以赋值给 父类的对象 / 父类的指针 / 父类的引用。我们认为是天然的,中间不产生临时对象的,这个叫做父子类赋值兼容规则(切割/切片)
派生类对象赋值给基类对象:
派生类对象赋值给基类指针:
派生类对象赋值给基类引用:
// Student的_num和Person的_num构成隐藏关系 class Person { protected : int _num = 110;//身份证号 }; class Student : public Person { public: void Print() { cout<<" 学号:" << _num << endl; } protected: int _num = 120;//学号 }; void Test() { Student s1; s1.Print(); }; int main() { Test();//由于就近原则,找自己定义的,所以打印出,, 学号:120 return 0; }
父类成员和子类成员可以相同,因为它们处于不同的作用域;
默认情况下是直接访问子类的,子类同名成员隐藏了父类的同名成员;
继承中,同名的成员函数,函数名相同就构成隐藏,不管参数和返回值;
如果需要访问基类中的_num成员可以使用下面的方式:
使用 基类::基类成员
cout << Person::_num << endl; //指定访问父类当中的_num成员
对于以下代码,调用成员函数fun时将直接调用子类当中的fun,若想调用父类当中的fun,则需使用作用域限定符指定类域。
class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { A::fun(); cout << "func(int i)->" << i << endl; } }; void Test() { B b; b.fun(10); //func(int i)->10 b.A::fun();//func() }; int main() { Test(); return 0; }
上述代码中,父类中的fun和子类中的fun不是构成函数重载, 因为函数重载要求两个函数在同一作用域。
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
- 6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个
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; } 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 id) :Person(name) , _id(id) { cout << "Student(const char* name, int id)" << endl; } Student(const Student& s) :Person(s) ,_id(s._id) { cout << "Student(const Student& s)" << endl; } Student& operator=(const Student& s) { if (&s != this) { Person::operator =(s); _id = s._id; } cout << "Student& operator=(const Student& s)" << endl; return *this; } ~Student() { cout << "~Student()" << endl; } protected: int _id; }; int main() { Student s1("zhangsan", 18); Student s2(s1); Student s3("lisi", 19); s1 = s3; return 0; }
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
#include#include using namespace std; class Student; class Person { public: //声明Display是Person的友元 friend void Display(const Person& p, const Student& s); protected: string _name; //姓名 }; class Student : public Person { protected: int _id; //学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; //可以访问 cout << s._id << endl; //无法访问 } int main() { Person p; Student s; Display(p, s); return 0; }
对于上述代码Display函数仅是Person类的友元,但并不是Student类的友元所以
cout << p._name << endl; //可以访问
cout << s._id << endl; //无法访问
若想使下面语句正常使用则要在派生类Student当中进行友元声明。
class Student : public Person { public: //声明Display是Student的友元 friend void Display(const Person& p, const Student& s); protected: int _id; //学号 };
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
在基类Person当中定义了静态成员变量_count,Person又继承了派生类Studen但在整个程序中只有一个该静态成员。
在基类Person的构造函数当中设置_count进行自增,就可以得出构造了多少个Person和Studen类的对象
class Person { public: Person() { ++_count; } protected: string _name; // 姓名 public: static int _count; // 统计人的个数。 }; int Person::_count = 0; class Student : public Person { protected: int _stuNum; // 学号 }; int main() { Student s1; Person p1; cout << Person::_count << "\n"; //2 return 0; }
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的继承方式存在数据冗余和二义性的问题。
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() { // 这样会有二义性无法明确知道访问的是哪一个 //a._name = "zhangsan" Assistant a; // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决 a.Student::_name = "同学"; a.Teacher::_name = "老师"; } int main() { Test(); return 0; }
虽然该方法可以解决二义性的问题,但仍然不能解决数据冗余的问题。因为在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; }; int main() { Assistant a; a._name = "zhangsan"; //此时_name唯一一份 return 0; }
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
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; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
首先,我们对于不使用菱形虚拟继承进行分析, 我们发现D类对象当中各个成员在内存当中的分布情况如下:
这样就看出D类对象中含有两个_a成员,如果成员多那就浪费空间极大。
其次,我们在使用菱形虚拟继承进行分析我们发现D类对象当中各个成员在内存当中的分布情况如下:
其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,经过一系列计算最终都可以找到成员_a。
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
若是两个类之间既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合。
// Car和BMW Car和Benz构成is-a的关系 class Car { protected: string _colour = "白色"; // 颜色 string _num = "陕ABIT00"; // 车牌号 }; class BMW : public Car { public: void Drive() {cout << "好开-操控" << endl;} }; class Benz : public Car { public: void Drive() {cout << "好坐-舒适" << endl;} }; // Tire和Car构成has-a的关系 class Tire { protected: string _brand = "Michelin"; // 品牌 size_t _size = 17; // 尺寸 }; class Car { protected: string _colour = "白色"; // 颜色 string _num = "陕ABIT00"; // 车牌号 Tire _t; // 轮胎 };
Q:什么是菱形继承?菱形继承的问题是什么?
菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,我们称这种继承为菱形继承。
菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
Q:什么是菱形虚拟继承?如何解决数据冗余和二义性的
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式,即可解决问题。将Person类的成员存储一份,采用虚基表指针和虚基表使得Assistant类对象当中继承的Student类和Teacher类可以找到自己继承的Person类成员,从而解决了数据冗余和二义性的问题。
Q:继承和组合的区别?什么时候用继承?什么时候用组合?
继承是一种is-a的关系,而组合是一种has-a的关系。
如果两个类之间是is-a的关系,使用继承;(什么是什么)
如果两个类之间是has-a的关系,则使用组合;(什么有什么)
如果两个类之间的关系既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合。