您的位置:首页 > 文旅 > 旅游 > C++语言第八章 继承

C++语言第八章 继承

2024/10/6 20:25:22 来源:https://blog.csdn.net/Nightmare1000/article/details/142185662  浏览:    关键词:C++语言第八章 继承

面向对象的程序设计有三大特征:封装,继承,多态。在前面类和对象的文章中,我们已经初步理解了封装及它的意义。继承机制是面向对象程序设计中代码复用的重要手段,本文主要介绍继承的相关概念,定义及应用。

8.1 继承的概念及定义

8.1.1 继承的概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。只看上述继承的概念可能会一头雾水,下面我们来看一段代码,代码中有两个类,且相似度极其的高

class Student
{
public:void identity(){cout << "学生" << endl;}void studing(){cout << "学习" << endl;}
private:string _name = "Killy";string _address;string _tel;int _age;int _stuid;
};
class Teacher
{
public:void identity(){cout << "老师" << endl;}void teaching(){cout << "授课" << endl;}
private:string _name = "Ms Brown";int _age = 18;string _address; string _tel;string _title;
};

这两个类的相似度极其的高,成员变量和成员方法里都有相同的内容,这样的代码写法会显得比较冗余,使用继承机制就可以很好的解决这一问题。

8.1.2 继承定义及格式

我们可以将学生类和老师类的共同内容抽象出来一个Person类,再利用继承的机制去复用这些成员,不需要再重复定义了,代码写法如下

class Person
{
public:void identity(){cout << "void identity()" << endl;}
protected:string _name;string _address;string _tel;int _age;
};
class Teacher :public Person
{
public:void teaching(){cout << "授课" << endl;}
protected:int _title;
};
class Student :public Person
{
public:void studing(){cout << "学习" << endl;}
protected:int _stuid;
};

继承的格式可以参照下图,代码中的Person类称之为父类/基类,被继承的类称之为子类/派生类
在这里插入图片描述
在Teacher类的成员变量中,虽然只有_title变量,但由于Teacher类是继承Person的派生类,所以它还保留了原基类中的成员变量和成员函数,也可以调用并访问基类中的成员。

8.1.3 基类成员访问方式的变化

上面的继承方式选用的是public的方式继承,而在父类中,成员变量也以protected的方式进行修饰。这是因为以不同的方式继承,基类成员的访问权限也会不同。所以不同的访问限定和不同的继承方式,产生了下面9种情况
在这里插入图片描述
虽然有9种不同的情况,但最常用的方式是图片中标蓝的两种,至于原因,看完了以下5点继承的特性,就理解了。
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
通过下面的代码可以看出,在父类中可以访问私有成员和保护成员,但继承到子类之后,是无法访问父类的私有成员,想要访问,就必须将其定义为保护

class Person
{
public:void identity(){cout << "void identity()" << endl;cout << _age << endl;  //父类的私有成员,在父类中可以直接访问}
protected:string _name = "Killy";string _address;string _tel;
private:int _age;
};
class Teacher :public Person
{
public:void teaching(){cout << "授课" << endl;cout << _age << endl;  //父类的私有成员,继承后无法进行访问cout << _name << endl;  //父类的保护成员,继承后可以进行访问}
protected:int _title;
};

运行结果
在这里插入图片描述
通过VS2022的调试窗口也能发现,父类的私有成员变量是被继承下来了,但继承之后在子类中并不能访问
在这里插入图片描述
3.基类的私有成员在派生类都是不可见。基类的其它成员在派生类的访问方式 == min(成员在基类的访问限定符,继承方式),也就是两种方式中,访问权限较小的那个,其中public > protected >private。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
总结一下,虽然继承之后访问方式有9种不同的情况,但在实际运用中一般都是使用public继承,几乎很少使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

8.2 继承的其它特性

介绍完继承的概念与定义格式后,下面来介绍一下有关继承的其它语法特性。

8.2.1 基类和派生类的转换

在public继承的方式下,派生类对象可以赋值给基类的指针或者基类的引用。可以形象的理解为切片或者切割。
在这里插入图片描述
大致意思是把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。基类对象不能赋值给派生类对象。下面的代码就是把派生类的对象赋值给了指向基类的指针

#include <iostream>
#include <string>
using namespace std;
class Person
{
protected:string _name; string _sex; int _age; 
};
class Student : public Person
{
public:int _No; 
};
int main()
{Student s1; //子类对象可以赋值给父类对象/指针/引用Person* p1 = &s1;Person& p2 = s1;Person p3 = s1;//父类对象不可以赋值给子类对象,编译报错//s1 = p3;return 0;
}

8.2.2 继承中的作用域

上文提到,派生类继承了父类之后,虽然派生类中没有写明基类的成员变量和成员函数,但也可以通过派生类进行调用。如果派生类和基类中有同名的成员变量和成员函数会怎么样呢?比如看下面的代码,两个类中有相同的成员变量,读者可以先猜测一下运行结果

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
protected:string name = "Brown";int _num = 111;
};
class Student :public Person
{
public:void Print(){cout << _num << endl;}
protected:int _num = 999;
};
int main()
{Student s1;s1.Print();return 0;
}

在正式说明上述代码运行结果前,先来了解一下继承中的隐藏机制。
1.在继承体系中基类和派生类都有独立的作用域。
2.派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
所以在上面的代码中,由于基类和派生类有相同的成员变量,构成隐藏关系,基类的成员变量被派生类隐藏了,所以上述代码运行结果如下
在这里插入图片描述
但如果想在派生类成员函数中调用基类的同名成员变量,可以使用类域操作符进行显示的访问

void Print()
{cout << Person::_num << endl;
}

3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
由于函数名相同就构成隐藏,且基类和派生类所在的类域并不相通,所以下面的代码中,基类和派生类的fun()函数实际的关系为隐藏关系,不用管函数的参数列表是否相同。这点需要与函数重载的定义作区分,虽然函数重载的一个要求也是函数名相同,但必须在同一个域中,才能构成函数重载。这也是函数重载与隐藏不一样的地方。

#include <iostream>
using namespace std;
class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" << i << endl;}
};
int main()
{B b;b.fun(10);return 0;
};

运行结果
在这里插入图片描述

8.2.3 实现一个不能被继承的类

想要让一个类无法被继承,可以使用C++11中新增的final关键字,用final关键字修饰基类,这样该类就无法被继承,使用代码如下

class base final
{
public:void func(){cout << "void func()" << endl;}
protected:int _num;
};
class derive :public base
{
public:
protected:
};

运行之后会编译报错,derive类无法继承用final关键字修饰的base类。

8.3 派生类的默认成员函数

每个类中,即便我们不写,它都会自动生成的默认成员函数。那么当一个派生类继承自父类之后,该派生类的默认成员函数也会有一些新的特性。
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
那么具体的代码实现如下,两个默认的构造函数只需要在初始化列表阶段手动调用父类对应的函数,即可达到相关的目的

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:Person(const char* name = "Killy"):_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 num):Person(name)  //调用父类的构造函数,_num(num){cout << "Student()" << endl;}Student(const Student& s):Person(s)  //调用父类的拷贝构造函数,_num(s._num){cout << "Student(const Student& s)" << endl;}
protected:int _num;
};

3.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
赋值重载的实现基本跟上述两个函数一样,都需要手动调用父类的对应函数,再单独对子类成员变量进行相应处理,代码实现如下

#include <iostream>
#include <string>
using namespace std;
class Person
{
public: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& operator=(const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator=(s);  //派生类赋值给基类时的兼容转换_num = s._num;}return *this;}
protected:int _num;
};

在之前类和对象的文章中说过,如果不手动实现拷贝构造和赋值重载,系统默认生成的函数会自动完成浅拷贝。所以如果派生类中没有需要进行深拷贝的资源,系统默认生成的拷贝构造和赋值重载就已经够用了,不需要手动实现。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5.派生类对象初始化先调用基类构造再调派生类构造。
6.派生类对象析构清理先调用派生类析构再调基类的析构。
关于这三点可以运行以下代码,便可验证这些结论

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:Person(const char* name = "Killy"):_name(name){cout << "Person()" << endl;}~Person(){cout << "~Person()" << endl;}
protected:string _name;
};
class Student :public Person
{
public:Student(const char* name, int num):Person(name),_num(num){cout << "Student()" << endl;}~Student(){cout << "~Student()" << endl;}
protected:int _num;
};
int main()
{Student s1("July", 10);return 0;
}

运行结果
在这里插入图片描述
7.由于多态的一些特性,派生类析构函数和基类析构函数构成隐藏关系,想要在派生类中手动调用基类的析构函数需要指定类域
所以当一个派生类在没有资源需要释放的情况下,系统默认生成的析构函数就够用了,不需要手动去实现。

8.4 继承中的友元,静态成员变量

定义在类外部的公有函数中,我们是不能访问类的私有成员的,如果想进行访问,其中一种方法就是在对应类的内部进行函数的友元声明。如果另一个类继承了该类,友元关系会被继承吗?答案是不能,派生类并不会继承基类的友元关系,所以想继续在函数内部访问派生类的私有成员,也要在派生类内部进行友元声明,具体代码写法如下

#include <iostream>
#include <string>
using namespace std;
class Student;  //提前声明派生类,防止编译报错
class Person
{
public://在基类内进行友元声明friend void display(const Person& p, const Student& s);
protected:string name = "Jack";
};
class Student: public Person
{
public://在派生类内进行友元声明friend void display(const Person& p, const Student& s);
protected:int _num = 10;
};
void display(const Person& p,const Student& s)
{cout << p.name << endl;cout << s._num << endl;
}
int main()
{Person p;Student s;display(p, s);return 0;
}

运行结果
在这里插入图片描述
另外再来谈一谈静态成员变量,如果在基类中有定义静态成员变量,那么在整个继承体系中都只有一份静态成员变量,无论派生出多少个对象,静态成员变量始终只有一份,下面的代码很好的进行了演示

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:string _name;static int _count;
protected:};
int Person::_count = 0;
class Student :public Person
{
public:
protected:int _num;
};
int main()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << &p._count << endl;cout << &s._count << endl;  //p和s对象的_count变量共用一块地址p._count++;cout << p._count << endl;cout << s._count << endl;  //p和s对象的_count变量同时进行了改变return 0;
}

运行结果
在这里插入图片描述

8.5 多继承与菱形继承

一个派生类只有一个直接基类时称这个继承关系为单继承,有了单继承,自然也会有多继承。不过多继承是C++语言的一种缺陷,本节内容主要介绍多继承及其它所带来的一些问题。

8.5.1 多继承

一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后⾯继承的基类在后面,派生类成员在放到最后面。来看下面的代码,其中assistant类就继承了两个类,是多继承的关系

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 major_course;
};

但这不免会产生一个问题,Student类和Teacher类中都有一份Person类的成员变量,当Assintant又继承了这两个类后,Person类的数据内容也应被保存了两份,当我们想访问Person类的成员,不免会产生数据冗余和二义性的问题,会造成对成员变量访问不明确的现象,这是多继承中经典的菱形继承问题

8.5.2 菱形继承与虚继承

所谓菱形继承其实是多继承的一种特殊情况。菱形继承的问题,从上面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中其Person成员会有两份。支持多继承就一定会有菱形继承。
在这里插入图片描述
想要解决这个数据冗余及二义性的问题,就需要使用到虚继承,在公共基类继承的下一个派生类中,继承的时候加上virtual关键字,就可以解决这个问题,该模型也叫做菱形虚拟继承。下面代码的运行结果可以看出,使用virtual关键字后解决了这个数据冗余的问题

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:string _name;
};
class Student :virtual public Person
{
protected:int _num;
};
class Teacher :virtual public Person
{
protected:int _id;
};
class Assistant :public Student, public Teacher
{
protected:string major_course;
};
int main()
{Assistant a;a._name = "Jackeylove";cout << a._name << endl;return 0;
}

通过VS2022的监视窗口可以观察到这一现象
在这里插入图片描述
但即便如此,也不建议设计出菱形继承,菱形继承所带来的问题绝对不是我们可以想象的到的。

8.5.3 多继承中的指针偏移问题

下面来看一段代码,读者可以大概猜测一下指针p1,p2,p3之间的关系是什么

#include <iostream>
using namespace std;
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

正确答案是p1==p3>p2,通过VS2022的监视窗口可以观察到
在这里插入图片描述
因为多继承会先继承声明中的第一个基类,再继承第二个基类,所以这个多继承的模型及其指针的指向如下图所示
在这里插入图片描述
如果代码中继承基类的顺序进行了改变,那么这个结果也会发生改变

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
//继承基类的顺序发生了改变
class Derive : public Base2, public Base1 { public: int _d; };
int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

运行结果
在这里插入图片描述
以上便是关于C++中有关继承的所有语法点,如有遗漏,欢迎补充。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com