资讯门户网站怎么做,可以查企业信息的软件,我想克隆个网站 怎么做,网站建设 甘肃本文已收录至《C语言和高级数据结构》专栏#xff01; 作者#xff1a;ARMCSKGT 继承 前言正文继承的概念及定义继承的概念继承的定义重定义 基类和派生类对象赋值转换派生类中的默认成员函数隐式调用显示调用 继承中的友元与静态成员友元静态成员 菱形继承概念 虚继承原理继… 本文已收录至《C语言和高级数据结构》专栏 作者ARMCSKGT 继承 前言正文继承的概念及定义继承的概念继承的定义重定义 基类和派生类对象赋值转换派生类中的默认成员函数隐式调用显示调用 继承中的友元与静态成员友元静态成员 菱形继承概念 虚继承原理继承和组合 最后 前言
面向对象的三大特性封装,继承和多态前面我们介绍了类和对象如何对数据和数据操作方法进行封装本章将为大家介绍另一大特性-继承继承可以增强代码的复用性和增强功能的可扩展性我们将学习如何通过父类衍生出更多特性的子类 这里水果类作为父类衍射出三种水果苹果,西瓜和荔枝这三种具体的水果都可以统称为水果但是各自又有着自己的特性例如苹果外皮是红色的西瓜外皮是绿色的等等这就是继承继承将一个抽象的父类更加具体的实例表达 正文 继承的概念及定义 继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展增加功能这样产生新的类称派生类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用继承是类设计层次的复用。 其中 被继承的类是父类/基类 (base)继承的类称为子类/派生类 (derived) 继承的本质就是复用代码我们通过一个校园信息管理类代码来展示和说明 #include iostream
#include string
using namespace std;//基本人员信息类
class Person
{
public:string Name; //姓名int Car_ID; //身份证号string Date; //生日long long telephone; //电话号码
};//教师类
class Teacher : public Person
{
public:int Employee_ID; //教师员工号string Subjects; //教师所教科目double Wages; //工资//...
};//学生类
class Student : public Person
{
public:int Student_ID; //学生学号string Faculties; //学生所属院系string Specialized; //学生专业//...
};我们在学校最常见的两个身份就是老师和学生老师和学生都有自己的姓名和身份证号等但是老师有自己的员工号和任职科目学生有自己的学号和专业信息通过继承父类Person可以让老师和学生都拥有人的基本信息然后在老师和学生类中增加不同身份所独有的特性这样就不需要每个类都写上人员基本信息的变量条目了实现了复用当然函数后亦是如此 继承的作用 子类在继承父类后会继承父类 公开(public)和保护(protected) 的成员除了父类私有成员外子类可以继承其余的所有成员 结合访问限定符可以让子类合理访问父类成员互不干扰 继承的定义 介绍完继承的定义相关知识我们开始使用继承 继承的格式如下 class 子类 : 继承方式 父类 { /*子类成员*/ };继承方式与权限 关于访问限定符有三种 公有(public)该成员可以被任意访问保护(protected)该成员只能被本类和子类中访问私有(private)该成员只能在本类中被访问 其中保护在没有继承的情况下和private一样外界无法访问在有父子类的情况下才能有区别 权限范围从大到小依次为 public protected private 访问限定符有三种继承的方式则也有三种继承方式与成员的访问限定符组合可以有多种情况 访问/继承 权限公有public保护protected私有private父类public成员仍然为public变为protected变为private父类protected成员仍然为protected仍然为protected权限为private父类private成员不可见不可见不可见 补充 父类(基类)的私有成员在子类中始终不可见但并不代表不在子类中其在子类中仍然被继承但是不可见父类(基类)的成员如果不想在类外被访问但是可以被子类访问则设置为protectedprotected是专门为该场景设计的在成员的继承上权限只能被缩小不能被放大即成员最终的权限是在成员本身的访问权限和继承权限中取最小权限即可在class继承时如果我们不显示指定继承方式则默认为private在struct继承时如果不显示指定继承方式则默认为public平时不建议省略继承方式在实际运用中一般使用都是public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强 重定义 重定义也叫隐藏(覆盖)是指子类继承父类后子类重新声明的成员名字与父类相同此时访问子类成员时默认访问的是子类的同名成员而隐藏父类成员(局部优先原则)同时也证明了父子类中的作用域是独立存在的 class Base
{
public:void func() { cout Base num: num endl; } //同名方法void Bfunc() { cout Bfunc endl; } //非同名方法int num 1; //同名变量char b b; //非同名变量
};class Derived : public Base
{
public:void func() { cout Derived num: num endl; }void Dfunc() { cout Dfunc endl; }int num 2;char d d;
};int main()
{Derived der; //声明子类对象return 0;
} 执行同名函数func 此时我们发现对于同名函数和同名变量执行时访问子类的同名函数和变量 如果我们删除子类的同名变量再执行 此时执行的是子类函数访问的是父类的num变量 访问各自的非同名成员 对于非同名成员访问是正常的 如果我们想要访问父类的隐藏成员可以使用 作用域限定符 :: 指定父类成员进行显示访问 int main()
{Derived der;der.Base::func();cout der.Base::b endl;return 0;
}总结 在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问这种情况叫隐藏 也叫重定义(在子类成员函数中可以使用 基类::基类成员 显示访问)。如果是成员函数的隐藏只需要函数名相同就构成隐藏。注意在实际中在继承体系里面最好不要定义同名的成员。 基类和派生类对象赋值转换 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。 基类对象不能赋值给派生类对象子类是在基类的基础上进行拓展将基类赋值给子类时需要编译器去推敲父类对象中需要增加什么编译器不敢随便做这种事情(毕竟儿子不可能道反天罡)。 当基类的指针(引用)指向子类对象时可以通过强制类型转换赋值给子类的指针(引用)。注意这里必须是基类指针(引用)指向子类对象才是安全的如果基类如果是多态类型可以使用C11中的安全的类型转换 dynamic_cast 进行赋值 总结: 对于父类和子类之间的对象赋值会涉及类型转换类似于double转int等。子类可以赋给父类对象其中会产生类型转换所以会有临时变量。但是父类不能赋值给子类(可以理解为子类的成员比父类多父类无法判断)但是父类的指针可以指向子类。子类赋值给父类需要公开继承是天然支持的不存在类型转换(子类对象是一个特殊的父类对象)。 class Person
{
protected: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;//2.基类对象不能赋值给派生类对象 例如 sobj pobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp sobj;Student * ps1 (Student*)pp; // 这种情况转换时可以的。ps1-_No 10;pp pobj;Student* ps2 (Student*)pp; // 这种情况转换时虽然可以但是会存在越界访问的问题ps2-_No 10;return 0;
}派生类中的默认成员函数 前面我们介绍过类的六大默认成员函数 同样的子类在继承父类后也会有这六个默认成员函数但是子类不写类的六大默认函数则调用时使用父类的默认函数如果实现了这六个默认成员函数则按照隐藏的规则在调用时默认调用子类的这六个默认成员函数对于父类的则需要显示调用这里我们对这种情况进行分析 隐式调用 子类在实例化调用构造函数时会先调用父类的构造函数先初始化父类再初始化子类本身同样的子类在析构时也会先析构自己再调用父类的析构函数析构父类空间 class Base
{
public:Base() { cout Base() endl; }~Base() { cout ~Base() endl; }
};class Derived : public Base
{
public:Derived() { cout Derived() endl; }~Derived() { cout ~Derived() endl; }
};int main()
{Derived der;return 0;
}注意 自动调用是由编译器完成的如果不存在默认构造函数那么需要我们手动在子类的构造函数中初始化父类对象如果不处理则会报错 显示调用 对于一些类我们不妨会使用 赋值重载 和 拷贝构造 这里如果我们不对相关函数进行特殊处理在发生拷贝时子类无法调用父类的赋值重载和拷贝构造函数父类部分极易容易发生浅拷贝 class Base
{
public:Base() {}~Base() {}Base(const Base b) { cout Base(const Base b) endl; }Base operator(const Base b){ cout operator(const Base b) endl; return *this;}
};//子类中不对父类对象做特殊除了
class Derived : public Base
{
public:Derived() {}~Derived() {}Derived(const Derived d) { cout Derived(const Derived d) endl; }Derived operator(const Derived d) { cout operator(const Derived d) endl; return *this; }
};//子类中对父类进行特殊处理
//class Derived : public Base
//{
//public:
// Derived() {}
// ~Derived() {}
// Derived(const Derived d):Base(d) //切片 构造父类对象 初始化父类
// {
// cout Derived(const Derived d) endl;
// }
// Derived operator(const Derived d)
// {
// Base::operator(d); //切片 构造父类对象 赋值给子类的父类部分
// cout operator(const Derived d) endl;
// return *this;
// }
//};int main()
{Derived der1;Derived der2(der1);cout ----------------------------- endl;der1 der2;return 0;
}我们分别对特殊处理和不特殊处理的Derived类在同一段main函数代码下测试测试结果 我们可以发现在子类的成员函数中对父类隐藏的成员函数显示调用进行特殊处理可以避免浅拷贝的问题而且我们可以在子类中 通过 初始化列表 和 :: 显示调用父类 构造函数 和 默认成员函数父类的拷贝问题进行处理 总结 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数则必须在派生类构造函数的初始化列表阶段显示调用。派生类的拷贝构造函数必须调用基类的拷贝构造在初始化列表完成基类的拷贝初始化。派生类的operator必须要调用基类的operator完成基类的复制。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。派生类对象初始化先调用基类构造再调派生类构造析构清理先调用派生类析构再调基类的析构(父类总是先构造最后析构)。对于析构函数编译器会统一处理成 destructor 函数对于父类的析构函数我们不能显示调用因为父类先构造根据栈区规则必须先析构子类此时因为类析构被统一处理为destructor所以父类析构函数被隐藏析构时只执行了子类析构造成父类内存空间不能被正常释放此时我们需要对父类的析构函数进行重写(将父类析构使用virtual修饰成为虚函数)以满足析构要求在多态中我们会重点介绍。 继承中的友元与静态成员 友元 友元关系不能继承也就是说基类友元不能访问子类私有和保护成员! 静态成员 基类定义了static静态成员则整个继承体系里面只有一个这样的成员。无论派生出多少个子类都只有一个static成员实例。 静态变量为于静态区不同于普通的堆栈区静态变量的声明周期很长通常是程序运行结束后才会被销毁因此 假设父类中存在一个静态变量那么子类在继承后可以共享此变量 class Base
{
public:void Bfunc() { cout Base a a endl; }static int a;
};class Derived : public Base
{
public:void Dfunc() { cout Derived a a endl; }
};int Base::a 0;
//int Derived::a 1; //继承中的静态成员只能被指定一次初始化int main()
{Base b;Derived d;b.Bfunc();Base::a 1;d.Dfunc();Derived::a 2;b.Bfunc();return 0;
}如预期所料无论我们是使用子类访问静态成员a还是父类访问静态成员a都访问的是同一个静态成员a 菱形继承 C中继承是可以单继承也可以多继承单继承就是一个子类只继承一个父类多继承就是一个子类继承多个父类 关于多继承即支持一个子类继承多个父类使其基础信息更为丰富但凡事都有双面性多继承 在带来巨大便捷性的同时也带来了个巨大的坑即菱形继承问题 因为多继承的弊端其他大部分面向对象的语言都禁止多继承 关于多继承只需要使用 , 将继承的多个父类连接起来即可 class 子类 : 继承方式 父类1, 继承方式 父类2, .... { /*子类成员*/}概念 菱形继承是多继承的一种特殊情况 菱形继承的问题从下面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题。 在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; // 主修课程
};int main()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name peter; //此语句会报错// 需要显示指定访问哪个父类的成员可以解决二义性问题但是数据冗余问题无法解决a.Student::_name xxx;a.Teacher::_name yyy;return 0;
}我们注释掉这句错误访问代码编译运行进行调试发现 a对象中出现了两个 _name 变量如果我们直接访问编译器会提示不明确因为编译器不知道我们要访问哪一个 _name 。 此时我们仍然要坚持访问 _name变量 只能通过 :: 指定访问域进行访问 这种方法解决了访问的问题但是数据冗余和二义性的问题仍然没有被解决 我们在实际使用中不需要两个 _name 我们想 _name 是唯一的此时我们就需要借助虚继承(与虚函数没有任何关系)来解决这个问题换句话说虚继承就是专门为解决菱形继承而产生的 虚继承方式 class 子类 : virtual 继承方式 父类 { /*子类成员*/ }这就是对父类的虚继承 需要注意的是虚拟继承不要在其他地方去使用。 我们只需要在多继承的腰部也就是父类在继承父父类时添加virtual即可 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; // 主修课程
};int main()
{Assistant a;a._name peter; //此时可以正常赋值return 0;
}此时我们可以发现我们可以通过a对象直接访问 _name 且修改 _name 后 所有的_name成员都发生了变化此时就解决了数据冗余和二义性的问题 此时内存中的成员分布为 为什么会是这样接下来我们探究一下虚继承的原理 虚继承原理 利用 虚基表 将冗余的数据存储起来此时冗余的数据合并为一份原来存储 冗余数据 的位置现在用来存储 虚基表指针 为了研究虚拟继承原理我们给出了一个简化的菱形继承继承体系再借助内存窗口观察对象成 员的模型。 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;
};int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0;
}下图是菱形继承的内存对象成员模型这里可以看到数据冗余 B和C类中都有各自的A类成员 下图是菱形虚拟继承的内存对象成员模型这里可以分析出D对象中将A放到的了对象组成的最下 面这个A同时属于B和C那么B和C如何去找到公共的A呢这里是通过了B和C的两个指针指 向的一张表。这两个指针叫虚基表指针这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。 此时无论这个 冗余 的数据存储在何处都能通过 基地址 偏移量 的方式进行访问 补充 当我们使用父类指针指向子类时会发生父子类赋值兼容问题也就是切片切出父类的那一部分从首地址开始但我们访问公共成员时仍然是通过 基地址偏移量 实现的 当然我们也可以实例化B对象让ptrb指向b对象 我们发现当不存在继承关系时只要我们对父类进行了虚继承其父类对象的存储发生就是在 原数据位存放虚表地址来获取偏移量再通过偏移量找到父类的成员 之所以这么做就是因为对象指针ptrb可能指向子类类型的对象也可能指向本类类型对象此时如果我们要访问父类成员 _a 一条语句 ptrb-_a 对于指向D对象和指向B对象的地址不同场景需要有不同的处理方式为了统一操作只要是虚继承其父类对象的都放在对象后面采用偏移量的方式去访问只要简化了处理过程 当编译器识别到需要通过 基地址偏移量 去访问时会在汇编指令中进行特殊处理而不是按照原方式直接去访问对象中的成员 如果虚继承的父类中其成员是另一个对象存在多个成员在访问时编译器先通过偏移量找到该成员对象的首地址然后通过成员对象的声明顺序去依次访问该场景例如 class test
{
public:int a;int b;int c;
};
//此时被虚继承的父类A中有一个对象t
class Base
{
public:test t;
};class Derived : virtual public Base {};
//此时Derived实例的对象首地址仍然是存放Base对象中t对象所在偏移量
//访问时通过int一个一个跳过即可(即使涉及内存对齐问题编译器也会根据规则做出调整)所以无论最终位置在何处最终汇编指令都一样得益于偏移量的设计模式 总结 虚继承底层是如何解决菱形继承问题的 对于冗余的数据位改存指针该指针指向虚表虚表中从首地址偏移4字节即为冗余的数据位偏移到数据地址的偏移量对于冗余的成员统一放置在后面通过首地址和虚表中偏移量进行访问为何在冗余处存指针 方便统一访问同一个变量解决二义性和数据冗余问题同时偏移量存放在虚表首地址偏移4字节处(一般为第二个条目)而首地址的四个字节是将来存放多态的虚表地址的虚基表指针 和 虚基表 是否会造成空间浪费 不会指针大小固定为 4/8 字节虚基表可以忽略不计所有对象共享 为了解决 菱形继承 问题想出了 虚继承 这种绝妙设计但在实际使用中要尽量避免出现 菱形继承 问题! 说明在多继承中关于多个父类谁先初始化与其声明顺序有关例如在上面的D类 class D : public B, public C
{
public:int _d;
};此时D类实例化会先初始化B再初始化C最后初始化自己所以初始化的顺序与继承时声明父类的顺序有关 继承和组合 除了可以通过继承使用父类中的成员外还可以通过 组合 的方式进行使用前提是父类对象中的成员是public权限允许在类外访问或可以通过函数进行访问 关于继承和组合 公有继承is-a — 高耦合可以直接使用父类成员组合has-a — 低耦合可以间接使用父类成员 实际项目中更推荐使用 组合 的方式这样可以做到 解耦避免因父类的改动而直接影响到子类不过具体使用哪种方式还要取决于具体场景具体问题具体分析 //父类
class A {};//继承-直接使用
class B : public A {};//组合
class C
{
private:A _oa; //创建 _oa 对象使用成员及方法
}继承的作用主要是为多态做准备继承是多态不可或缺的一步 最后
以上就是关于C继承的内容本节我们介绍了面向对象三大特性之一的继承介绍了什么是继承怎么用有那些问题等等最后介绍了C多继承中的问题菱形继承使用虚继承解决了这个问题虚继承的原理等这些知识将为后面的多态进行铺垫大家在学习后一定要动手实践多多琢磨
本次 C 继承 就先介绍到这里啦希望能够尽可能帮助到大家。
如果文章中有瑕疵还请各位大佬细心点评和留言我将立即修补错误谢谢 其他文章阅读推荐 C STL容器适配器 -CSDN博客 C STL之list模拟实现 -CSDN博客 C STL之list使用 -CSDN博客 欢迎读者多多浏览多多支持!