继承概念与定义
概念
举个例子:
在定义类的时候,内部的成员甚至成员函数都有相同之处,假设我定义,学生,老师,导员,院长,校长等等,他们都有共同的特性,名字,性别,年龄等等,不相同的有学号,工号等等。
那么如果像往常一样去定义,会写很多重复的内容。
这时候C++就提供了一种语法叫做继承。
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。
#include<iostream> using namespace std; class information//信息类 { public: void print() { cout << "name:" << name << endl; cout << "age:" << age << endl; } protected: char name[10] = "Baiye";//名字 int age = 18;//年龄 }; class student:public information { protected: int student_number;//学号 }; class teacher:public information { protected: int job_number;//工号 }; int main() { student s1; teacher s2; s1.print(); s2.print(); return 0; }
这里打印结果显示了年龄,名字,但是看原来的这两个类中的并没有打印函数,也没有这两个成员。
调试发现,学生类和教师类创建的对象中有信息类的成员和成员函数。
定义
这里来说说定义的格式:
派生类也可以叫子类,基类也可以叫做父类。
这里就算子类继承了父类成员,他们也是两种不同的类,里面的成员也是各自的。
继承方式是什么呢?
就是将父类的内容以什么访问限定符继承在子类当中。
当然,父类的内容里面也是有三种访问限定符的,父类的访问限定符和继承方式也是有关系的 ,所以这样看来,就有9中方式了。
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
1.这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.那么基类的成员又不想被外面访问,又想继承到子类,这个时候父类的内容就要用保护了,也可以看得出保护是因此而出现的,之前保护和私有是没什么去别的。
3.继承方式也可以不写,使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
4.其实最常用的也只有这两种情况而已:
总结:继承和访问限定符的9种情况,其实就是取权限小的那一种情况。
基类与派生类对象的赋值转换
我们平时在进行不同类型赋值的时候,都会产生临时变量,但是派生类和基类的对象却不会。(公有继承)
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
基类对象不能赋值给派生类对象。
这里不是类型转换,是类似与切割的方式:
子类给父类赋值其实只是让子类中的父类成员给赋值,多余的就不要了,这就像切割一样。
引用和指针也是一样的:
#include<iostream> using namespace std; class information//信息类 { public: void print() { cout << "name:" << name << endl; cout << "age:" << age << endl; } protected: char name[10] = "Baiye";//名字 int age = 18;//年龄 }; class Student:public information { protected: int student_number = 0; }; int main() { Student s1; information s2; information& p1 = s1;//不用加const return 0; }
注意:父类不能赋给子类。
继承中的作用域
成员同名
在继承体系中基类和派生类都有独立的作用域。
这说明命名是可以重复的,那么同名成员要怎么算呢?
#include<iostream> using namespace std; class information//信息类 { protected: int age = 18; }; class Student :public information { public: void print() { cout << age << endl; } protected: int age = 28; }; int main() { Student s1; s1.print(); return 0; }
这里打印的是谁呢?
通常都是采取就近原则,打印函数在子类,那就按照子类中的成员来:
子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
如果想访问父类的成员可以指定作用域:
函数同名
这里函数同名是不构成重载,重写,也是隐藏。
#include<iostream> using namespace std; class B { public: void print() { cout << "B" << endl; } protected: int age = 18; }; class A :public B { public: void print(int i) { cout << "A "<< i << endl; } protected: int age = 28; }; int main() { A s; s.print(1); return 0; }
如果是调用父类的成员函数:
这里报错了。
如果想访问父类的也要加指定作用域:
所以在定义的时候尽量不要定义名字相同的。
派生类的默认成员函数
#include<iostream> using namespace std; 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: protected: int _num;//学号 }; int main() { Student s; return 0; }
这里调用了一次构造,一次析构。
构造函数:
首先要知道,在子类当中,父类的成员要调用父类的构造函数才能初始化,这里父类的构造函数我们用的缺省值,如果将缺省值去掉,还是像这样去创建子类对象是会报错的。
那么如果在子类想写构造函数,初始化父类的成员函数必须去调用父类的构造函数,不然会报错。
#include<iostream> using namespace std; 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 num) :Person(name)//调用父类的构造函数 ,_num(num) { } protected: int _num;//学号 }; int main() { Student s("baiye",18); return 0; }
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
拷贝构造:
因为都是内置类型,如果是子类的成员就调用子类的默认拷贝构造,父类的成员就去调用父类的拷贝构造。
那么如果遇到深拷贝的时候,子类就必须去写构造函数了。
在初始化列表中调用父类的拷贝构造就可以了。
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
赋值重载:
派生类的operator=必须要调用基类的operator=完成基类的复制。
析构函数:
析构函数有两个特点,;
1.子类析构和父类析构默认是隐藏关系。(由于多态的关系,所有的析构函数都会被处理成destrutor)
2.先析构子类,后析构父类。
我们发现这里析构函数打印了两次。
因为这里子类析构调用完之后又回去调用父类析构,这是编译器的默认行为。
也就是说我们根本不用去显示的调用父类的析构函数。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。