C++--继承
一,引言
C++语言的三大特性;封装,继承,多态。继承作为C++⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有 类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承 呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。下面将从一下几个方面来介绍继承的用法以及使用;1,继承的定义;2,继承类模板;3,父类与子类间的转化;4,继承中的作用域;5,子类的默认成员函数;6继承和组合这六大类进行详细讲解。
二,继承的定义
首先来看一组例子:
class Student
{
public:void identity(){//身份认证操作}protected:string _name = "peter"; // 姓名string _address;// 地址string _tel;//电话int _age = 18;//年龄int _stuid;//学号
};
这是一个学生类,成员函数的作用,用于识别学生在校的一系列操作,成员函数有一下几种。修饰类型文章下方会专门讲到。
class Teacher
{
public:void identity(){//身份认证操作}void Teachering(){//讲课}
protected:string _name = "peter"; // 姓名string _address;// 地址string _tel;//电话int _age = 18;//年龄string _title;//职称
};
这是一个老师的类,里面有成员函数,以及成员变量。会发现学生类和老师类有许多相同的部分。为此定义一个Person类,将两者共有的成员函数以及变量定义到这个公共类中,就省去了在各种的类中定义的麻烦。如下:
class Person
{
public:// 进⼊校园//图书馆,实验室刷⼆维码等⾝份认证void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "张三"; // 姓名string _address;// 地址string _tel;int _age = 18;
};
在公共类成员中有对人属性的基本特性如姓名,地址,电话年龄等等。
class Student : public Person
{
public:void study(){//学习相关}
protected:int _stuid; //学号
};
学生类继承了person类中的成员函数和成员变量。因此在学生类中,只需要写自己所特有的成员函数和成员变量。
class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:string title;
};
在老师这个类也一致。
1,定义格式
引出继承的定义规则如下:
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以 既叫基类/派⽣类,也叫⽗类/⼦类)。
在继承的学习中出现了一种新的方式,叫做继承方式。在类中修饰成员函数或变量的叫做限定符。
2,继承基类成员访问⽅式的变化
1,基类的private成员在派生类中都是不可见的,也就是说基类的private成员不管在派生类还是类外都是无法被访问的。但是这并不代表这基类的成员没有被继承下来,只是由于语法的限定,基类的成员在该修饰限定符的限定下无法访问。
2,如果基类的成员变量想要被派生类所访问但是不想类外访问就可以定义成protect。得出protect是因为继承才出现的。
3,在通过对图表的观察,可以得出基类的私有成员在派⽣类都是不可⻅。派生类的访问权限在继承关系和修饰限定符之间去小值;public>protect>private。
4,class修饰的类默认继承方式为private;struct修饰的类默认继承方式为public。但是通常在继承的时候还是要写明继承方式。
5,在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。
通过上面的例子可以通过改变继承方式或者改变限定修饰符来实现上述的不同情况,这里就不一一进行演示。
三,继承类模板
在学习模板这一章节,有学到类模板。在进行栈和队列的模拟实践中都使用了类模板具体内容访问如下链接C++--Stack的模拟实现-CSDN博客。在进行栈的模拟实现过程中。通过对vector的复用来对栈的接口进行封装。看似实现的是栈对应的接口,实际上底层是vector数据结构进行实现。而在继承的介绍中讲到,继承通过对父类的继承,也可以实现对父类成员函数的复用。为此通过继承也可以实现栈。如下:
template<class T>
class stack : public vector<T>
{
public:void push(const T& x){vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}
};
我们可以发现与用组合的方式相比。通过继承的方式,每一个成员函数的调用的vector接口 都需要指定是在vector<T>中访问的对应接口。而使用组合的时候缺不需要。这是由于在类模板在调用的时候是按需实例化;也就是说只有当用户调用对应的接口之后才会实例化对应的接口。引出如果不指定类域在之后的调用中就会找不到对应的成员函数。
四,父类与子类间的转化
1,public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
2,基类对象不能赋值给派⽣类对象。
3,基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-TimeType Information)的dynamic_cast 来进⾏识别后进⾏安全转换。
在上述过程中,并不存在类型的转化。为什么说在第三点的指针或者引出可以通过强制类型进行转换。因为该基类的指针有可能本来就是指向派生类。因此派生类指向派生类就可以进行转换。举例子:
class Person
{
//protected:virtual void func(){}
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:int _No; // 学号
};int main()
{Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;rp._name = "张三";return 0;
}
五,继承中的作用域
1,在继承体系中基类和派⽣类都有独⽴的作⽤域。
2,派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)
3,需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4,注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
针对隐藏情况需要注意的是,因此并不是不继承,只是继承之后不显示,只有通过指定类域的方式进行访问。如若不指定访问,派生类是找不到和基类相同的哪个成员函数或者成员变量的。
下面有一道题来考察对继承的作用域:
可以思考一下选哪个答案。
第一题构成隐藏,第二题编译报错。这里回顾一下函数重载的条件1,在相同的作用域2,函数名相同参数类型不同或者个数不同构成函数重载。第一题由于两者不在同一作用域所有不构成重载,构成隐藏。第二题由于构成隐藏,所有不指定类域找不到基类的成员函数所有会编译报错。
六,子类的默认成员函数
默认成员函数一共有6种,重要的前四种:默认构造,拷贝构造,析构函数,赋值重载。
在学习类和对象的时候;数据类型分为两种1,内置类型数据 2,自定义类型数据。而在继承中派生类的成员变量除了上述两种类型之外,还多一种基类成员。因此在默认构造的初始化和类和对象基本上一致,要注意基类成员会自动调用基类的默认构造,其余都和原本一致。
1,派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。如下:
class Person
{
public:Person(const char* name ): _name(name){cout << "Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num, const char* addrss):Person(name), _num(num), _addrss(addrss){}protected:int _num = 1; //学号string _addrss = "西安市高新区";int* _ptr = new int[10];
};
此时由于基类没有默认构造,由此派生类就需要初始化列表进行显示调用。
默认函数的构造行为:
1,内置类型->编译器自行处理
2,自定义类型->调用它们本身的默认构造
3,继承父类的成员看成一个整体对象->调用父类的默认构造
2,派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。如下:
class Person
{
public:Person(const char* name ): _name(name){cout << "Person()" << endl;}Person(const Person& p){_name = p._name;cout << "Person& p" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num, const char* addrss):Person(name), _num(num), _addrss(addrss){}Student(const Student& s):Person(s), _num(s._num), _addrss(s._addrss){// 深拷贝}protected:int _num = 1; //学号string _addrss; //地址;};
当派生类有开辟新的空间需要深拷贝,否则不需要深拷贝。拷贝构造和默认构造相似。
3,派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。
class Person
{
public:Person(const char* name ): _name(name){cout << "Person()" << endl;}Person(const Person& p){_name = p._name;cout << "Person& p" << endl;}Person& operator=(const Person& p){cout << "operator=" << endl;_name = p._name;return *this;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num, const char* addrss):Person(name), _num(num), _addrss(addrss){}Student(const Student& s):Person(s), _num(s._num), _addrss(s._addrss){// 深拷贝}Student& operator=(const Student& s){_num = s._num;_addrss = s._addrss;Person::operator=(s);return *this;}protected:int _num = 1; //学号string _addrss = "西安市高新区";
};
此时要注意,派生类的赋值运算符重载和基类的运算符重载构成隐藏关系,所以一定要指定类域进行访问。
4,派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派 ⽣类对象先清理派⽣类成员再清理基类成员的顺序。这里要注意:前几种都需要指定调用。析构函数不需要指定调用。编译器会自动调用基类的构造函数。
七,继承和组合
1,public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
2,组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
3,继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可 ⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依 赖关系很强,耦合度⾼。
4,对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-boxreuse), 因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。组合类之间没有很强的依赖关 系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
总的来说优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
八,总结
继承中还有一些友元和继承的关系,以及多继承和菱形继承,继承和静态成员之间的关系。由于平时的使用场景并不多,这里就不详细讲解。注意注意的是C++虽然有菱形继承但是并不建议使用,会造成数据冗余和二义性的问题。