热门标签 | HotTags
当前位置:  开发笔记 > 程序员 > 正文

面向对象三个特性之继承(C++)

目录1.继承的概念和定义1.1继承的概念1.2继承的定义1.2.1定义格式1.2.1不同继承方式下父类成员访问方式的变化2.父类和子类对象赋值转换3.继承中的作用域

目录

1.继承的概念和定义

1.1继承的概念

1.2继承的定义

1.2.1定义格式

1.2.1不同继承方式下父类成员访问方式的变化

2.父类和子类对象赋值转换

3.继承中的作用域

3.1成员变量

 3.2成员函数

4.子类的默认成员函数

 4.1子类的默认构造函数

 4.2子类的默认析构函数

4.3子类的默认拷贝构造函数

4.4子类默认operator=

5.继承与友元

5.1友元函数

5.2继承中的友元函数

6.继承与静态成员

7.菱形继承及菱形虚拟继承

7.1单继承、多继承

7.1.1单继承:一个类只有一个直接父类。

 7.1.2多继承:一个类有两个或两个以上的直接父类。

 7.1.3菱形继承(多继承的一种特殊情况)

7.2菱形继承存在的问题 

7.3菱形继承的二义性

7.4虚拟继承

8.继承和组合的对比




1.继承的概念和定义


1.1继承的概念


继承:代码复用的重要的手段,使得子类具有父类的属性、重新定义、追加属性和函数等;可以在保持原有类特性的基础上进行扩展,增加功能。


继承的概念并不是固定的,只要能够通过自己的语言组织起来,再结合一些实例能够解释就可以了。


1.2继承的定义

既然提到继承的定义,那么至少要有两个类才能够完成,我们可以先定义一个Person类:

class Person
{void show(){cout <<"name:" <};

1.2.1定义格式

以学生(Student)类为例,继承Person类:

class Student : public Person
{};

1.Person类是父类,也称为基类;Student类是子类,也称为派生类。

2.继承方式和访问限定符一样,也有三种(public、protected、private),但是和访问限定符表示的有所差别。



1.2.1不同继承方式下父类成员访问方式的变化

下面来演示一下,不同限定符的父类成员在子类中的变化:

1.父类中的public成员,公有继承

class Person
{
public:void show(){cout <<"name:" <};
class Student : public Person
{};
int main()
{Student s1;s1.name = "阿飞";s1.age = 19;s1.show();return 0;
}

Student中没有任何成员,只有从Person类中继承下来的name和age。


2.父类的protected成员,公有继承

同样使用1中的Person类,只是把成员变量name和age改为了protected:

class Person
{
public:void show(){cout <<"name:" <protected:string name;int age;
};

子类继承之后成员属性为protected,不能在类外进行访问。


 protected属性的成员在类内是可以访问的:

class Student : public Person
{
public:void Set(string m_name, int m_age){name = m_name;age = m_age;}
};
int main()
{Student s1;s1.Set("阿飞", 19);return 0;
}

类外无法访问类内的protected/private成员,但是可以设置公有的接口对类内的protected/private成员进行访问。 


3.父类的private成员,公有继承

上面提到,父类的private成员在子类中是不可见的,那么这个不可见是什么含义呢?

class Person
{
public:void show(){cout <<"name:" <private:string name;int age;
};

在子类Student中设置公有的属性去访问父类中的private是否可行?


不可见: 子类对象在类内和类外都无法进行访问。

一般的话,我们不会设置父类的成员为private,除非不行被子类继承的成员。


关于继承方式和访问限定符就演示这三种情况,剩下的几种情况大家感兴趣的话可以自己去演示一下。


下面进行一下总结:

  1. 父类中的private成员在派生类中无论以什么方式继承都是不可见的。(语法上限制子类对象不管是在类内还是类外都不能访问)
  2. 如果子类成员不想在类外被访问,但需要在类内访问的,就可以定义为protected。
  3. 父类的私有成员子类不可见,其他成员在子类中的访问方式 为继承方式和访问限定符中权限小的一个。(public > protected > private)
  4. 使用class定义类时默认的继承方式是private,使用struct默认继承方式为public,不过最好显示写出继承方式。
  5. 实际运用中一般使用public继承,很少用到protected和private继承。

     



2.父类和子类对象赋值转换

Student类公有继承Person类:

class Person
{
public:string name;int age;
};
class Student : public Person
{
public:int id;//学号
};

定义一个子类的对象,那么能不能赋值给父类?如果能是否发生了类型的转换?

int main()
{Student s;Person p = s;return 0;
}

通过编译,可以得出结论:子类对象可以赋值给父类对象。

在子类对象赋值给父类对象的时候,实际上发生了切片,把子类对象中继承父类的成员切割赋值给了父类对象。 


下面对以上结论进行扩展,既然子类对象可以赋值给父类,那么子类对象的地址能不能赋值给父类对象的指针?子类对象能不能赋值给父类对象的引用?

int main()
{Student s;Person p = s;Person* pp = &s;Person& ps = s;return 0;
}

这两种情况同样也是正确的,通过画图来加深一下理解:

  


上面我们遗留了一个问题:赋值的时候发生了切割, 为什么不是类型转换?

int main()
{int a = 10;const double& d = a;//a赋值给d的时候产生一个临时变量,临时变量具有常性,不加上const会报错return 0;
}

子类对象赋值给父类引用的时候,没有加const,而且没有报错,说明没有发生类型转换。


注意:子类对象可以赋值给父类对象,但是父类对象不能赋值给子类对象。



3.继承中的作用域


3.1成员变量

每一个变量都有其对应的作用域,类中也有属于自己的类域;而且不同的类有不同的类域。

父类和子类中的成员在不同的类域中。

class Person
{
public:string name;int age;
};
class Student : public Person
{
public:string name;//父类中有同名的nameint id;//学号
};
int main()
{Student s1;s1.name = "阿飞";s1.age = 19;return 0;
}

通过s1访问name,首先访问的是Student类中的name,因为在这里存在一个就近原则;s1属于Student类,首先调用Student类域中的name。



 3.2成员函数

class Person
{
public:void func(int n){cout <<"func(int n)" <};
class Student : public Person
{
public:void func(){cout <<"func()" <};
int main()
{Student s1;s1.func();return 0;
}

成员函数的调用同样满足就近原则:

那如果使用子类对象传参数调用func(),运行结果是什么?

答案是:编译报错。


这里来总结一下:


  1. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫作重定义。
  2. 在子类成员函数中,可以使用  父类::父类成员 显示访问。
  3. 成员函数的隐藏,只需要函数名相同。 

显示访问一下父类中的func():

注意:父类和子类中的同名函数参数不同,并不能构成函数重载;因为函数重载要求函数必须要在相同的作用域。


4.子类的默认成员函数

提供一个Person类,类中提供了构造函数(有缺省值)、拷贝构造函数、operator=、析构函数。

class Person
{
public:Person(const char* name = "peter"): _name(name){cout <<"Person()" <protected:string _name; // 姓名
};

 4.1子类的默认构造函数


  1. 子类成员,跟类和对象一样(内置类型不处理,自定义类型调用它的构造函数)。
  2. 继承的父类成员,必须调用父类的构造函数。

class Student : public Person//子类Student公有继承Person类
{
protected:int _id; //学号
};

Student(const char* name, int id): Person(name)//调用父类的构造函数, _id(id)
{cout <<"Student()" <}

注意:子类构造函数的调用顺序是父类先于子类。



 4.2子类的默认析构函数


  1. 子类成员,跟类和对象一样(内置类型不处理,自定义类型调用它的析构函数)。
  2. 继承的父类成员,必须调用父类的析构函数。

~Student()
{~Person();cout <<"~Student()" <}

析构函数可以这样写吗?


由于多态的需要,父类和子类析构函数的名字会统一处理为destructor();

这也就造成了子类的构造和父类的构造构成了隐藏。


指定调用父类的析构:

~Student()
{Person::~Person();cout <<"~Student()" <}

如果显式的调用析构会存在一个问题,创建一个子类对象,清理的时候会调用两次父类的析构。

注意:子类析构函数不需要显式调用父类的析构函数。

每个子类析构函数后面,会自动调用父类的析构函数,这样才能保证先析构子类,再析构父类(栈中先进后出)。



4.3子类的默认拷贝构造函数


  1. 子类的成员跟类和对象一样(内置类型值拷贝,自定义类型调用它的拷贝构造)
  2. 继承的父类成员,必须调用父类的拷贝构造。

Student(const Student& s): Person(s)//子类传参给父类时发生切片, _id(s._id)
{cout <<"Student(const Student& s)" <}

 

 拷贝构造函数调用之后,s2中的_name和_id都是相等的,显式写的子类拷贝构造完成。



4.4子类默认operator=


  1. 子类的成员跟类和对象一样(内置类型值拷贝,自定义类型调用它的operator=)
  2. 继承的父类成员,必须调用父类的operator=。

一种错误示例写法:

Student& operator = (const Student& s)
{cout <<"Student& operator= (const Student& s)" <}

 父类中的operator=和子类中的函数名相同,构成了隐藏,如果不显式调用父类中的operator=,会不断的进行子类operator,最后导致栈溢出。 


正确写法:

Student& operator = (const Student& s)
{cout <<"Student& operator= (const Student& s)" <}

5.继承与友元


5.1友元函数


友元函数:某些虽然不是类中的成员却能够访问类的所有成员的函数,类授予它的友元特别的访问权。



class Person
{friend void Display(const Person& p);//Display是Person类的友元函数
public:void SetName(const string& name){_name = name;}
protected:string _name; // 姓名
};
void Display(const Person& p)
{cout <}
int main()
{Person p;p.SetName("阿飞");Display(p);return 0;
}


友元函数解决了类外不能访问类中protected/private的问题。



5.2继承中的友元函数

友元关系不能继承,也就是说父类的友元函数不能访问子类中的protected和private成员。

class Student;//声明子类
class Person
{
public:friend void Display(const Person& p, const Student& s);//父类的友元函数
protected:string _name = "阿飞"; // 姓名
};
class Student : public Person
{
protected:int _stuNum = 666; // 学号
};
void Display(const Person& p, const Student& s)
{cout <}
int main()
{Person p;Student s;Display(p, s);return 0;
}

函数Display()是父类Person的友元函数,可以访问父类中的protected/private成员;

但是友元关系不能继承,所以无法访问子类中的_stuNum。


 如果同时想要访问子类中的protected/private,可以把函数声明为子类的友元。

class Student : public Person
{friend void Display(const Person& p, const Student& s);
protected:int _stuNum; // 学号
};

6.继承与静态成员

一般成员在子类和父类中都是单独的一份,而静态成员在父类和子类中是同一份。


class Person
{
public:static int _count;
};
int Person::_count = 0;//静态成员只能在类外进行初始化
class Student : public Person
{};
int main()
{Person p;Student s;cout <}


​ 

子类对象和父类对象中的静态成员_count是同一份,改变父类对象中的_count,子类对象中的_count也会随之改变。 



7.菱形继承及菱形虚拟继承


7.1单继承、多继承


7.1.1单继承:一个类只有一个直接父类。


 7.1.2多继承:一个类有两个或两个以上的直接父类。


 7.1.3菱形继承(多继承的一种特殊情况)


7.2菱形继承存在的问题 

 从上图的对象模型,可以看出菱形继承有数据冗余和二义性的问题。(在Assistant对象中Person成员有两份)


7.3菱形继承的二义性

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";return 0;
}

 菱形继承的二义性导致Assistant类中的_name访问不明确的问题,此问题可以显式指定类域来解决。


int main()
{Assistant a;a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}

虽然可以指定类域来进行访问,但是这样无法从根本上解决菱形继承存在的问题。



7.4虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余问题。

class Student : virtual public Person
{
protected:int _num; //学号
};
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};

下面来借助VS2019中的调试内存窗口来观察一下类对象成员分配:

1.菱形继承(非虚拟继承)

B中和C中都有一份A,导致数据冗余。

2.菱形虚拟继承


下图是菱形虚拟继承的内存对象成员模型:A同时属于B和C,那么B和C如何去找公共的A呢?

这里通过了B和C的两个指针,指向的一张表;这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量,通过偏移量可以找到A。


使用到虚基表指针和虚基表的两种情况举例:

int main()
{//1.切片->需要找到AD d;B b = d;B* pb = &d;//2.通过父类B、C访问A中的成员->通过偏移量和地址找到_apb->_a = 10;return 0;
}

8.继承和组合的对比


  • public继承是一种is-a的关系;例如 Student is-a Person
  • 组合是一种has-a的关系;例如 车has-a轮胎(a只是一个量词,实际不一定是一个)
  • 如果两个物体之间的关系既可以是is-a,也可以使has-a,那么优先使用has-a的组合

在编程中,我们追求的是一种“高内聚,低耦合”:继承一定程度上破坏了基类的封装,父类中的protected属性的成员在子类内是可以使用的,父类和子类之间的依赖关系强,耦合度高。

但如果使用的是组合,那么protected属性脱离了类内,在类外不能够使用,依赖关系较弱,耦合较低。

也有些关系适合使用继承,例如要实现多态就必须要使用继承。


面向对象三大特性之——继承到这里就结束了,喜欢这部分内容的铁汁们可以给博主一个三连支持,你们的支持是博主最大的动力,后续会继续更新面向对象三大特性中的多态,喜欢的铁汁们记得三连哈。


推荐阅读
  • 深入解析Android自定义View面试题
    本文探讨了Android Launcher开发中自定义View的重要性,并通过一道经典的面试题,帮助开发者更好地理解自定义View的实现细节。文章不仅涵盖了基础知识,还提供了实际操作建议。 ... [详细]
  • 本文详细分析了JSP(JavaServer Pages)技术的主要优点和缺点,帮助开发者更好地理解其适用场景及潜在挑战。JSP作为一种服务器端技术,广泛应用于Web开发中。 ... [详细]
  • 1.如何在运行状态查看源代码?查看函数的源代码,我们通常会使用IDE来完成。比如在PyCharm中,你可以Ctrl+鼠标点击进入函数的源代码。那如果没有IDE呢?当我们想使用一个函 ... [详细]
  • 数据管理权威指南:《DAMA-DMBOK2 数据管理知识体系》
    本书提供了全面的数据管理职能、术语和最佳实践方法的标准行业解释,构建了数据管理的总体框架,为数据管理的发展奠定了坚实的理论基础。适合各类数据管理专业人士和相关领域的从业人员。 ... [详细]
  • 本文详细介绍了Java中org.eclipse.ui.forms.widgets.ExpandableComposite类的addExpansionListener()方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。这些示例来源于多个知名开源项目,具有很高的参考价值。 ... [详细]
  • 解决PHP与MySQL连接时出现500错误的方法
    本文详细探讨了当使用PHP连接MySQL数据库时遇到500内部服务器错误的多种解决方案,提供了详尽的操作步骤和专业建议。无论是初学者还是有经验的开发者,都能从中受益。 ... [详细]
  • Java内存管理与优化:自动与手动释放策略
    本文深入探讨了Java中的内存管理机制,包括自动垃圾回收和手动释放内存的方法。通过理解这些机制,开发者可以更好地优化程序性能并避免内存泄漏。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • DNN Community 和 Professional 版本的主要差异
    本文详细解析了 DotNetNuke (DNN) 的两种主要版本:Community 和 Professional。通过对比两者的功能和附加组件,帮助用户选择最适合其需求的版本。 ... [详细]
  • Python自动化处理:从Word文档提取内容并生成带水印的PDF
    本文介绍如何利用Python实现从特定网站下载Word文档,去除水印并添加自定义水印,最终将文档转换为PDF格式。该方法适用于批量处理和自动化需求。 ... [详细]
  • 尽管某些细分市场如WAN优化表现不佳,但全球运营商路由器和交换机市场持续增长。根据最新研究,该市场预计在2023年达到202亿美元的规模。 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • 本文探讨了如何在编程中正确处理包含空数组的 JSON 对象,提供了详细的代码示例和解决方案。 ... [详细]
  • Ralph的Kubernetes进阶之旅:集群架构与对象解析
    本文深入探讨了Kubernetes集群的架构和核心对象,详细介绍了Pod、Service、Volume等基本组件,以及更高层次的抽象如Deployment、StatefulSet等,帮助读者全面理解Kubernetes的工作原理。 ... [详细]
author-avatar
娶位红太郎_442
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有