继承的引言
概念
继承(inheritance)机制是面向对象程序设计使代码可以 复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
定义
class People { public: People(string name = "John", int age = 18) { _name = name; _age = age; } void print() { cout << "姓名->" << _name << endl; cout << "年龄->" << _age << endl; } protected: int _age ; string _name; }; class Student : public People { public: Student(string name = "muyu", int age = 20, string id = "9210401227") :People(name, age) { _id = id; } protected: string _id; }; class Teacher : public People { public: Teacher() { People::_name = "mutong"; People::_age = 22; } protected: int _JobNumber; }; int main() { People p; p.print(); cout << endl; Student s; s.print(); cout << endl; Teacher t; t.print(); return 0; }
运行结果:
姓名->John 年龄->18 姓名->muyu 年龄->20 姓名->mutong 年龄->22
解释 class Student : public People
People类是 父类/ 基类, Student类是 子类/ 派生类
Student类继承People类的本质就是 复用 ⇒ Student 对象可以使用 People类里面的成员
成员包括 成员变量 和 成员函数.
成员变量是在对象空间内的, 而成员函数是不在对象空间内的, 属于整个类.
成员的 访问限定符 有三种 public, protected, private
继承方式不同 && 基类成员的访问限定符不同 ⇒ 决定了基类的成员在派生类中的存在情况
继承的方式
继承方式有三种: public, protected, private
成员的 限定符 有三种 public, protected, private
所以, 一共有 九种继承方式 👇👇👇
基类中的private成员, 在派生类中都是 不可见的
不可见 和 private成员是不一样的, private成员是 类里面可以访问, 类外面不可访问, 不可见是 类里面看不见/ 不可访问, 类外面不可访问
其余继承方式, 派生类中的情况是 继承方式 和 类成员访问限定符中 权限小的那一个
权限的大小: public > protected > private
父类如果是 class, 默认继承是 私有继承, 父类如果是 struct, 默认继承是 公有继承. 不过建议显示继承方式
常用的继承方式为 图中绿色的区域 ⇐ 继承的本质是 复用, 私有继承 和 基类中的私有成员在继承中是没有复用的意义的.
为什么 派生类没有 print函数 , 但能调用 print函数?
我们可以认为 子类对象里面包含两个部分: 父类对象成员变量 + 子类本身成员变量
子类对象中的 成员变量 = 自己本身的成员变量 + 父类的成员变量 (受访问限定符 和 继承方式共同限制)
子类对象中的 成员函数 = 自己本身的成员函数 + 父类的成员函数 (受访问限定符 和 继承方式共同限制)
print函数 是公有继承 && 访问限定符是公有 ⇒ 子类对象可以调用
为什么在 Teacher类中 可以People::_name = "mutong";
我们已经知道了 派生类对象的基本结构了.
那么派生类对象在 初始化阶段, 即调用默认构造 是先父类还是先子类呢?
通过调试, 我们发现: 子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数
Person类中有默认构造函数, 但是我们想改变一下 Teacher类对象中的 关于父类对象的那一部分, 那我们该怎么做呢?
首先, 我们不能直接写
_name = "mutong"; _age = 22;
因为受 域 的影响, 域是编译器在编译阶段查找变量的规则.
虽然, 我们可以认为子类对象中有 父类对象成员 + 子类对象成员, 但彼此是 独立的.
调用默认构造函数还是去 Person类中去调用
编译器在 编译阶段默认查找的顺序是 局部域 , 子类域, 父类域, 全局域
我们在子类中去给父类对象成员赋值 ⇒ 我们应该告诉编译器, 这个变量直接去父类中去查找就OK
即, 这个时候我们要用 Person(父类)::
为什么在 Student类中 可以 :People(name, age)
子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数.
那么如果 父类对象没有默认构造函数呢?
我们就需要 在子类的初始化列表处 显示调用父类的构造
基类和子类的赋值转换
int main() { People p; Student st; st = p; // error p = st; // 可以进行转换 return 0; }
父类对象 不能 赋值给子类对象, 而子类对象 可以 赋值给父类对象
可以这样想: 子类对象的成员 > 父类对象的成员 ⇒ 可以 变小一点, 但不能变大一点
父类对象 = 子类对象, 属于不同类型之间的赋值 ⇒ 一般都会发生 类型转换 ⇒ 类型转换, 那就意味着要产生 临时常量拷贝. 但结果真的如我们想的这般吗?
验证 父类对象 = 子类对象 是否有临时常量拷贝
拷贝是 常量的 ⇒ 要进行区分, 我们可以使用 引用 &
如果生成了临时拷贝, 我们用普通引用 就会导致 权限的放大 , 就会报错
如果没有生成临时拷贝, 我们用普通引用, 就是 权限的平移, 就不会报错
int main() { // 类型转换 int i = 0; double d = i; // double& dd = i // error const double& dd = i; // 赋值兼容转换 Student st; People ps = st; People& p = st; return 0; }
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫 切片 或者切割
. 寓意把派生类中父类那部分切来赋值过去
🗨️那么这个切片是怎样完成的呢?
继承中的作用域
🗨️在继承过程中, 可能会出现 父类中的成员名 和 子类中的成员名相同的情况
, 那么派生类对象调用该成员会是怎样的情况呢?
- 先看下面的代码:
class People { public: People(string name = "John", int age = 18) { _name = name; _age = age; } void print() { cout << "class People" << endl; } protected: int _age ; string _name; }; class Student : public People { public: Student(string name = "muyu", int age = 20, string id = "9210401227") :People(name, age) { _id = id; } void print() { cout << "class Student : public People" << endl; } protected: string _id; }; void test1() { Student st; st.print(); } int main() { test1(); return 0; }
运行结果:
class Student : public People
父类和子类中都有 print函数
, 通过结果显示 派生类内部的print函数
这是因为 域
, 跟上面的People::_name = "muyu";
是一样的道理
那么, 如果我们非要通过派生类对象 调用基类中的print函数
呢?👇👇👇
void test1() { Student st; st.People::print(); }
总结:
子类和父类中的成员尽量不同名!
上面的例子, 子类和父类有同名的成员, 子类隐藏父类的成员, 这种关系叫做 隐藏/ 重定义
注意: 隐藏 != 重载
重载的前提条件是 同一作用域, 而隐藏是 父类和子类成员同名
隐藏 != 重写
隐藏是 子类中同名成员隐藏父类中同名成员, 而重写是 子类中重写父类有关函数的实现
派生类中的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
派生类的operator=必须要调用基类的operator=完成基类的复制。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
派生类对象初始化先调用基类构造再调派生类构造。
派生类对象析构清理先调用派生类析构再调基类的析构。
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
class Person { public: Person(string name = "muyu", int age = 20) :_name(name) ,_age(age) { cout << "Person()" << endl; } Person(const Person& tem) { _name = tem._name; _age = tem._age; cout << "Person(const Person& tem)" << endl; } Person& operator=(const Person& tem) { _name = tem._name; _age = tem._age; return *this; cout << "Person& operator=(Person& tem)" << endl; } ~Person() { cout << "~Person()" << endl; } protected: string _name; int _age; }; class Student : public Person { public: Student(const string name,const int age, const int num) : Person(name,age) , _num(num) { cout << "Student()" << endl; } Student(const Student& s) : Person(s) , _num(s._num) { cout << "Student(const Student& s)" << endl; } Student& operator = (const Student& s) { cout << "Student& operator= (const Student& s)" << endl; if (this != &s) { Person::operator =(s); _num = s._num; } return *this; } ~Student() { cout << "~Student()" << endl; } protected: int _num; //学号 }; void test2() { Student st1("牧童", 20, 20230101); Student st2(st1); Student st3("沐雨", 18, 20230102); st3 = st1; } int main() { // test1(); test2(); return 0; }
运行结果:
Person() Student() Person(const Person& tem) Student(const Student& s) Person() Student() Student& operator= (const Student& s) ~Student() ~Person() ~Student() ~Person() ~Student() ~Person()
🗨️其他函数都是 先父类, 后子类, 唯独 析构函数 先子类后父类?
首先, 构造函数是 先父类, 后子类
栈, 先进后出 ⇒ 析构的时候, 先子类, 后父类.
其次, 父类可以调用子类的成员, 而子类不能调用父类的成员
如果先析构父类, 如果子类对象还想调用父类的成员,那就完蛋了!
🗨️在子类的析构函数中, 调用父类的析构函数
首先,
~Student() { ~Person(); // 提示有一个重载 cout << "~Student()" << endl; }
纳闷? 这个还能有重载?
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor().
那么子类和父类中的 析构函数名 都是 destruction ⇒ 那么就构成了隐藏关系
那么我们在子类中调用父类的析构函数应该如下:
~Student() { Person::~Person(); cout << "~Student()" << endl; }
结果如下:
编译器默认帮我们 先调用了父类的析构函数
⇒ 不信任我们用户, 由编译器自己完成