快刷网站,visual studio网站开发教程,叮当设计app官方下载,wordpress获取自定义文章类型分类前言#xff1a;
本篇我们将开始讲解C的继承#xff0c;我想要说的是#xff0c;C的主体基本就是围绕类和对象展开的#xff0c;继承也是以类和对象为主体#xff0c;可以说#xff0c;C相较于C优化的地方就在于它对于结构体的使用方法的高度扩展和适用于更多实际的场景…前言
本篇我们将开始讲解C的继承我想要说的是C的主体基本就是围绕类和对象展开的继承也是以类和对象为主体可以说C相较于C优化的地方就在于它对于结构体的使用方法的高度扩展和适用于更多实际的场景这里的继承便是一个很好的体现。
1.继承的概念
何为继承 这个词常见于子一代接过来上一代的资产和社会地位比如中国古代的君位传给嫡长子或者欧洲的爵位可以世代去传承而由前言我们又得到继承用于类和类之间因此我这样推测继承一定是某一个类的一些成员传给另一个和它有继承关系的类。 由此我们引入继承的概念 **继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展增加功能这样产生新的类称派生类继承后父类的成员成员函数成员变量都会变成子类的一部分。 继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用继承是类设计层次的复用继承后父类的成员成员函数成员变量都会变成子类的一部分。 如下的例子
class person
{
public:void print(){couthello world!endl;}int _a;
};
class student :public person
{public:int _s;
};此时我们就说student继承了person.
2.继承的使用规定
1.继承的成员构成
依旧是以上面的例子为例 我们将诸如person这种类称为父类或者也叫基类。 将诸如student这种类称为子类也叫派生类。
2.格式
我们的继承格式是在派生类即子类后面加上我们的继承方式和要继承的基类即可构成继承。 和我们的类内部的三种访问限定符一样继承方式的访问限定符也是private,protected,public三种。如下 由排列组合可知他们之间一共可以构成9种情况如下 我们之前说过在类和对象中,private和protected是没有去别的当时一定有人疑惑那分开这两个的目的是什么而它的目的就在这里根据上面的表格我们可以这样大致总结一下 1.对于基类的private成员无论继承关系是什么在派生类中都是不可见的 2.对于基类不是private成员的取继承关系和访问限定符中权限最小的为这个成员在派生类中的最终的权限属性比如基类的一个成员是public成员然后被以private的方式继承则这个成员最后就是派生类中的private成员 注意 1.这里要区分一下不可见和私有的区别 首先一定要注意只要是继承派生类就会继承全部的基类成员只是权限导致访问不了而不是没有被继承下来。 其次不可见是不论在派生类内还是类外都不能访问和使用而私有是在类外不能访问和使用但是在类内是可以的
2.省略继承方式(不建议不写有时候会出现错误还是写上为好 不写继承方式直接跟父类名称也是可以的不过这样就不能随意调控继承方式了而是遵从默认的继承方式 一般struct的父类的继承方式为public,而class的父类的继承方式为private
3.派生类和基类对象的赋值转换
在进行派生类和基类的赋值转换之前我们首先先复习一下我们之前的赋值转换的一些规则
double a3.3;
const int m3;
const int sa;我们以引用为例子这里加上const的原因就在于浮点型转换为整型的时候会涉及到赋值转换即会有一个临时变量将a转换成整型的结果存储起来这个临时变量具有常性就像我们之间引用常数一样因此我们需要加上const.包括我们的常规的类的赋值转换也是如此比如
string strXXXX;我们这里采用了匿名构造PPP匿名构造就是类的临时对象报错结果如下 没错匿名构造的临时对象也具有常性也需要我们加上const才能编译通过。 那么对于具有继承关系的子类和派生类来说呢 我们先尝试一下
class person
{
public:void print(){couthello world!endl;}int _a;
};
class student :public person
{public:int _s;
};
int main()
{student q;person sq;person smq;student msq;return 0;
}报错的条件如下 我们发现可以直接将派生类赋值给基类但是把基类赋值给派生类就会报错。 派生类和基类赋值转换的原理如下 1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。 2.基类对象不能赋值给派生类对象倘若非要赋值就只能通过强转成派生类类型后赋值但是那样会存在越界的问题 图解如下 3.派生类赋值给基类对象的方式有三种指针引用和赋值三种他们对应的含义在上方图解 1.如果是赋值就是把子类中的部分拷贝一份给这个基类对象 2.如果是指针就是直接指向子类中属于基类这部分的地址 3.如果是引用就是直接是派生类中属于基类这部分的别名
4.继承中的作用域
继承中一般的规则如下 1. 在继承体系中基类和派生类都有独立的作用域。 2. 子类和父类中有同名成员(包括成员函数)子类成员将屏蔽父类对同名成员的直接访问这种情况叫隐藏也叫重定义。且是子类对父类的同名成员隐藏故我们直接访问的时候默认访问的是子类的成员若想访问父类的成员需要我们加上父类的访问限定符才能访问。 3. 需要注意的是如果是成员函数的隐藏只需要函数名相同就构成隐藏。 4. 注意在实际中在继承体系里面最好不要定义同名的成员。 如下
class Person
{
protected :string _name MMM; int _num 111;
};
class Student : public Person
{
public:void Print(){cout name:_name endl;cout number:Person::_num endl;cout num:_numendl;}
protected:int _num 999;
};
void Test()
{Student s1;s1.Print();
};
// B中的fun和A中的fun不是构成重载因为不是在同一作用域
// B中的fun和A中的fun构成隐藏成员函数满足函数名相同就构成隐藏。
class A {
public:void fun(){cout func() endl;}
};
class B : public A {
public:void fun(int i){A::fun();cout func(int i)- iendl;}
};
void Test()
{B b;b.fun(10);
};在这里我想强调一下函数重载和函数隐藏的区别 函数重载的前提是在同一个作用域下两个函数的函数名相同但参数返回值等不同而函数隐藏是在不同的作用域下是否在同一个作用域下就是函数重载和函数隐藏的本质区别
5.派生类的默认成员函数
在类的对象中我们详细的对派生类的默认成员函数进行了讲解那么在继承这里还有哪些新的概念呢 首先我想问的一个问题是我们如何看待派生类里的基类是否把它当成一个派生类的成员来看呢 我认为应该将其当成一个派生类成员来看而C的处理方式也是类似的它的大体规则如下 1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用。 2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。 3. 派生类的operator必须要调用基类的operator完成基类的复制。 4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。 5. 派生类对象初始化先调用基类构造再调派生类构造。 6. 派生类对象析构清理先调用派生类析构再调基类的析构。因此就不用我们单独再去写一次基类的析构了会出现多次释放的问题 7. 因为后续一些场景析构函数需要构成重写重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理处理成destrutor()所以父类析构函数不加virtual(即虚函数多态)的情况下子类析构函数和父类析构函数构成隐藏关系第7点很关键我们要记得析构函数的使用被统一处理成了destrutor()所以后面的多态才能使用析构。 我用我写的一个例子来证明上面的语法规定
#includeiostream
#includeassert.h
using namespace std;
class person //final
{
public:person(int age20 , int score30):_age(age), _score(score){cout person() endl;my_count;}int count(){return my_count;}/*person(const person p)//拷贝构造古典写法:_age(p._age), _score(p._score){cout person(const person p) endl;}*/void swap( person s)//交换数据{std::swap(_age, s._age);std::swap(_score,s._score);}person( const person s)//拷贝构造现代写法{cout person (const person s) endl;person p(s._age,s._score);swap(p);}/*person operator( person p)//赋值运算符重载古典写法{cout person operator(const person p) endl;if (p ! this){_age p._age;_score p._score;_name p._name;}return *this;}*/person operator( person p)//赋值运算符重载现代写法{cout person operator(person p) endl;swap(p);return *this;}~person(){cout ~person() endl;}void Print1(){cout _name endl;cout _age endl;}string _name peter;int _age;int _score;
private:static int my_count;
};
int person::my_count 0;class student :public person
{
public:student(int a132,int a233,int stuid100):_stuid(stuid),person(a1,a2){cout student() endl;}student(const student p):person(p)//由于我们传的是p的引用在person的部分因此它代表的正是p的父类成员部分的别名直接拷贝即可,_stuid(p._stuid){cout student(const student p) endl;}student operator(const student p){cout student operator(const student p) endl;if (p ! this){person::operator(p);//赋值也是父类调用父类的赋值子类单独调用子类的_stuid p._stuid;}return *this;}void Print1(){person::Print1();}~student(){cout ~student() endl;}int _stuid;int _major;
};
class teacher :public person
{
public:int _jobid;
};
int main()
{student s;teacher t;s.Print1();t.Print1();person s1 s;person* s3 s;s1._age;s1._name hbw;s3-_age 100;s3-_name LCNB;s.Print1();person a4(12, 33);person MM(a4);person ss(100,200);person s10;s10 ss;student zcx(100, 200, 300);student jbl(1000, 2000, 3000);jbl zcx;student HBW(300000, 20000, 1);student LC(HBW);person CXZ;cout CXZ.count() endl;return 0;
}通过我的例子你可以发现我几乎都使用了分别处理基类和派生类成员的方式就像我说的我把基类单独看成一个派生类的成员去看每次都是分开去处理基类和派生类。写派生类的各种默认函数时就在其内部单独处理基类的对应的默认函数。 用图解表示就是这样。
6.友元继承问题
友元关系不能继承也就是说基类友元不能访问子类私有和保护成员如果想要派生类也具有和父类一样的友元关系必须在派生类里也加上友元声明才可以。 例如
class Person
{
public:friend void Display(const Person p, const Student s);
protected:string _name;
};
class Student : public Person
{
protected:int _stuNum;
};
void Display(const Person p, const Student s) {cout p._name endl;cout s._stuNum endl; }
void main()
{Person p;Student s;Display(p, s);
}在这里这样写会报错原因正是友元的问题
7.静态成员的继承问题
静态成员的本质依旧是不变的不管有多少个类存在继承依旧只存在一个这样的静态成员都只有一个static成员实例因此对于派生类来说说它继承了这个静态成员变量也好说没有继承也可以而由于派生类又会调用基类的默认成员函数所以派生类有时会自动去调用静态成员变量。 不过静态成员变量依旧严格遵守继承关系和访问限定符的修饰的权限设置虽然可以继承但是访问权限的限制导致不一定可以访问到。
8.继承的类型 菱形继承以及虚拟继承virtual:
继承主要的类型分为两种单继承和多继承 单继承 如图所示就是一种子类去继承一个父类然后依次向下 多继承 如图所示就是一个子类同时继承了多种父类如图 多继承是一个泛用于对一个对象进行各种精细功能和建模的构建形成的当然这只是我的一种解法比如很多以时装和外观修饰饰品的游戏来说由于它的目的是针对各种不同的部位作为卖点吸引玩家因此它的实现过程可能就是把每个部位作为一个父类来实现然后由一个子类作为整体去继承这些代表部位的父类。
难点菱形继承问题
菱形继承是多继承的一种特殊情况 如上图所示这种就属于是菱形继承但菱形继承不仅仅是这种仅限于4个类构成菱形结构的继承方式我认为叫它菱形继承不够严谨比如下面的情况 这种也属于一种菱形继承因此**我更喜欢叫它闭环继承即继承关系之间形成了一个环状结构的封闭圆环。** 那么这种结构的问题出现在哪里呢 我们还是以第一个标准菱形继承为例 菱形继承的问题从上面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。 这是由于我们的最底层的派生类同时继承了两个基类但这两个基类又同时继承了同一个基类因此导致我们的最底层的派生类有两份名字和类型都相同的成员因此在我们访问的时候不知道应该访问哪个就会出现二义性同时数据也存在冗余的问题比如电话号码只需要一份就可以但是同时存储了两份不同的电话号码还都是打给同一个人的数据就会冗余尤其是在切片时引用或者指针不知道怎样指向公共数据的位置。 如何解决二义性 二义性是一个问题但同时也不一定是一个问题因为比如一个人在社会中有时确实有不同的称呼因此想要访问不同的名字只需要加上对应基类的访问限定符显式访问即可如下
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 Test ()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a ;
a._name peter;
// 需要显示指定访问哪个父类的成员可以解决二义性问题但是数据冗余问题无法解决a.Student::_name xxx;a.Teacher::_name yyy; }但是数据冗余的问题依旧无法解决依旧是有两份重复的数据被存储了起来。 如何解决数据冗余的问题 在C中了一种方式可以让我们解决这个问题虚拟继承virtual 即是在继承方式的前面加上vitual表示虚拟继承即可这样数据冗余就被解决了并且变成了修改一个公有的基类的成员则所有的拥有这个成员的类都会对应做出相应的改变统一去进行改变。并且此时的共有的父类的成员只被存储了一份而不是被重复存储多份了 virtual的添加位置也有讲究要在第一批继承了重复数据的子类上加比如在这里就是Student和teacher加而Assistant是不需要加的。 注意一定是第一批继承了基类的重复数据的派生类上加
虚拟继承解决数据冗余和二义性的原理
为了查看原理我们在这里用一个实验性质的程序进行查看
class A
{public:int _a;
};
class B : public A//普通继承
class B : virtual public A //vitual继承
{
public:int _b;
};
class C : public A//普通继承
class C : virtual public A //vitual继承
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0; }当我们调试这个代码时查看内存情况如下 首先是不加virtual的普通继承时内存的存储情况你可以发现它是在两个类里重复存储了两份相同的a从而实现了效果 然后是加上vitual的虚拟继承时内存的情况 这里可以分析出D对象中将A放到的了对象组成的最下面(有的时候,根据编译器的不同也可能在最上面)这个A同时属于B和C那么B和C如何去找到公共的A呢这里是通过了B和C的两个指针他们分别指向一个存储着一个整型数据的地址而这个整型数据就是在最底层派生类在内存中的当前位置距离成员a的偏移量B C两个指针对应的地址存储的数据不同正好对应着他们由于存储的位置不同同a的偏移量也不同。如下 由此我们可以这样总结其原理 虚继承的原理其实有点类似静态成员变量/成员函数的处理方式对于这种冗余的数据在存储时直接将其存在派生类成员内存的最上面(最下面)并作为一个公共的数据存储在内存中的特定位置而对应包括继承了这个类的成员来说想访问公共成员会利用他们在内存中存下的一个地址这个地址所存储的数据时这个类的当前位置距离这个公共数据位置的偏移量这样不管是切片还是访问都只要通过这个地址偏移量即可找到对应的公共数据的地址进行访问
9.继承的总结和反思以及一些细节的探究
1.在继承中谁先被继承谁就先被声明先去调用相关的函数即继承的顺序是按照从左到右的顺序进行
例子如下
class A;
class B;
class C;
class D:public C,public B,public A在这里就是先去拷贝构造C然后是B然后是A根据继承的先后顺序去调用
2.一般可以去使用多继承但不建议使用菱形继承问题很大
3.多继承由于菱形继承的问题有很大的缺陷因此后续的语言都没有多继承如JAVA
4.继承和组合的优缺点
1.继承是is-a型也就是说只要是继承则派生类里必定有一个基类的对象存在 而组合是has-a型也就是说在一个类里显式写了另一个类则这个类才会包含在当前的类中否则这就是两个独立的类没有关系 2.在两者都通用的前提下优先可以考虑组合其次是继承因为组合对权限有更多的限制或许你会说继承的private基类权限限制更高但是那样的基类即使继承了也没有什么意义大多实际使用的还是public和protected因此此时的组合对类的权限限制更加严谨类和类之间的修改导致的影响更小符合高内聚低耦合的设计思想 3.继承是一种白箱复用权限更大能看清楚底层但随之导致耦合度高而组合是一种黑箱复用权限更小看不清底层只有上层提供的缺口但耦合度更低。** 虽然如此但是我们已经要根据实际情况去考虑根据设计的逻辑和设计的侧重点去考虑使用哪种不要公式化套用。先理清当前的场景是组合好还是继承好
5.如何构建一个不能被继承的类
法一构造函数私有化 即在访问限定符private的作用域内部写构造函数这样派生类没法调用基类的构造函数也就没法继承 法二 在C11中提供了一个关键字final,最终类修饰基类从而直接让其不能被继承**
总结
以上便是继承的全部内容了可以说继承的出现让我们对类和对象的使用又有了更多的花样同时也进一步确定了类和对象在C的地位之高基本上几乎所有的语法都是在为类和对象的使用进行扩展和补充他们在实际的工程中十分常用现在你可以用自己的想象力为自己构建一个游戏人物的对象模型让我们进一步熟悉继承和类和对象这方面的知识点。