本篇要分享的内容是关于继承的内容哼哼哼啊啊啊啊啊啊啊啊啊啊啊啊啊啊
以下为本篇目录
目录
1.简单了解继承
2.继承的简单定义
3.继承简单使用
4.继承方式
4.1基类的privat
4.2基类的protected
4.3不可见与private的区别
5.父子类对象赋值转换
6.继承的作用域
7.子类 / 派生类的默认成员函数
7.1默认构造
7.2拷贝构造
7.3析构函数及其原因
首先封装、继承、多态是面向对象的三大特性,本篇先了解继承。
继承(inheritance)机制是一个一个一个面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
简单来说继承就是可以将代码的内容进行复用
比如我们在学习冒泡排序、堆排序、快速排序这些排序需要对数据进行交换,那么使用交换数据的功能就属于函数集的复用,也就是将公共集的部分抽取出来被调用,这就是之前我们学到的复用。
1.简单了解继承
下面使用代码简单认识继承
可以看到这里定义了两个类,一个学生类一个教师类,我们发现有些数据是两个类中共有的,那么我们就可以单独将这些属性单独领出来,重新创建一个类来使用。
class Person
{string _name;string _id;string _tel;int age;
};
根据上图,具有两个类公共部分的叫做父类,或者基类;在图的下方的两个类就叫做子类,或者派生类;
2.继承的简单定义
我们可以先暂时记住这种继承方法,下文还会详细介绍
3.继承简单使用
用代码简单演示一下继承的使用
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;Teacher t;return 0;
}
可以看到如上代码定义了三个类,一个是Person类,还有Student类和Teacher类,并且两个子类中都有自己特有的的元素,学号和工号,在这样的情况下继承了父类并各自创建对象,那这个对象的属性都有什么呢?
通过调试后的监视窗口我们可以看到在使用继承之后,子类创建的对象都具有父类的属性。
同样的也可以继承成员函数
可以看到即使在子类中没有定义函数,在继承了父类之后,子类的对象也可以调用父类中的函数。
4.继承方式
在上面的代码中呢我们会看到一些关键字,比如protected,并且会有疑问那继承方式只能是public吗,接下来我们回到继承的定义
同时也要和之前类和对象中的封装的内容联系起来
我们在之前的封装中谈到过protected(保护)和private(私有),也说过保护和私有没有什么区别, 保护和私有在类的外部都不能访问,共有的在类外和类里面都能访问。
现在我们所接触到的继承中也有三种继承方式。但是这里需要理解的是,在父类中设置访问权限后,继承到子类中是否还设置了同样的权限呢?所以如下表
这个表来表示父类的成员在子类中又怎样的访问关系。
这个表的内容是很重要的,在记忆是要讲究技巧
首先我们可以将父类和子类想象成为父亲和儿子,父亲的私房钱是无论如何都不可以被儿子所知的,所以儿子不可以拿到父亲的私房钱;
我们使用最开始的代码演示
4.1基类的privat
可以看到在父类中将打印函数设为私有,子类对象便不能访问;
同样的不仅是在类的外部,在子类的内部同样不能访问,爸爸的私房钱是不能动的;
4.2基类的protected
在之前的封装中protected和private是没有区别的,但是在继承中两个方式便有了区别
还是上列代码,我们将Print保护起来,在类的外部显然是不能调用的
但是在子类的内部却能被调用;
所以protected在继承中才能体现出它的价值。
当我们通过两个例子之后就会发现继承方式会取在类的基类访问限定符和继承方式中权限小的那一个
所以再次观察这个表格你会发现两者相交权限会取小的限定符进行访问,这就是记忆的技巧,了解他的规律即可。
最后还需要注意的是我们都知道class类在不写继承方式的情况下默认是私有继承
可以看到Student继承父类没有写继承方式,默认为私有继承
但是struct类在不写继承方式的情况下默认为共有继承
可以看到运行成功
但是在这里我希望大家在使用继承时最好最好将继承方式规范写完整。
4.3不可见与private的区别
在上述的表格中我们看到了这两个信息
他们的本质区别就在于:私有属性是类里和类外都不可用,如同父亲的私房钱,怎么样都看不见用不到;
不可见属性是类里可以使用,但是类外不可使用;如同结婚时的彩礼,只能结婚时用,其他时间不能使用;
区别就这么简单。
5.父子类对象赋值转换
我们知道不同类型之间的对象赋值的时候,如果是相近类型会发生隐式类型转换,发生隐式类型转换就会产生中间变量
比如这里d时double类型,double和int类型相似,都是用来记录数据的类型所以可以发生隐式类型转换,但是转化时会产生中间变量,
在这里可以看到使用引用会报错,原因是因为这里的r引用的是隐式转换的中间变量,但是中间变量具有常性,所以需要加const才能完成转换;
那类和对象之间的转化是怎样的呢?
我们使用学生和教师类创建对象,可以看到两个相似的类同样可以发生隐式类型转换。
但是当我们使用引用的时
我们发现这里并没有产生临时对象。
那是不是内置类型隐式转换会产生临时对象,而自定义类型隐式转换不会发生隐式转换呢?
如下例
可以看到还是产生了临时对象
同样需要加上从const;
所以这里有一个结论:public继承时,父类和子类是一个is-a的关系,相当于召唤出替身,
子类的对象赋值给父类的对象、或者父类指针、或者父类引用,我们认为是天然的,中间不产生临时对象,也被称作父子类的赋值兼容规则,也叫做切片。
那is-a是什么关系呢?简单说就是:你有的,我都有
观察下图
当子类继承了父类之后,子类创建的对象可以看作是一个一个一个父类的对象,也就可以直接赋值给父类不会产生中间变量。
那这一小串代码就如下图
6.继承的作用域
父类和子类是有独立的作用域的,所以可以分别再父类和子类中定义相同的变量
虽然子类继承了父类的属性和函数,但是两者的作用域不同。那么有以下问题
class Person
{
protected:string _name = "wdd";int _num = 111;};
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << _num << endl;}
protected:int _num = 999; // 学号
};
在子类定义的打印函数中能否访问到_num?如果能访问,访问的又是谁的_num呢?
测试如下
可以看到_num访问的是Student中的属性;
和我们之前讲到的就近原则同理 。
那我们想要在子类中访问父类的属性加上作用域和访问限定符即可
可以看到还是在子类中,可以通过指定的作用域来访问父类中的属性 。
根据以上来看,我们得出结论:父类和子类可以有同名成员,因为他们是独立作用域;默认情况是直接访问子类的,子类同名成员会隐藏父类同名成员。
同样的成员函数也是如此
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;}
};
如上代码中两个func函数不是函数重载,而是函数隐藏。
之前我们说过函数重载是在同一个作用域中,函数名相同,函数的参数不同才能构成函数重载,但是如上代码中两个函数的作用域不同
可以看到我们使用B类创建了对象,并且使用其调用fun函数,调用的是B类中的fun函数;
但是我们想要去调用父类的fun函数会出现错误
可以看到它的错误为函数中的参数太少,因为父类的函数是无参的,并且与子类中的函数同名,于是便对父类中的fun函数进行了隐藏;
7.子类 / 派生类的默认成员函数
7.1默认构造
我们之前在构造函数中说过在类中不写构造函数,编译器会默认生成构造函数,在我们写了构造函数便不会生成构造函数,如此观察以下代码和运行结果
可以看到的是1.我们使用子类继承了一个父类2.使用子类创建了一个对象3.在子类中定义任何的构造函数和析构函数4.输出的结果中出现了父类的构造和析构函数
这里的原因是因为这里子类(Student)是有两部分构成的,一部分是子类本身的结构,另一部分是继承了父类的结构,接下来观察父类的构造函数和析构函数
父类内容如下
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; // 姓名
};
可以看到的是我们在父类中定义了构造函数和析构函数;
那么接下来我们在子类中写一个构造函数,观察它在创建对象时是否会使用自己的构造函数
可以看到在子类中写了构造函数之后s对象也调用了自家的构造函数。
这里需要注意的是需要初始化父类的内容时需要声明父类中的初始化内容,要将父类的内容当成一个完整的对象去初始化,而不是单个的使用初始化(与之前的匿名对象有些相似)
7.2拷贝构造
复用上面的代码我们观察拷贝构造
使用s1拷贝构造了s2,运行的结果可以看到还是会优先调父类的拷贝构造
我们可以继续在子类中定义拷贝构造的函数
可以看到这里又在子类中定义了拷贝构造函数,在输出的结果中又多了一行
这就是子类调用了自己的拷贝构造函数。
7.3析构函数及其原因
和构造函数相同,自己定义之后编译器就不会默认生成,没有自己定义编译器就会默认成生成,在上面的代码中可以看到运行的结果一直在调用父类的析构函数,析构函数同样可以自己定义
可以看到在最下方我们想要使用复用父类的析构函数却发现析构函数被调用了两次,
这是因为编译器对构造和析构是有顺序的;
我们之前都了解过构造和析构的顺序
构造的顺序是先父后子
析构的顺序是先子后父
这也是为什么上面写的子类的析构函数可以调用父类的成员函数。
简单来说如同没有你的父母就没有你
如果析构函数的顺序是先父后子的话,可能会将父类的数据空间释放调,如果子类需要用到父类的数据时父类已经先被析构,再访问父类数据会有风险。
所以为了保证析构安全,要先子后父
父类析构函数不需要显式调用,子类析构函数结束时会自动调用父类析构函数。
以上就是关于继承的基础内容,如果对你有所帮助还请三连支持,感谢您的阅读。