C++中的继承

简介: C++中的继承

1 继承的定义

继承的概念:就是把某些类中共有的成员变量或者是成员方法抽取出来,形成一个类,为了更好的实现代码的复用!

继承定义的格式如下:

那么联想到之前所学的访问限定符,在结合今天所学的继承方式。那么在子类中对于父类成员访问方式的变化可以总结如下:

举个例子来说明一下吧!如果我们采用public继承方式,父类中的成员变量是private修饰,那么此时子类就不能对父类成员变量进行访问,也就是在子类中不可见,不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它!

总结一下:

1️⃣除了父类成员变量是用private修饰的,在子类中不可见!其他情况下,子类对于父类成员变量的访问方式取决于父类成员变量访问限定符和继承方式中更小的那个权限

2️⃣class与struct区别就是:class默认的访问权限是private,在继承时,也是默认private方式继承。而struct恰恰相反,默认的访问权限是public,继承方式也是默认public,但是我们更加提倡更直观的写出继承方式!

3️⃣一般我们都是用public继承方式更多!


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

我们先来回顾一下之前学过的一个代码片段:

为什么这里就会报错呢?原因很简单,就是类型不一样,赋值的过程中是会产生一个临时变量!如下图所示:

而我们知道临时变量是具有常性的,只能读不可以写的,而默认的引用是可读可写的,所以我们就必须在double前面加上const!

那我们来看下面这段代码:

那么为什么这里就没有报错呢?而且这里做的原理是什么呢?首先我来回答第一个问题,因为这里就是C++中的一个规定,这里就不会产生临时变量!上述赋值过程可以用下图来进行表示!

所以就是子类对象可以赋值给父类的对象/父类的指针/父类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。还要注意的就是父类的对象是不可以赋值给子类对象的!

3 继承中的作用域

子类与父类是独立的作用域!如果子类与父类中有相同的名字的成员变量,那么想要访问父类中的那个成员就必须指定对应的作用域!所以一般不建议取有重名的成员变量!

de058cfc348529c53c13d58c1e27e04a_29182095cf6d439ab2d68b48b86d9d0a.png


3.1 重定义(隐藏)

定义:子类和父类中存在相同名字的函数就构成了重定义(隐藏),不管参数类型以及返回值是什么,包括我们上面将的有同名的成员变量,也叫做重定义(隐藏)

例子如下:

在这里,即使子类与父类的参数不同,返回值不同,也是调用不到父类中的print的!所以我们想要调用父类的,就必须指定作用域!还需要注意的是,重载是在同一个作用域下面,参数不同,函数名一样就称之为重载!

4 默认的成员函数

我们采用以下代码来理解一下,在继承中,默认的成员函数是怎么做的。

class Person
{
public :
 Person(const char* name = "peter")
 : _name(name )
 {
 cout<<"Person()" <<endl;
 }
    
 Person(const Person& p)
 : _name(p._name)
 {
 cout<<"Person(const Person& p)" <<endl;
 }
    
 Person& operator=(const Person& p )
 {
 cout<<"Person operator=(const Person& p)"<< endl;
 if (this != &p)
 _name = p ._name;
        
 return *this ;
 }
    
 ~Person()
 {
 cout<<"~Person()" <<endl;
 }
protected :
 string _name ; // 姓名
};
class Student : public Person
{
public :
 Student(const char* name, int num)
 : Person(name )
 , _num(num )
 {
 cout<<"Student()" <<endl;
 }
 
 Student(const Student& s)
 : Person(s)
 , _num(s ._num)
 {
 cout<<"Student(const Student& s)" <<endl ;
 }
 
 Student& operator = (const Student& s )
 {
 cout<<"Student& operator= (const Student& s)"<< endl;
 if (this != &s)
 {
 Person::operator =(s);
 _num = s ._num;
 }
 return *this ;
 } 
 
 ~Student()
 {
 cout<<"~Student()" <<endl;
 }
protected :
 int _num ; //学号
};

可以自己先创建一个Student类的对象,调用一下就可以了解


1️⃣子类的构造函数必须调用父类的构造函数初始化基类的那一部分成员。如果父类没有默认的构造函数(或者你要改变父类中的成员),则必须在子类构造函数的初始化列表阶段显式调用。并且是先调用完父类的构造函数,在去调用子类的构造函数

2️⃣子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

3️⃣ 子类的operator=必须要调用父类的operator=完成父类的复制。并且必须指定类域,因为函数名相同已经构成了隐藏

4️⃣在析构函数中,不用去调用父类的析构函数的,会先清理完子类中的资源,然后自动调用父类中的析构函数,从而清理父类中的资源

5️⃣子类与父类之间的析构函数构成了隐藏关系,因为编译器会对析构函数名进行特殊处理,处理成destrutor()


5 多继承

单继承:顾名思义,就是只继承了一个父类

多继承:就是继承了多个父类

5.1 菱形继承

在C++中,正是因为有多继承的出现,所以就会出现菱形继承!如下图所示:

D类继承了B,C两个类,然后B,C两个类都继承了同一个类,这种情况就叫做菱形继承!

菱形继承就会带来以下两个问题

1 成员变量的二义性(就是不确定性)

2 代码冗余

例如下图所示

这就是一个典型的二义性问题,这是为什么呢?让我来画一个内存图来理解一下:

因为D类继承了B,C两个类,而B,C两个类又继承了A类,所以在D对象中a这个成员变量会存储两遍!如果访问成员变量a,那么到底是B类中的a呢,还是C类中的a呢?当然我们可以指定类域来进行访问!但是如果A中的成员变量不止是一个a呢?那代码的冗余程度是不是就大了!


5.2 解决办法

那么如何解决菱形继承问题呢?在C++中采用了虚继承的方式来解决!如下图所示:

88cd7c087687f66524a63d769031e15a_d08abb1cb60848f79431281a7f237aa2.png

那么此时对应的内存图又是张啥样的呢?如下图所示:

97f57b22fe133ea4d81ef6dbb6e30313_c2ec6b82f5ce4c3fa6d3b852e12df084.png


此时成员变量a就只是存储了一份,并且B,C类中的成员中存储的是地址,加上偏移量,就可以指向成员变量a所在的地址,从而就找到了a了!


6 组合与继承

什么是组合呢?如下图所示就是一种组合

就是在一个类中,用到了另一个类中的方法或者成员函数!本质上其实也是一种代码的复用!像我们前面所学过的反向迭代器就是一种组合!


组合和继承的区别在哪里?

1️⃣ 组合的耦合性更低,继承的耦合性更高!举个例子说明一下,如果一个项目需要改动代码,如果是在继承关系里面改动的,那么成员函数或者变量的改变就会影响其他模块也必须跟着改,但是如果是组合,只有公共的成员函数或者函数改变,才会影响着对应的模块改变!

2️⃣组合本质上就是一种黑箱复用,你只需要知道组合使用的那个类,可以实现那些个功能就可以了,不用关心实现的细节!而继承也是一种白箱复用,我们需要了解父类中是如何实现的,而不是仅仅关注功能!要求会更高

3️⃣组合就是一种 has-a的关系,继承则就是一种is-a的关系!


对于继承与组合的使用,在这里建议尽量使用组合,降低代码的耦合性!

toKeep
+关注
目录
打赏
0
0
0
0
23
分享
相关文章
|
1月前
|
c++--继承
c++作为面向对象的语言三大特点其中之一就是继承,那么继承到底有何奥妙呢?继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用,继承就是类方法的复用。
58 0
|
4月前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
236 6
|
6月前
|
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
111 16
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
150 5
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
96 1
【C++】继承
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
158 11
|
9月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
106 1
|
9月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
105 1
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
72 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问