中国工程建设质量管理协会网站,大型网站制作公司飞数,公司建立网站流程,深圳网站建设找哪家#x1f431;作者#xff1a;一只大喵咪1201 #x1f431;专栏#xff1a;《C学习》 #x1f525;格言#xff1a;你只管努力#xff0c;剩下的交给时间#xff01; C是面向对象的编程语言#xff0c;它有很多的特性#xff0c;但是最重要的就是封装#xff0c;继承… 作者一只大喵咪1201 专栏《C学习》 格言你只管努力剩下的交给时间 C是面向对象的编程语言它有很多的特性但是最重要的就是封装继承多态三大特性封装本喵就不介绍了前面我们一直都在使用这里本喵来详细介绍继承。 继承继承的概念和定义继承关系和访问限定符基类和派生类的赋值转换特性继承中的作用域派生类的默认成员函数构造函数拷贝构造函数赋值运算符重载函数析构函数继承与友元继承与静态成员菱形继承多继承虚拟继承继承和组合总结继承的概念和定义 继承是面向对象程序设计使代码可以复用的最重要手段它运行程序员在保持原有类特性的基础上进程扩展。继承是类设计层次的复用。 如上图这几个类都是在描述人扮演的不同角色分别是学生老师其他职业的人。
每一个角色在描述的时候都有自己特有的属性学生有学号班级老师有工号和所教科目其他职业的人也有工号和具体的岗位。 但是不同的角色之间也有共同的属性比如姓名性别年龄。 将那些无论扮演什么角色都必须有的属性看作是人共有的属性。 此时在使用不同类型的类描述不同的角色时都需要复用到人的属性。为了方便创建一个Person的类型来描述人在描述不同角色创建新的类时只需要增加Person这个成员即可然后再添加各自的属性。
下面3个蓝色框中的类复用红色框Person这个类的过程就叫做继承。
具体到代码上来看
class Person
{
public:void Print(){cout _name endl;cout _age endl;}
protected:string _name;//姓名int _age;//年龄
};//继承
class Student : public Person
{
protected:int _stuid;//学号
};Student类继承了Person类。 Student称为基类也叫做父类。Person称为派生类也叫做子类。public是继承方式。 在创建了Student对象后该对象中不仅有class Student中的_stuid成员还有class Person中的_name,_age成员以及class Person中的Print()成员函数。 基类中的一切都被派生类继承了下来。 继承关系和访问限定符 访问限定符之前本喵讲解过继承方式也是有这三种。使用不同继承方式继承不同权限的基类成员排列组合后共有9中继承结果。
类成员\继承方式public继承protected继承private继承基类的public成员派生类的public成员派生类的protected成员派生类的private成员基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见
看起来非常复杂但是有规律可循。
基类中的private成员 Person中是private成员。 在Student继承Person的时候分别采用publicprotectedprivate三种继承方式。
在调试窗口中可以看到每个派生类对象中是有基类成员的但是基类成员的前面有个小锁(途中可能看不清)表示派生类中的基类成员无法访问。
派生类对象在访问基类成员的时候编译器会报不可访问的错误。
基类中的private成员无论以什么方式继承都是不可见的。 这里的不可见是指基类的成员仍然被继承到了派生类中但是在语法上不让派生类去访问。无论是在派生类的内部访问还是外部访问都是不可以的。 基类中的protceted成员 Person中是protected成员。 在Student继承Person的时候分别采用publicprotectedprivate三种继承方式。
在调试窗口中可以看到每个派生类中是有基类成员的而且没有小锁说明此时在派生类中的基类成员是可见的也就是可以访问的。 用派生类的成员函数是可以访问基类中的成员的。在派生类的外部访问基类成员会报错。 无论什么继承方式在派生类的内部是可以访问基类中的保护成员的但是在派生类的外部不能访问基类的保护成员。
基类中的public成员 Person中是public成员。 在Student继承Person的时候分别采用publicprotectedprivate三种继承方式。
在调试窗口中可以看到派生类中有基类的成员并且没有小锁说明是可见的。 派生类内部都可以访问基类的成员。派生类外部 共有继承的公有成员派生类外部可以访问基类成员。 保护继承的公有成员派生类外部不可以访问基类成员。 私有继承的公有成员派生类外部不可以访问基类成员。 无论哪种继承方式派生类内部都可以访问基类公有成员保护继承和私有继承派生类只能在内部访问基类成员。
规律总结
在刚学习类和对象的时候本喵说过将访问限定符private和protected暂时看成是一样的现在就可以介绍他俩的区别了。
三个访问限定符的访问权限大小关系如下 访问权限public protected private 继承关系和访问限定符的组合可以分为两大类 基类中是private成员无论哪种继承方式基类成员对于派生类都是不可见的不可访问。 基类中的其他权限成员继承方式和基类成员访问限定符二者取权限小的作为派生类中成员的权限。
例如基类中是protected成员使用public继承方式那么继承下来的基类成员在派生类中的访问权限就是protected。而此时protected就和private是一样了在类的内部可以访问在类的外部无法访问。
再例如基类中的public成员使用private继承方式那么继承下来的基类成员在派生类中的访问限定符就是private。
可以看出protected访问限定符就是因为继承才出现的。所以protected和private的区别在继承中才得以体现。 在实际运用中一般使用都是public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承。 因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强。 继承方式可以省略不写采用默认继承方式 class定义的类内部如果不写访问限定符成员的默认权限是private。class定义的派生类如果不写继承方式默认的继承方式也是private。 struct定义的类内部如果不写访问限定符所有成员的默认访问权限是public。struct定义的派生类如果不写继承方式默认的继承方式也是public。 不过最好还是显示写出继承方式。
基类和派生类的赋值转换 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。 class Person
{
public:void Print(){}
protected:string _name;//姓名string _sex;//性别int _age;//年龄
};class Student : public Person
{
public:int _No;
};基类和派生类如上。 可以看到派生类对象赋值给基类的对象/基类的指针/基类的引用全部都可以。 派生类赋值给基类的原理如上图所示。派生类中基类有的变量保留其余的舍弃_No就是派生类特有的所以在赋值的时候会舍弃。 这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切来赋值过去。 特性 派生类对象赋值给基类对象不存在类型转换。 double类型的变量赋值给int就会报错这是因为 类型转换时会产生中间变量double值会先赋给int类型的临时变量再将临时变量的值赋给int类型的变量。 上面代码中int变量是临时变量的引用而临时变量具有常性此时相当于权限放大了所以会报错。 在引用变量前加const就可以解决这个问题。
而派生类对象赋值给基类的引用时就没有这个问题可以之间赋值所有说派生类对象赋值给基类对象不存在类型转换。
没有类型转换可以很大程度上节省系统的开销。
注意 基类对象不能赋值给派生类对象。
继承中的作用域
class Person
{
protected:string _name 张三;int _num 150;
};class Student : public Person
{
public:void Printf(){cout 姓名 _name endl;cout 身份证号 _num endl;cout 学号 _num endl;}
protected:int _num 370;
};基类和派生类中都有变量_num在派生类的成员函数中既想打印基类中的_num也想打印派生类中的_num。 派生类对象中既有自己的_num也有基类中的_num。在使用的时候直接使用_num时发现使用的是派生类中的_num。 派生类中的_num和基类中的_num属于不同作用域。 默认情况下使用的是派生类中的_num。
当基类和派生类中的成员变量名相同时基类中的成员变量会被隐藏也叫做重定义。 要想使用被隐藏的基类中成员变量需要在变量前加上域名和域作用限定符(显示访问)。 基类和派生类中各有一个成员函数而且成员函数名相同。此时这俩个函数之间构成的关系是隐藏/重定义而不是函数重载。 函数重载的前提是同名函数在同一个作用域中。 此时基类和派生类中的同名函数显然不在同一个作用域所以不能构成重载同样是隐藏关系。 默认情况下同样调用的是派生类中的成员函数。 要想使用基类中被隐藏的成员函数也是需要加域名和域作用限定符(显示访问)。
注意 如果是成员函数的隐藏只需要函数名相同就构成隐藏因为作用域不同不构成重载。 在继承体系里面最好不要定义同名的成员。
派生类的默认成员函数
先回顾一下普通类的默认成员函数 派生类同样只看四个默认成员函数。
构造函数
class Person
{
public:Person(const char* name 张三):_name(name){cout Person(const char* name \张三\) endl;}
protected:string _name;
};class Student : public Person
{
protected:int _num;//学号
};Person有显示定义的默认构造函数Student没有显示定义的默认构造函数。 在创建派生类对象的时候发现基类的默认构造函数被调用了。 在派生类中显示定义默认构造函数在创建派生类对象的时候发现先调用了基类的默认构造函数再调用了派生类的默认构造函数。
我们知道派生类中继承了基类中的成员那么能不能在派生类的构造函数中去初始化基类的成员呢 此时就报错了说明派生类的构造函数不能直接去初始化基类的成员。 但是可以在派生类的构造函数中显示调用基类的构造函数来初始化基类。 当基类中没有默认构造函数时必须在派生类的构造函数中显示调用基类的构造函数进行初始化。
结论
派生类对象在创建的时候先调用基类的构造函数来初始化基类在派生类中的成员再调用派生类的构造函数初始化自己的成员。派生类只能通过显示调用基类的构造函数来控制基类初始化不能直接去初始化从基类中继承下来的成员。如果基类中没有默认构造函数派生类必须在构造函数中显示调用基类的构造函数并且传值。
派生类相比于普通类的构造函数多了一步对基类成员的处理。
拷贝构造函数
class Person
{
public:Person(){}//默认构造函数//拷贝构造函数Person(const Person p):_name(p._name){cout Person(const Person p) endl;}
protected:string _name;
};class Student : public Person
{
public:Student(){}//默认构造函数//拷贝构造函数Student(const Student s):_num(s._num), Person(s){cout Student(const Student s) endl;}
protected:int _num;//学号
};拷贝构造函数是构造函数的重载所以它们的特性几乎是一样的。 派生类的拷贝构造函数先调用基类的拷贝构造函数。派生类的拷贝构造函数不能直接处理基类的成员必须显示调用基类的拷贝构造函数。 派生类的拷贝构造函数在显示调用基类的拷贝构造函数时传的值是派生类对象。 基类的拷贝构造函数的形参是基类对象。 类型不同但是没有发生类型转换而是发生了切割。
赋值运算符重载函数
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;//学号
};派生类的赋值运算符重载函数中调用基类的赋值运算符重载函数赋值从基类继承下来的那部分成员然后再初始化自己的、这里同样发生了派生对象给基类对象赋值时的切割现象。 由于基类的运算符重载函数是在派生类的运算符重载函数内部调用的所以在给派生类对象赋值时会先调用派生类的在派生类运算符重载函数中再调用基类的。 同样不可以在派生类的运算符重载函数中自行处理基类的成员必须使用基类的运算符重载函数去处理。 基类和派生类的运算符重载函数构造了隐藏/重定义。 在派生类中调用基类的operator()时必须指明作用域否则会默认调用派生类的此时就会造成栈溢出。
析构函数 按照之前几个默认成员函数的做法在派生类的析构函数中显示调用基类的析构函数但是发现基类的析构函数一共调用了两次。
这显然是不行的一块动态空间只能被释放一次。
class Person
{
public:~Person(){cout ~Person() endl;}
protected:string _name;
};class Student : public Person
{
public:~Student(){cout ~Student() endl;}
protected:int _num;//学号
};在派生类的析构函数中没有显示调用基类的析构函数发现父类和子类的析构函数各调用了一次。 析构函数第一怪派生类析构函数会自动调用基类析构函数这是必然发生的所以无需显示调用基类的析构函数。 先调用派生类的析构函数再调用基类的析构函数。这样做是为了保证先清理派生类成员再清理基类成员。 析构函数第二怪派生类析构函数和基类析构函数构成隐藏关系。(由于多态关系需求所有析构函数都会特殊处理成destruct函数名以后会讲解)。 派生类相比于普通类的四类默认成员函数多了一步对基类成员的处理而且只能通过基类的默认成员函数去处理不能由派生类自行处理。 上图表示了派生类和基类对象的行为。
继承与友元 定义一个Display函数它是基类的友元函数可以访问基类内部的保护成员。 由于基类中的友元声明中包含派生类但是编译器只会向上寻找所以必须在友元声明之前加上派生类的声明。否则会报Student未声明的错误。 在调用基类的友元函数访问派生类中的成员时报错了不让访问。因为友元关系不继承。 若想让基类中的友元也成为派生类中的友元需要在派生类中也进行友元声明。
注意 一般不建议使用友元因为它会破坏类的封装。
继承与静态成员
class Person
{
public:int _Pnum 1;static int _cout;
};int Person::_cout 0;class Student : public Person
{
public:int _Snum 1;//学号
};基类中有一个int类型的变量有一个static变量派生类中自己有一个int类型的变量。 创建两个对象一个基类对象一个派生类对象。 派生类会继承基类中的成员所以基类对象和派生类对象中都有成员变量_Pnum。
两个对象创建后分别将基类对象和派生类对象中的_Pnum打印出来发现它们的值是一样的。 将基类对象中的_Pnum加1然后打印出来发现值加1。将派生类对象中的_Pnum加1然后打印出来发现值加1。 基类对象和派生类对象中的_Pnum中的值是相互独立的也就是有两个值。 派生类同样会继承基类中的静态变量。 基类对象中对静态变量加1发现值加1.派生类对象中对静态变量加1发现值相对于刚创建时加了2. 也就是说基类对象对静态变量的值加1同样作用到了派生类对象中的静态变量上。 它们两个对象中的静态变量不是独立的。
在基类对象和派生类对象中的静态变量是同一个变量。
因为静态变量同样存放在数据段基类定义了static静态成员则整个继承体系里面只有一个这样的成员。无论派生出多少个子类都只有一个static成员实例。
菱形继承
多继承
首先需要知道的是单继承一个子类只有一个直接父类时称这个继承关系为单继承。 如上图虽然最后的子类中有不止一个父类中的成员但是每个子类都只有一个父类。
多继承一个子类有两个或以上直接父类时称这个继承关系为多继承。 如上图子类只有一个但是父类有两个这种就属于多继承。 多继承中一个子类可以有多个父类。
但是由于多继承的存在就会引起菱形继承的问题。 Student继承自PersonTeacher也继承自Person。 Assistant继承自Student和Teacher。
class Person
{
public:string _name;//姓名
};class Student : public Person
{
public:int _num;//学号
};class Teacher : public Person
{
public:int _id;//工号
};class Assistant : public Student, public Teacher
{
public:string _majorCourse;
};在设计上菱形继承完全是合理的学生和老师属于人所以继承人的属性而助教既是老师也是学生所以继承老师和学生的属性。
但是菱形继承会存在问题。 从上面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
数据冗余 可以看到Assistant中有两个_name成员变量一个是继承自Student的一个是继承自Teaher的但是都是继承自Person的。 也就是相同的值存在了两份实际上只有一个就够用了。
二义性 如上图所示在访问_name的时候编译器也不知道你要访问的是Student作用域中的还是Teacher作用域中的所以就造成了二义性。
二义性的解决办法之一 可以通过指定作用域的方法来解决二义性的问题如上图所示但是并不符合实际情况一个人虽然有多种角色但是名字怎么会有两个甚至多个呢
虚拟继承
虚拟继承就是专门用来解决菱形继承导致的数据冗余和二义性问题的。 在腰部位置使用虚拟继承(virtual)。 哪里造成了菱形继承的问题就在哪里使用虚拟继承毫无疑问是在腰部位置造成的所以腰部的两个派生类都使用虚拟继承。
下面本喵来给大家分析一下虚拟继承是如果解决数据冗余和二义性问题的。
为了方便分析我们创建几个简单的类
class A
{
public:int _a;
};class B : virtual public A
{
public:int _b;
};class C : virtual public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};上面代码菱形继承的关系如上图所示。
数据冗余和二义性的解决
首先我们来看不使用虚拟继承时的内存模型 在D对象创建后通过内存窗口来看它内部的成员分别情况如上图所示。 最外边的蓝色框是整个d对象它一共有5个int类型的变量。中间的红色框中的成员是从B中继承下来的有两个int类型的变量。中间的律师框中的成员是从C中继承下来的有两个int类型的变量。红色框和绿色框中橘色细框中的变量都是从A中继承下来的。 此时可以清除的看到d对象中有两个_a变量分布在B域和C域中。
再看使用菱形继承后的内存模型 最外部的蓝色框是整个d对象他一共有6个四字节的数据。中间的红色框中的成员是从B继承下来的有2个四字节的数据。中间的绿色框中的成员是从C继承下来的有2个四字节的数据。最下边的红色细框中的变量是从A继承下来的。 先不管多了什么东西单看从A中继承下来的变量发现只有一个了。从原理的两个变成了一个解决了数据冗余的问题。
菱形继承中原本冗余的成员最后只有一个而且放在最终派生类对象中的最后位置。
此时数据冗余和二义性是解决了因为派生类对象中只有一个从A继承下来的成员了但是相比原来不用虚拟继承多出来4个字节不说还将原本是成员所在位置变成了奇奇怪怪的东西。 B中黄色框中地址中的数据是一个地址在新内存窗口中输入该地址得到一个新的黄色框。 C中紫色框中地址中的数据是一个地址在新内存窗口中输入该地址得到一个新的紫色框。 本喵用的机器是小端存储方式按照小端模式得到d对象中存放的两个地址。 在两个新内存窗口中看到的两个新的框被称为虚基表。
黄色虚基表 虚基表中第一个int类型的数据存放的是0具体什么意义在多态的时候再讲。 虚基表中第二个int类型的数据存放的是0x14它是一个偏移量。
再看d对象的内存模型 从B继承下来的成员起始地址是0x0055FA90。 从A继承下来的成员它的地址是0x0055FAA4。
这两个地址之间相差0x14也就是20。 当使用d.B::_a来访问A继承下来的成员时就从B继承下来的成员的起始地址处根据偏移量去访问具体的_a。 紫色虚基表 虚基表中第二个int类型的数据存放的是0x0c同样是一个偏移量。
再看d对象的内存模型 从C继承下来的成员起始地址是0x0055FA98。 从A继承下来的成员它的地址是0x0055FAA4。
这两个地址之间相差0x0c也就是12。 当使用d.C::_a来访问A继承下来的成员时就从C继承下来的成员的起始地址处根据偏移量去访问具体的_a。 再看派生类对象的内存窗口 B区域和A的偏移量是20C区域和A的偏移量是12。 从汇编中也可看到使用d.B::_a和d.C::_a步骤都比直接访问其他成员多因为这两种方式虽然访问的都是一个地址但是需要根据偏移量去计算。 由于使用了虚拟继承所以B对象和C对象同样采用有虚基表的结构将从A继承下来的成员放在最后原本的位置存放对应虚基表的地址虚基表中存放偏移量。
虚基表存在的原因
现在有个疑问为什么要根据偏移量来找从A中继承下来的那个成员B对象C对象或者是D对象它们自己肯定会知道自己成员的位置啊。 B的指针拿到的是对象b的地址时解引用访问_a此时只是在自己内部寻找不用偏移量也可以理解。 B的指针拿到的是对象d的地址时此时会发生切片但是d中的_a仍然会保留下来但是此时站在B指针的角度来看它根部不知道_a在哪里因为这是d对象安排的。 所以此时就需要通过虚基表获取_a距离B的偏移量来访问_a。
所以虚基表还是很有必要的。这部分只要了解就好。
继承和组合
组合我们之前其实一直都在使用就是在一个类中它的成员变量是其他类。继承和组合都是一种类设计层面的复用方式。 此时Student中同样有Person的成员变量但是不是通过继承得到的而是通过组合得到的。 public继承是一种is-a的关系。 如上图中学生是人所以Student和Person就用继承关系比较好。 组合是一种has-a的关系。 如上图中头上有眼睛而不能说成头是眼睛所以此时用组合更合适。
比较 继承方式派生类和基类耦合度比较高。基类中的成员变量发生改变后对派生类的影响较大除去private成员外其他成员的改变都会影响派生类。 组合方式最终的类和被组合的类耦合度比较低。被组合类中成员变量发生改变后对最终类的影响较小只有public成员改变才会影响最终的类。 继承方式中派生类中可以看到基类的所有成员只是基类的private成员不可以访问所以被称为白箱复用。组合方式中最终类只能看到被组合类的public成员其他成员和细节都看不到也无法访问所以被称为黑箱复用。 结论 在继承和组合都可以使用的情况下尽量使用组合而不用继承。
总结
很多人说C语法复杂其实多继承就是一个体现。有了多继承就存在菱形继承有了菱形继承就有菱形虚拟继承底层实现就很复杂。所以一般不建议设计出多继承一定不要设计出菱形继承。否则在复杂度及性能上都有问题。掌握好继承的各种特性对于后面的多态非常有帮助。