家用电器网站建设,网站备案核验单清晰,wordpress英雄联盟,wordpress系统邮件设置关于我#xff1a; 睡觉待开机#xff1a;个人主页 个人专栏: 《优选算法》《C语言》《CPP》 生活的理想#xff0c;就是为了理想的生活! 作者留言
PDF版免费提供#xff1a;倘若有需要#xff0c;想拿我写的博客进行学习和交流#xff0c;可以私信我将免费提供PDF版。…关于我 睡觉待开机个人主页 个人专栏: 《优选算法》《C语言》《CPP》 生活的理想就是为了理想的生活! 作者留言
PDF版免费提供倘若有需要想拿我写的博客进行学习和交流可以私信我将免费提供PDF版。 留下你的建议倘若你发现本文中的内容和配图有任何错误或改进建议请直接评论或者私信。 倡导提问与交流关于本文任何不明之处请及时评论和私信看到即回复。 参考目录 1.前言2.回顾封装的概念3.继承的概念4.继承的定义4.1继承的定义格式4.2继承权限 5.父类和子类对象赋值转换6.继承种的作用域7.子类的默认成员函数7.1构成函数7.2拷贝构造函数7.3赋值运算符重载函数7.4析构函数 8.继承与友元函数9.继承与静态成员10.菱形继承10.1单继承与多继承10.2多继承中的菱形继承问题及虚拟继承10.3菱形继承中虚拟继承的原理10.4在继承公共基类的时候使用虚继承10.5腰部父类也使用虚继承模型10.6菱形继承的实例 11.组合与继承 1.前言
我们知道CPP有三大特性封装、继承和多态。在本文中我们将简单回顾一下封装的理解由浅到深的去了解继承的相关语法和一些高级语法。
好那话不多说让我们开始吧。
2.回顾封装的概念
什么是封装呢 实际上这是一个CPP概念理解中一个挺重要的概念之一请你简谈一下对CPP封装语法的理解。
我是这样理解的以下回答仅供参考 从语法的角度就是把数据和函数方法进行合并并用访问限定符修饰加以限定。 从上下层角度就是把一个类放到另一个类里面通过typedef的方式封装成为一个全新的类型。 这个类型从上层使用来说可能会与一些其他类型使用保持一致但是底层差异很大比如我们之前接触过的deque和vectordeque的底层完全不是一个连续的空间但是通过封装的方式使得deque得使用与vector差别不大。再比如说我们之前使用正向迭代器封装成为反向迭代器反向迭代器虽然在使用上与与正向迭代器一致实际上就是给正向迭代器套了一层壳。
好的以上便是我对封装得一点简单理解。下面我们开始谈继承得相关概念~我先说继承是一种特殊得代码复用得一种形式。 谈到代码复用我们前面学过一些代码复用简单来回顾一下
函数逻辑的代码复用针对类/函数大体逻辑相似只有类型不同的模板代码复用继承是一种类层次设计上的代码复用
那我们现在正式进入介绍继承语法这一阶段。
3.继承的概念
继承类层次设计的一种代码复用。
如何理解继承的概念呢举个比较形象的例子你父亲的东西你可以拿过来用这就是你继承了你父亲的一些东西。比如说你长大了是不是可以继承一些你父亲的财产你父亲的技术哈哈(仅仅是举个例子)。代码也是同理我们可以定义一个父类然后去让别的类继承比如说上图中我们下面定义的学生类、老师类、导员类都是可以去继承人类这个类的继承了有什么好处呢很多重复的类代码就不用自己去写了呗本质就是一种代码复用。比如说学生、老师、导员都可以继承人类的名字这样就不用再在学生类、老师类、导员类中每个中都去定义一个名字变量了对不对。
如此好用的继承那该如何定义呢下面来简单进行介绍。
4.继承的定义
4.1继承的定义格式
先来说一下继承的定义格式 主要是在一个一般的类声明的基础上在后面跟个冒号然后写继承方式(public/protected/private)然后再去写明继承父类的名字就好了。
总结下来就是class 继承子类名称 : 继承方式 继承父类名称
是不是语法设置很简单呢但是在这里我们提到了继承方式那啥是继承方式呢 继承方式指的是继承子类打算以什么方式去继承这个父类的一些成员继承方式有下面三种public、private、protected三种继承方式。 除此之外父类中每个成员都会访问限定符进行修饰。 继承方式和每个成员的访问限定符共同决定了子类中到底继承到的成员具有什么权限。 继承方式有三种每个父类成员的访问限定符又有三种所以组合起来一共有9种情况。情况比较多待我一一道来。
4.2继承权限
前面提过子类成员继承成员权限 父类成员修饰限定符 继承方式共同决定。
我总结了下面表格 可能有些同学会对这个表格的一些内容感到不太理解没关系下面我挨个说明挨个去举例。
class F
{
private:void PriTest(){cout F:PriTest() endl;}
protected:void ProTest(){cout F:ProTest() endl;}
public:void PubTest(){cout F:PubTest() endl;}
};class PriS : private F
{
public:void PriSTest_Pri(){//PriTest();//父类私有成员私有继承类内不能访问}void PriSTest_Pro(){ProTest();//父类保护成员私有继承类内可以访问}void PriSTest_Pub(){PubTest();//父类公共成员私有继承类内可以访问}
};
void PriSTest()
{PriS pris;//对于私有继承所有父类成员均不可在类外访问
}class ProS : protected F
{
public:void ProSTest_Pri(){//PriTest();//父类私有成员保护继承类内不能访问}void ProSTest_Pro(){ProTest();//父类保护成员保护继承类内可以访问}void ProSTest_Pub(){PubTest();//父类公共成员保护继承类内可以访问}
};
void ProSTest()
{ProS pros;//对于保护继承所有父类成员均不可在类外访问
}class PubS : public F
{
public:void PubSTest_Pri(){//PriTest();//父类私有成员保护继承类内不能访问}void PubSTest_Pro(){ProTest();//父类保护成员保护继承类内可以访问}void PubSTest_Pub(){PubTest();//父类公共成员保护继承类内可以访问}
};
void PubSTest()
{PubS pubs;pubs.PubTest();//对于公共继承所有父类成员种只有公共成员才可在类外访问
}
void test1()
{PriSTest();ProSTest();PubSTest();
}可能有些同学还是不太能理解虽然上面附了一些代码… 那我直接总结了一些规律来供大家快速理解上面表格。
对于保护访问限定符的理解 protected是针对于CPP继承语法而诞生的。 protected所修饰的父类成员允许在子类中使用但是不允许在子类类外使用。私有继承和私有成员的理解 私有继承继承方式是private的继承私有成员被private修饰符所修饰的类成员。 私有继承对父类的public、protected修饰的成员是可见的。但是任何继承方式对于父类种private修饰的成员是不可见的。继承访问限定的确定 对于不是父类私有的成员我们可以取其继承方式和权限修饰限定符的权限较小者。比如说继承方式是protected对于父类中的public成员那么继承下来的就是protected权限。struct和class默认继承 其实针对于struct和class继承是可以进行默认继承的就是写继承定义语法的时候可以省略继承方式。对于struct默认继承方式是public继承对于class默认就是private继承。
这里我们不妨来做个引申CPP中struct与class的区别是什么 struct、class做类默认是public公开成员的而class是默认private成员的。 struct、class对于继承来说struct默认继承是公开继承方式而class默认继承是私有继承方式。
5.父类和子类对象赋值转换
CPP中支持把子类对象赋值给父类对象有个专属的名词叫做切片或切割 很新奇吧为啥其这么个名字呢
class Father
{
private:int f_a;
protected:int f_b;
public:int f_c;
};class Son : public Father
{
private:int s_a 1;
protected:int s_b 2;
public:int s_c 3;
};void test2()
{Son s;Father f s;//代码为 0。
}在上图种父类有name、sex、age三个成员变量子类呢比父类多个_no的变量 你想要把一个子类对象强行放到一个父类类型里面那是不是_no变量会被扔掉所以十分切合我们所说的这种意思CPP就形象的称此为“切片”/“切割”啦。
实际上除了子类对象可以赋给父类对象之外自然也支持把子类指针给到父类指针把子类引用给到父类引用啦(请参见下图)。
void test2()
{Son s;Father f s;//代码为 0。Father* pf s;//代码为 0。Father qf s;//代码为 0。
}除此之外我还需要介绍子类给父类对象的时候是没有中间变量产生的。 我们都知道隐式类型转换、强制类型转换都会在赋值中间产生一个临时对象而子类和福哦类的复制转换是没有临时对象产生的。 这是为什么呢编译器做了特殊处理。其中的道理我也不太懂暂且留到以后有机会再说吧哈哈。
之后我还要去强调另外一点父类对象不能给到子类类型变量哈。
6.继承种的作用域
两个类构成继承那么对于作用域而言两者也是相互独立的。 子类和父类中有同名成员不会报错此时会构成 隐藏 。 需要主要的是成员函数的隐藏构成条件是函数名一致即可不需要参数进行比较两个不同类中的函数不会构成重载哈只有在同一个作用域的函数才会有重载这一说我们刚开始就说了两个类有着相互独立的作用域。 我个人建议大家在继承体系定义的时候尽量不要定义重名的成员因为容易进坑。
class Father2
{
public:int a 1;void func(){cout father endl;}
};class Son2 : public Father2
{
public:int a 2;void func(){cout son endl;}
};void test3()
{Son2 son2;cout son2.a endl;//访问的是son中的变量son2.func();//访问的是son中的函数cout son2.Father2::a endl;//访问的是father中的变量son2.Father2::func();//访问的是father中的函数
}7.子类的默认成员函数
对于子类的默认成员函数认识比较复杂首先需要对子类的默认成员函数有三个方面进行认识一整个父类 子类中的内置类型 子类中的自定义类型
7.1构成函数
子类构造的逻辑 如果你不写子类的构造函数那么编译器帮你自动生成一个默认构造函数这个默认构造函数会忽略子类中的内置类型会去自动调用子类中的自定义类型会去自动调用父类的默认构造函数如果此时父类没有默认构造函数就会报错哈
class Fa
{
public:int _fa;
};
class So: public Fa
{
public:int _so;
};void test4()
{So s;//在父类和子类都不写构造的情况下子类会生成默认构造//子类默认构造里会去调用父类的默认构造
}class Fa
{
public:int _fa;Fa(int f, char c)//此时Fa没有默认构造函数{}
};
class So : public Fa
{
public:int _so;};void test4()
{So s;//So::So(void)”: 由于 基类“Fa”不具备相应的 默认构造函数 或重载解决不明确因此已隐式删除函数//此时So不写默认构造编译器会自动生成子类默认构造函数并去调用父类的默认构造函数、//但是父类没有默认构造因而报错
}什么是默认构造函数 全缺省的编译器默认生成的你显示写的无参的构造函数我们都叫做默认构造函数。
如果你显示写了子类的构造函数并且都正常去对子类中的内置类型做了处理也调用了子类中自定义类型的构造函数指明调用了父类中的构造函数那么编译器就会按照你写的去走。 但是如果你显示写了子类的构造函数但是里面什么都没写那么编译器怎么做呢此时请注意编译器依然会对子类内置类型忽略对子类中的自定义类型去调用对应的构造函数仍然会调用父类的默认构造。为什么明明我什么都没有写啊因为编译器会自动走构造函数的初始化列表
class Fa
{
public:int _fa;//此时_fa存在默认构造函数
};
class So : public Fa
{
public:int _so;So():_so(1){}
};void test4()
{So s;//此时so写了子类构造函数会去调用父类默认构造函数。
}class Fa
{
public:int _fa;Fa(int c):_fa(1){//此时Fa没有默认构造函数}
};
class So : public Fa
{
public:int _so;So():Fa(1)//明确写要调用父类的非默认构造函数,_so(1){}
};void test4()
{So s;//So明确写了构造函数虽然父类中没有默认构造但是子类构造明确调用父类有参构造所以也可以正常运行
}7.2拷贝构造函数
拷贝构造的逻辑基本与构造函数是一样的依然编译器会自动给你生成一个。这里就不再多介绍了。
不过有一点我需要强调哈就是拷贝构造函数与构造函数是并列关系显示写有参构造不会影响编译器生成拷贝构造函数。但是我写一个拷贝构造函数编译器不再生成默认构造函数了哈。
这个地方比较奇怪这都怪CPP的老古董语法了~
class father
{
public:int _f 1;
};class son : public father
{
public:int _s 1;
};void test5()
{son s;son s2(s);//子类有默认拷贝构造父类也有所以这时候是没有问题的
}class father
{
public:int _f 1;father() default;//强制生成默认构造函数//拷贝构造father(father f):_f(f._f){}
};class son : public father
{
public:int _s 1;son() default;//强制生成默认构造函数son(son s):_s(s._s){cout father endl;}
};void test5()
{son s2;son s(s2);//father//子类拷贝构造即使不写调用父类拷贝构造也会去默认调用
}class son : public father
{
public:int _s 1;son() default;//强制生成默认构造函数son(son s):father(s)//明确写调用父类的拷贝构造注意这个地方会发生切片,_s(s._s){cout father endl;}
};void test5()
{son s2;son s(s2);//father//子类拷贝构造写调用父类拷贝构造那么也会去调用父类的拷贝构造函数
}7.3赋值运算符重载函数
这个跟上面的构造函数还是不太一样的需要着重说一下。
如果子类和父类的赋值运算符重载函数自己都不写编译器都会默认进行生成对于子类的内置类型直接浅拷贝(值拷贝)对于自定义类型那么就直接调用对应的拷贝构造函数同样对于父类的赋值也自然会去调用。
如果子类中明确写了赋值但是子类赋值没有写要访问父类赋值此时并不会去调用父类赋值。为什么跟前面两个拷贝构造、构造不一样呢因为前两个构造都要走初始化列表但是赋值函数没有初始化列表这一说。
class F1
{
public:int _f;F1(){_f 2;cout F1() endl;}F1 operator(const F1 f){if (this ! f)//排除自己给自己赋值的情况{cout F1 operator(const F1 f) endl;_f f._f;}return *this;}
};class S1 : public F1
{
public:int _s;S1(){_s 1;cout S1() endl;}S1 operator(const S1 s){if (s ! this){//不写不去默认调用父类的赋值函数。cout S1 operator(const S1 s) endl;_s s._s;}return *this;}
};void test6()
{S1 s1;//F1() S1()S1 s2;//F1() S1()s2 s1;//S1 operator(const S1 s)
}要显示写调用的话怎么写前面要加类名限定符。不写的后果就是死递归然后程序挂掉。
class F1
{
public:int _f;F1(){_f 2;cout F1() endl;}F1 operator(const F1 f){if (this ! f)//排除自己给自己赋值的情况{cout F1 operator(const F1 f) endl;_f f._f;}return *this;}
};class S1 : public F1
{
public:int _s;S1(){_s 1;cout S1() endl;}S1 operator(const S1 s){if (s ! this){F1::operator(s);//这个地方前面一定要写F1不然就是死递归cout S1 operator(const S1 s) endl;_s s._s;}return *this;}
};void test6()
{S1 s1;//F1() S1()S1 s2;//F1() S1()s2 s1;//F1 operator(const F1 f) S1 operator(const S1 s)
}7.4析构函数
子类的析构函数调用结束后会自动调用父类的析构函数。-原因在于要保证先析构子类后析构父类因为子类是可以访问父类的如果先析构父类那么再访问父类的成员会出现意想不到的结果。
class Fa
{
public:int _fa;~Fa(){cout ~Fa() endl;}};
class So : public Fa
{
public:int _so;~So(){cout ~So() endl;}
};void test7()
{So s;//~So()//~Fa()
}
子类和父类的析构函数在子类函数中也会发生隐藏/重定义写的时候也要前面加上类名-这是因为后面多态的缘故编译器对析构底层做了特殊处理使得子类和父类的析构函数产生了隐藏/重定义。
class Fa
{
public:int _fa;~Fa(){cout ~Fa() endl;}};
class So : public Fa
{
public:int _so;~So(){Fa::~Fa();//这个地方前面也得指明类域cout ~So() endl;}
};void test7()
{So s;//~Fa()//~So()//~Fa()
}8.继承与友元函数
把继承这个新语法加入与友元函数又有什么火花呢
继承对于友元函数是没什么关系哈我们如果把友元函数比作是朋友那么继承就类似于父子之间的关系你父亲的朋友跟你没啥关系你的朋友也跟你父亲没啥关系。
class Student;//类的声明
class Person
{
public:friend void Display(const Person p, const Student s);protected:string _name 1; // 姓名
};class Student : public Person
{
protected:string _s 2;
};void Display(const Person p, const Student s)
{cout p._name endl;//友元函数仅可以访问父类的东西//cout s._stuNum endl;//报错
}void test81()
{Person p;Student s;Display(p, s);
}不过需要注意的是因为你继承了你父亲的一些成员所以友元函数是可以访问你继承了你父亲这一部分的成员的因为这些成员是属于你的(加入说函数在该类友元的话)。
class Student;//类的声明
class Person
{
public:
protected:string _name 1; // 姓名
};class Student : public Person
{friend void Display(const Person p, const Student s);protected:string _s 2;
};void Display(const Person p, const Student s)
{cout s._name endl; //友元函数可以访问子类继承父类的东西cout s._s endl; //友元函数仅可以访问子类的东西//cout p._name endl;//此时去访问父类的东西会报错
}void test81()
{Person p;Student s;Display(p, s);
}9.继承与静态成员
对于一般的变量父类对象有一份继承他的子类对象也有一份(前提是父类变量不是私有的哈)。 对于静态变量比较特殊CPP规定只有一份既属于父类也属于子类。请注意整个父类无论有多少对象都只有一个static变量
基类定义了static静态成员则整个继承体系里面只有一个这样的成员。无论派生出多少个子类都只有一个static成员实例 。
应用统计下生成了多少个父类子类对象
class Person
{
public:Person() { _count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};
int Person::_count 0;
class Student : public Person
{
protected:int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;cout 人数 : Person::_count endl;Student::_count 0;cout 人数 : Person::_count endl;
}10.菱形继承
10.1单继承与多继承
在介绍什么是菱形继承之前我先来说一下什么是单继承与多继承的概念。 继承按照可以继承父类的数量可以分为单继承和多继承。 单继承 多继承 前面讲的都是单继承CPP中也有多继承机制在多继承机制下CPP为多种场景提供了更好的支持但是多继承中的菱形继承存在一定的小问题
10.2多继承中的菱形继承问题及虚拟继承 在上面菱形继承中我们发现同一份变量会继承两份。这样会造成数据冗余和二义性问题。
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 _majorCourse; // 主修课程
};
void test10()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;//a._name peter;// 需要显示指定访问哪个父类的成员可以解决二义性问题但是数据冗余问题无法解决a.Student::_name xxx;a.Teacher::_name yyy;
}虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系在Student和Teacher的继承Person时使用虚拟继承即可解决问题。需要注意的是虚拟继承不要在其他地方去使用。
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 _majorCourse; // 主修课程
};
void test10()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name peter;// 需要显示指定访问哪个父类的成员可以解决二义性问题但是数据冗余问题无法解决a.Student::_name xxx;a.Teacher::_name yyy;cout a._name endl;//yyycout a.Student::_name endl;//yyycout a.Teacher::_name endl;//yyycout a._name endl;//000000860A0FFAA8cout a.Student::_name endl;//000000860A0FFAA8cout a.Teacher::_name endl;//000000860A0FFAA8
}10.3菱形继承中虚拟继承的原理
虚拟继承是如何解决菱形继承二义性、数据冗余的问题的呢 为了研究虚拟继承原理我们给出了一个简化的菱形继承继承体系再借助内存窗口观察对象成员的模型。
//模型代码
class A
{
public:int _a;
};class B : public A
//class B : virtual public A
{
public:int _b;
};class C : public A
//class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
void test11()
{//这是在没有使用虚拟继承情况下的菱形继承看d的内存空间D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;
}class A
{
public:int _a;
};//class B : public A
class B : virtual public A
{
public:int _b;
};//class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
void test11()
{//这是在使用虚拟继承情况下的菱形继承看d的内存空间D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;
}我们发现在d的存储中的确是只有一个_a了然后多了两个指针一个是黄色曲线的一个是蓝色曲线的。 黄色曲线的指针指向了一个00后面紧跟着一个数字20这个20代表在d内存中_b到_a之间的偏移量蓝色同理代表的是在d内存中_c到_a之间的偏移量。 B区域的开始0x63C0x140x650C区域的开始0x6440x0C0x650 其中_a我们称之为虚基类一般放在最下面用偏移量进行访问主要用于切片时候。
菱形继承中的指针
void test11()
{//这是在使用虚拟继承情况下的菱形继承看d的内存空间D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;B* pb d;C* pc d;cout pb endl;//00AFF658cout pc endl;//00AFF660
}需要虚继承的话那在什么地方加上关键字virtual呢
10.4在继承公共基类的时候使用虚继承 在这种情况下也属于菱形继承在使用虚拟继承的时候应该把关键字virtual加到Student、Teacher类上。因为他俩有公共的基类。
10.5腰部父类也使用虚继承模型
之所以这样是因为方便指针进行访问。使用了菱形虚拟继承之后定义一个中间父类的指针我们发现既可以是子类做切片进行访问又可以是访问它本身为了统一处理CPP干脆把中间的父类模型结构也换成了与子类大体一致的模型。
10.6菱形继承的实例
菱形继承在库中有一个案例就是iostream这个地方用到的就是菱形虚拟继承的方式进行处理的。
11.组合与继承
与继承相似的一种代码复用方式叫做组合。 组合的概念一个类把另一个类作为他的成员变量。类似于一种包含关系。
class A
{
public:int _a;
};class B
{
public:A _aa;//组合int _b;
};void test13()
{B b;
}好的如果本篇文章对你有帮助不妨点个赞~谢谢。 EOF