在学完C++的类和对象,并掌握了类的核心语法与基本用法之后;我们就得去学习一下继承的语法,与继承的用法。简单概括一下,继承是C++中一种代码复用的手段,它允许我们,对已有的类,增添新的成员函数或变量,产生的新类,叫做子类。而本期博客,就是讲解继承的用法,如果这篇博客对你学习或工作上有帮助,还请多多点赞+收藏,万分感谢~
一、继承的基础概念(核心)
1、继承的用法展示
在我们现实生活中,每一所学校都拥有自己的教务系统,而教务系统中,存储着学生,老师以及学校一些其它的信息。在用代码实现教务系统的时候,我们需要用一个学生类储存学生的信息,我们又需要一个老师类储存老师的信息。类的定义如下:
这里只是简单的表示了一下:一个学生类,一个老师类,分别需要哪些属性和方法。可是我们从代码中发现,两者存在一定的代码重复,显得过于冗余。而C++中,因为继承机制的存在,我们可以对这两个类进行,下面的处理:
class Person{protected:string name = "周天津"; // 名字int old = 58; // 年龄string ID = "111081200631313111"; // 身份证号string tel = "11188888888"; // 手机};class teacher : public Person{public:void stude(){cout << "老师" << name << endl;cout << "年龄" << old << endl;cout << "ID" << ID << endl;cout << "电话" << tel << endl;cout << "职工" << work << endl;}private:string work = "数学老师";};
运行一下,看看效果:
2、继承的定义与使用
继承是C++中一种代码复用的手段,它允许我们,对已有的类,增添新的成员函数或变量,产生的新类,叫做子类。继承是一种层次结构的复用,体现了代码从简单到复杂;通过继承手段,我们可以使类得到复用。每种书的用语习惯不同,被复用的类可以称为父类也可以是基类,在此基础上产生的新类,叫做子类或派生类。继承的使用格式:class 子类名 : 继承方式 父类名3、继承方式
在C++中,继承方式存有3种:public继承、protected继承、private继承(这里复用三个访问权限修饰符),那不同方式的继承有什么作用呢?——不同方式的继承,影响着父类中不同,访问权限的成员继承之后,在子类当中的访问权限。 无论是什么继承,父类的私有成员,在子类中都是不可见的。父类的其余成员,继承方式与访问权限限定符,选择其中权限最小的;如:保护继承,父类的公有成员变保护,保护成员还是保护。当父类的成员,不想在类外之间访问,而想在子类中访问的时候,可以将父类的成员实现为保护成员。可见保护成员与私有成员的不同就在此处。继承方式不显示写的时候,struct定义的类,默认是公有基础,class定义的类,默认是私有继承。现实中,最常用的继承方式是公有继承~三种继承方式总结:
父类成员 | public继承 | projected继承 | private继承 |
---|---|---|---|
public | public | projected | private |
protected | protected | projected | private |
private | 不可见 | 不可见 | 不可见 |
4、隐藏规则
在继承体系中⽗类和⼦类都有独⽴的作⽤域。父类与子类,若存在成员变量名相同,则构成成员变量隐藏;父类与子类,若存在成员函数名相同,则构成成员函数隐藏;注意隐藏,只需要函数名相同即可。所谓隐藏,就是子类指针或引用,去调用构成隐藏关系的成员变量或成员函数的时候,默认的是子类中的成员函数,或成员变量~若想要调用父类中成员函数,或成员变量,则可以通过指明类型。5.子类与父类对象间的赋值兼容转换
子类的实例化对象、或其指针、引用,可以赋值给父类的实例化对象、或其指针、引用。这里是一种切片的效果,所以不涉及类型转换问题,在赋值的时候,不会产生临时对象。父类的实例化对象不能赋值给子类的实例化对象,但父类的指针或引用,可通过强制类型转换,赋值给子类的指针或引用,但前提是确保安全性。6、子类的默认成员函数
子类(派生类),作为一个类,同样会存在六种默认成员函数(C++11,会存在八种);而需要我们去重点学习的有:构造函数、析构函数、拷贝构造函数、赋值运算符重载,我们需要通过以下两个方面去学习这四种函数。 编译器默认生成的,其行为是什么?如果编译器默认生成的,不能满足我们呢的需要,我们又该如何显示实现呢?我将通过代码+注释,将这四种成员函数的行为,以及如何显示实现,展示出来。还请区分侧重点,自行通过IDE进行逐一验证,或详细内容还请跳转至类与对象篇。我这里只会总结,比较重要的默认成员函数中的知识点~详细内容还请跳转至类与对象篇。i、构造函数
总结:(初始化列表,定义变量的顺序:先父类后子类,其中又符合先声明的先定义)子类默认构造函数的行为: 调用父类的默认构造函数(无参构造函数,有参但全缺省构造函数,编译器自动生成的构造函数都属于默认构造函数);若父类无默认构造函数,则编译器报错。内置类型编译器的行为是未知的。自定义类型,编译器调用自定义类型的默认构造函数,若无默认构造函数,则编译器报错。可见,编译器对子类中父类的行为,和,对对象的行为基本类似。显示写子类构造函数: 父类的初始化,必须在初始化列表中进行;若不显示写父类的初始化,则调用父类的默认构造函数,若父类无默认构造函数,则编译器报错。内置类型编译器的行为是未知的。显示调用父类构造函数:父类名(参数)自定义类型和内置类型的初始化,和在普通类中,无区别。class Person // 基类{public:Person() = default; // 强制编译器生成 -> 默认构造函数Person(const char* name, size_t age, const char* Addr) //普通构造函数:_name(name), _age(age), _Addr(Addr) {}protected:string _name = "庄梦涵"; // 名字size_t _age = 17; // 年龄string _Addr = "河南龙门镇庄村"; // 籍贯};class student : public Person // 派生类{public:student() = default; // 强制编译器生成 -> 默认构造函数student(const char* name, size_t age, const char* Addr, const char* grade) // 普通构造函数:Person(name, age, Addr), _grade(grade){}void test(){cout << "_name:" << _name << endl;cout << "_age:" << _age << endl;cout << "_Addr:" << _Addr << endl;cout << "grade:" << _grade << endl;}protected:string _grade = "大二"; };int main(){student zmh;zmh.test();cout << endl;student cmy("常梦圆",17,"神垕","大四");cmy.test();return 0;}
ii、析构函数
总结:(析构函数,析构变量的顺序要求:先子类后父类,其中又符合后声明的先析构)子类默认析构函数的行为: 对于子类的,内置类型编译器的行为是未知的。对于子类的,自定义类型,编译器会调用它的析构函数。将子类的成员变量都析构之后,再自动调用父类的析构函数。显示写子类析构函数: 同普通的自定义类型一样,无论我们是否显示调用其析构函数,编译器都会在其销毁之前自动调用析构函数。所以,我们写子类的析构函数的时候,只需要负责子类新增,内置类型的成员变量指向资源的清理就行。至于自定义类型和父类,编译器会自动为我们清理。而这一目的,也是为了保证,先析构子类后析构父类。虽然编译器会自动为我们调用父类的析构函数,可若我们偏要显示调用的时候,也不是不可以——但必须要指明类域,否则编译器会报错。父类和子类的析构函数,构成隐藏。至于为什么,因为C++的重写,需要函数名相同。iii、拷贝构造函数
总结:(默认生成的拷贝构造函数是一种浅拷贝;若需要资源的申请与释放则需要自己实现)子类默认拷贝构造函数的行为: 需要知道的是,拷贝构造函数是构造函数的重载形式之一;对父类的处理:首先会去调用父类的拷贝构造函数,对自定义类型的处理:会去调用自定义类型的拷贝构造函数。对内置类型的处理:编译器直接进行值拷贝~显示写子类拷贝构造函数: 拷贝构造函数的函数名为类名相同;第一个参数必须是类名的引用(通常是const 引用,也不能有缺省值);并且没有返回类型(void也 没有);拷贝构造函数可以有其他参数(必须有缺省值),但这不是常见做法。对父类的处理:在初始化列表中,显示调用父类的拷贝构造函数。对自定义类型的处理:在初始化列表中,显示调用自定义类型的拷贝构造函数。对内置类型的处理:完成正确的初始化与赋值,可以是在构造函数的函数体中,也可以是初始化列表。iiii、赋值运算符重载
总结:赋值运算符重载函数的行为,无论是编译器自动生成的,还是人为实现的,都与拷贝构造函数相似。只不过前者是对已有对象进行赋值,后者是对正在创建的对象的初始化。
注:父类与子类的赋值运算符重载函数,构成隐藏关系,通过子类、子类指针或引用,显示调用父类的赋值运算符时,还请标明类域。
二、继承进阶(细节)
1、实现一个不能被继承的类
⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象。C++11新增了⼀个final关键字,final修改⽗类,⼦类就不能继承了。2、继承与友元
父类的友元函数,不能被子类继承;若想要该函数同时能访问父类和子类,则需要都进行友元声明。
3、继承与静态成员
父类被子类继承后,父类的静态成员在子类中,与父类共用,下面则是一个静态成员count,记录当前已实例化对象的个数。
class WK{public:WK(){cout << "当前已创建ZMH/WK类个数:" << ++count << endl;}~WK(){cout << "当前已创建ZMH/WK类个数:" << --count << endl;}protected:static int count;};class ZMH :public WK {};int WK::count = 0;int main(){WK d1,d2,d3;{ZMH d4, d5, d6;}WK d7;return 0;}