目录
一.继承的概念及定义
继承 (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; // 工号 }; int main() { Student s; s.Print(); Teacher t; t.Print(); return 0; }
当我们自定义两个类,一个类用来描述该人的学生身份~另一个类用来描述他的老师身份~这时候就会出现很多重复的地方~
而为了解决这种问题,我们设置了继承这一概念~它可以把二者对身份描述共同的属性进行集合,而差异化留给类自己去展现~这样继承下来的类既有了基本的属性也有了独自特有的属性~
总结一句话就是:继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。虽然是这样理解,但我们在实践中几乎只用到Public继承~
二.基类和派生类对象赋值转换
int main() { int i = 0; double j = i; const double& r = i; return 0; }
在我们以前的学习中,类型转换需要有一个临时变量作为中介,最后才能完成赋值转换~
class Person { protected: string _name; // 姓名 string _sex; //性别 int _age; // 年龄 }; class Student : public Person { public: int _No; // 学号 }; int main() { Student s; Person p = s; Person& r = s; Person* ptr = &s; return 0; }
但在继承中却不会有临时变量,因为子类是继承父类的,所以我们可以利用子类对象来赋值给父类对象/指针/引用,但是在赋值的过程中子类对象都会经过切片处理~
切片后的子类只会包含父类的成员变量,并不会把自己的成员变量也赋值过去~而且只能是子类赋值给父类的向上传递~而不能父类赋值给子类的向下传递~
三.继承中的作用域
父子类可以拥有同名成员吗?——当然可以,因为它们是各自独立的作用域~ 虽然没有重名的必要性~class Person { public: void func() { cout << _id << endl; } protected: int _id = 1; }; class Student : public Person { public: void func() { cout << _id << endl; } protected: int _id = 2; }; int main() { Student s; s.func();//2 Person p; p.func();//1 return 0; }
那如果我们想要子类对象调用父类成员函数的时候要怎么办呢?
可以用指定域+域限制符的方式去访问父类成员函数~这样子也可以~总结:子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)ps:如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
四.派生类的默认成员函数
构造函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。class Person { public: Person(const char* name = "peter") : _name(name) { cout << "Person()" << endl; } protected: string _name; // 姓名 }; class Student : public Person { public: private: int _id = 1; }; int main() { Student s1; return 0; }
默认成员函数仍是之前所学的那套规则,在子类的默认构造由编译器默认生成下对于子类而言内置类型不处理,对于自定义类型而言会去调用它的构造函数,只不过这里调用的是其父类的构造函数~
class Person { public: Person(const char* name) : _name(name) { cout << "Person()" << endl; } protected: string _name; // 姓名 }; class Student : public Person { public: Student(const char* name, int id) :_id(id) ,Person(name)//以匿名对象的形式显示调用构造 { cout << "Student(const char* name, int id)" << endl; } private: int _id = 1; }; int main() { Student s1("张三",2); return 0; }
若父类没有默认构造函数(这里取消了全缺省),我们想用张三去初始化name的时候只能在子类的构造函数中进行显示调用,直接明摆着在此处去调用父类的构造函数~
ps:成员初始化的顺序是看成员所属类谁优先声明的~所以这里name比id先初始化~
拷贝构造
class Person { public: Person(const char* name = "peter") //Person(const char* name) : _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 id) :_id(id) ,Person(name) { cout << "Student(const char* name, int id)" << endl; } Student(const Student& s) :Person(s) ,_id(s._id) { cout << "Student(const Student& s)" << endl; } private: int _id = 1; }; int main() { Student s1("张三",2); Student s2(s1); return 0; }
子类一般是不用写拷贝构造的,除非涉及到了深拷贝~
这里不用担心会把子类的成员拷贝到父类,因为拷贝构造设置了引用,所以只会取切片后的
子类对象~然后就是父类成员调父类函数,子类成员拷贝自己的内容~
赋值重载
默认生成的赋值重载对内置类型完成浅拷贝,自定义类型调用其赋值拷贝~
class Person { public: Person(const char* name = "peter") //Person(const char* name) : _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; } protected: string _name; // 姓名 }; class Student : public Person { public: Student(const char* name, int id) :_id(id) ,Person(name) { 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) { cout << "Student& operator=(const Student& s)" << endl; if (this != &s) { Person::operator=(s); _id = s._id; } return *this; } private: int _id = 1; }; int main() { Student s1("张三",2); //Student s2(s1); Student s2("李四", 3); s2 = s1; return 0; }
在子类中的赋值重载必须要对父类的赋值重载加上显调,否则就会一直调用子类的赋值重载导致溢出,这是因为二者构成了隐藏关系~
析构函数
析构函数比较特殊,子类的析构函数会和父类的析构函数构造隐藏关系~那么我们需要显调父类的析构函数吗?——也不用,这里编译器会作特殊处理,父类析构会在子类析构后自动被调用~我们在初始化的时候是先初始化父类再子类,而在析构这里反而要先析构子类再析构父类。
五.继承与友元
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; } void main() { Person p; Student s; Display(p, s); }
有了友元父类可以在类外访问自己的保护成员,但继承了父类的子类却不会继承这种友元关系,并不能从类外访问自己的保护成员~
六.继承与静态成员
class Person { public : Person () {++ _count ;} protected : string _name ; // 姓名 public : static int _count; // 统计人的个数。 }; int Person :: _count = 0; class Student : public Person { protected : int _stuNum ; // 学号 }; class Graduate : public Student { protected : string _seminarCourse ; // 研究科目 };
静态成员的作用域是在静态区的,所以即使是继承关系,但父类与子类都是可以访问到count的
七.复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
然而在实践中我们能不能多继承就不用多继承,因为它有很多的坑点~
菱形继承:菱形继承是多继承的一种特殊情况。假如我们的父类有许多数据,在菱形继承中会出现数据冗余与二义性的问题~
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; // 主修课程 }; int main() { Assistant a; a._name = "张三";//不明确,是想找Student,还是Teacher呢? a.Student::_name = "张三"; a.Teacher::_name = "李四"; return 0; }
虽然我们最后可以通过指定类域的形式来解决问题,但这样做治标不治本~
对于一个人来说他可以取很多个名字,但身高,年龄什么的都是固定的,没必要跟着搞那么多份~
这样会让两个类都各自占据大量空间,明明只是改动了一个数据而已~所以在菱形继承下不应该让这两个类都继承其父类的全部属性。如果有一个共同管理的仓库就好了,让两个类都过去就行了~
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 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; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
如果没有虚拟继承那么对象在内存中的分布是这样的~
b对象中有父类a的数据,c对象中也有父类a的数据,然后d对象则包含b与c的数据~
而当我们使用了虚拟继承后,原本放置在对象b,对象c中的a数据跑到了最下方,而原本的位置则由其他数据表示。我们最对这些数据解析发现这是当前位置到数据a地址的偏移量,在虚拟继承中是通过指针存储偏移量,最后用当前地址加上偏移量找到数据a从而访问它的~
然后为了统一化,就连对象d也是有存储到底数据a的偏移量~而这都是因为菱形继承不得不做的事情~
总结:虽然我们用virtual关键字解决了菱形继承,但是从底层上看是牺牲了时间效率去换取空间的。所以一般都不要去刻意使用多继承,以免造成虚拟继承~