C++的继承法则详解
作者:成工小白
一、继承的概念和定义
1、概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
2、使用
#include <iostream> using namespace std; class Person { }; class Student : public Person { }; class Teacher : public Person { }; int main() { }
作用:是将共有的数据和方法提取出来放到父类,自己定义自动独有的成员,使其减少代码冗余。
3、定义格式
上述中:
Person:称为父类或者基类。
Student / Teacher:称为子类或者派生类。
语法为:子类声明 :继承方式 继承类名
4、继承关系和访问限定符
继承方式有三种:
访问限定符也有三种:
注意两者的区别与联系。
5、继承父类时成员访问方式的变化(继承方式与访问方式的联系)
类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
基类的 public 成员 | 派生类的public成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的 protected 成员 | 派生类的protected 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的 private 成
员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
注意:不可见不是不存在,存在但是用不了,即使是自己的成员函数也不可以访问,只有调用父类的成员函数间接访问。
6、总结
(1)、基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。
(2)、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
(3)、实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected
> private。
(4)、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强
二、父类和子类对象赋值兼容转换
1、赋值兼容也可以叫“切割”:
(1)、派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
2、赋值兼容中对引用和指针的解读:
其中,r 对象继承的是父类与子类共有的这部分数据的别名,指针也是类似。
3、赋值兼容解释
平时我们知道,C++类型转换的时候是会产生临时对象的并且该临时对象具有常属性,赋值兼容这里子类和父类也是两种类型,会不会产生临时对象呐?
答案 :这过程不会产生临时对象,这是语法规定的特殊处理,证明如下:
三、继承中的作用域
(一)、介绍:
(1)、在继承体系中基类和派生类都有独立的作用域。
(2)、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
(3)、需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
(4)、注意在实际中在继承体系里面最好不要定义同名的成员。
(二)、同名成员:隐藏
同名成员指子类中定义的变量名可以和父类中的变量名相同,这是可以的。因为子类和父类是属于两个作用域。此时子类成员会隐藏父类成员,这个过程叫 隐藏。
class Person { protected: string _name = "父类赋值"; string _sex; int _age; }; class Student : public Person { public : void fun() { cout << _name << endl; } protected: string _name = "子类赋值"; string _tem; }; int main() { Student s; s.fun(); return 0; }
如上隐藏过后,默认访问的是子类成员,此时向访问父类成员需要加上域作用限定符。
实际中,不介意设计同名成员,这无疑是自己给自己设坑。
笔试题:
解答:
答案选【d】,首先排除a、c,因为c重写是在后面多态部分的知识,所以不作讲解。
父类和子类是属于两个作用域,而重载是需要再同一作用域中才能构成重载,所以b排除,最后,如果是成员函数:子类和父类中只要函数名相同就会构成隐藏关系(与返回值和参数列表无关)。
如图,会发现调不到没有参数的fun函数,因为该函数已经被隐藏了。
四、子类的默认成员函数
(一)、介绍:
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
(二)、默认构造函数
对于默认构造函数,可以结合自定义类型思考:
(1)、子类独有成员:按照内置类型和自定义类型分别处理
(2)、父类的成员:会调用父类的构造函数。
1、如何显示写继承构造函数
class Person { public: Person(string name) { cout << "调用父类的构造" << endl; } protected: string _name ="父类成员"; }; class Student : public Person { public : void fun() { cout << Person::_name << endl; } protected: string _tem ="子类成员"; }; int main() { return 0; }
错误写法:
正确写法:
隐式写法:
有点像自定义类型的处理方式,需要调用父类的默认构造,若父类没有默认构造,则会报错。
显示写法:
class Person { public: Person(string name) { cout << "调用父类的构造" << endl; } protected: string _name = "父类成员"; }; class Student : public Person { public : Student(string name, string tem) : Person(name), _tem(tem) { cout << "调用子类的构造" << endl; } void fun() { cout << Person::_name << endl; } protected: string _tem = "子类成员"; }; int main() { Student s("1","1"); return 0; }
无论怎么写,都是先调用父类的构造函数,再调用子类自己的构造函数,因为初始化列表的初始化顺序是根据成员声明的顺序确定的,而父类的成员变量在内存中始终在前面先声明。
2、显示写拷贝构造函数
显示写拷贝构造,因为跟自定义类型处理方式类似,子类成员调用子类的拷贝构造,父类成员调用父类的拷贝构造,所以我们需要知道谁是父类的成员,这里就需要用到赋值兼容的知识,也就是切割问题,直接把子类对象传递给父类的拷贝构造,父类会自己割出自己的那部分。
Student(const Student& s): Person(s), _tem(s._tem) { cout << "调用子类的拷贝构造"; }
3、赋值重载的写法
与拷贝构造类似的,也是使用切割:
错误写法:
因为子类赋值重载与父类的赋值重载函数名相同,所以构成隐藏关系,所以这样写的话是子类自己无限递归了,这时我们要调用父类的赋值重载就需要指定作用域:
正确写法:
Student& operator = (const Student& s) { if (this != &s) { Person::operator=(s); _tem = s._tem; } return *this; }
4、析构函数的写法:
错误写法:
原因如下:
C++特殊规定:子类的析构函数和父类的析构函数会构成隐藏关系,由于后面多态的远呀,析构函数被特殊处理了,父类子类的析构函数的函数名最后都会被处理成destruuctor(),所以会构成隐藏关系。
正确写法:
既然是隐藏关系,所以指定作用域:
但我们会发现居然调用了两次析构,原因是这里又做了特殊处理:
为了保证先调用子类析构,后调用父类析构,父类的析构会在子类析构后自动调用,所以不需要我们手动调用:
~Student() { cout << "调用子类析构" << endl; }
问题:为什么要先先调用子类析构,后调用父类析构?
解答:
(1)、为了保持构造函数的调用顺序规则。
(2)、因为子类的析构函数中可能会使用父类的成员,若先析构父类,父类资源就已经清理释放,而子类析构中还可能会去访问父类成员,就可能造成野指针等问题。显示调用就无法保证一定会先子后父,所以设置成自动调用。
5、继承类模板
要继承类模板就必须显示实例化继承:
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员(理解爸爸的朋友不是我的朋友)。
六、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。 运用: 在父类的构造函数中 ++静态成员,可以统计父类和子类创建的对象个数。
七、单继承、多继承、菱形继承
1、单继承:
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
2、多继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
注意:父类用“ , ”隔开。
3、菱形继承
有了多继承就可能出现菱形继承,菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。
class Person { public: Person(string name) { cout << "调用父类的构造" << endl; } Person(const Person& p) { cout << "调用父类的拷贝构造" << endl; } Person& operator = (const Person& s) { return *this; } ~Person() { cout << "调用父类析构" << endl; } protected: string _name = "父类成员"; }; class Student : public Person { public: Student(string name, string tem) : Person(name), _tem(tem) { cout << "调用子类的构造" << endl; } Student(const Student& s) : Person(s), _tem(s._tem) { cout << "调用子类的拷贝构造"; } Student& operator = (const Student& s) { if (this != &s) { Person::operator=(s); _tem = s._tem; } return *this; } ~Student() { cout << "调用子类析构" << endl; } void fun() { cout << Person::_name << endl; } protected: string _tem = "子类成员"; }; class Teacher :public Person { }; class Assistant :Teacher, Student { }; int main() { Assistant s; s._name; return 0; }
对_name访问不明确,因为既有来自Student的_name,还有来自Teacher的_name。 第一种解决方法是: 访问_name时可以指明作用域,但是这样只能暂时解决二义性的问题,没有解决本质问题,有些信息不可能存在两份,如年龄,学号等等,这样设置就会存在空间浪费。 第二种也是最合适的解决方法:使用虚继承,如下:
解决菱形继承的问题:虚继承virtual
在造成二义性的父类(即最顶层的父类)的直接子类继承处,加个virtual关键字:
菱形继承的使用场景:
菱形问题的底层分析:
2:46:00,偏移量等等知识。
到此这篇关于探索C++的继承法则的文章就介绍到这了,更多相关C++继承法则内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!