自学内容网 自学内容网

[c++高阶] 继承深度剖析

1.前言

   继承 是 面向对象三大特性之一(封装、继承、多态),所有的面向对象(OO)语言都具备这三个基本特征,封装相关概念已经在《类和对象》系列中介绍过了,今天主要学习的是 继承,即如何在父类的基础之上,构建出各种功能更加丰富的子类。

例:

在继承父类动物的基础上,展现出了自己的特性--即子类。

本章重点

本章将着重的讲解继承的概念和定义,父类和子类对象的赋值转换,继承的作用域,继承中父类和子类的默认成员函数,继承与友元,继承与静态成员以及菱形继承,和虚继承的概念。

2.继承的概念和定义

2.1 什么是继承

        继承(inheritance)机制是 ----面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有基类(父类)特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类(子类) 。

看官方解释:

这里就延伸了几个概念:

  • 被继承对象:父类 / 基类 (base
  • 继承方:子类 / 派生类 (derived

继承的本质就是 ------------ 复用代码 --避免重复的代码写多次

举一个例子:

假设我现在要设计一个校园师生管理系统,那么肯定会设计很多角色类,比如学生、老师之类的。 

设计好以后,我们发现,有些数据和方法是每个角色都有的,而有些则是每个角色独有的。 

为了复用代码、提高开发效率,可以从各种角色中选出共同点,组成基类,比如每个人都有姓名、年龄、联系方式等基本信息,而教职工与学生的区别就在于管理与被管理,因此可以在 基类 的基础上加一些特殊信息如教职工号表示教职工,加上学号表示学生。

这样就可以通过 继承 的方式,复用 基类 的代码,划分出各种 子类 。

基类:

class Person {
public:
Person(string name,int age = 18, string tel = "123456789", string address = "njust")
:_age(age)
,_name(name)
,_tel(tel)
,_address(address)
{}
void Print()
{
std::cout << "我的姓名是:" << _name << endl;
std::cout << "年龄是:" << _age << endl;
std::cout << "电话号码是:" << _tel << endl;
std::cout << "地址是:" << _address << endl;
}
private:
string _name;
int _age;
string _tel;
string _address;
};

子类:Student 和Ter

class Student : public Person
{
public:
Student(int stuid=123)
:_stuId(stuid)
,Person("张三",20,"1233445")
{
std::cout << "我是一个学生,我的信息如下" << endl;
std::cout << "我的学号是:" << _stuId << endl;
}
private:
int _stuId;
};
class Ter :public Person
{
public:
Ter(int workid=012)
:_workid(workid)
,Person("李四",45,"123234354")
{
std::cout << "我是一个老师,我的信息如下" << endl;
std::cout << "我的学号是:" << _workid << endl;
}
private:
int _workid;
};

继承后父类的 Person 成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student 和 Teacher 复用了 Person

2.2 继承的定义

了解了什么是继承,那么就要开始使用继承了。

定义格式:

格式如下: Person 是 父类,也称作基类Student 是 子类,也称作派生类。 

格式为 :子类 : 继承方式 父类,比如 class a : public b 就表示 a 继承了 b,并且还是 公有继承 

2.3 继承权限

继承有权限的概念,分别为:公有继承(public)、保护继承(protected)、私有继承(private 。

那么看到这里有小伙伴就比较疑惑了哈,这不是跟我们之前学过的访问限定符一致嘛。没错,就是一致,只不过在这里我们把他当成继承的权限。

根据排列组合,我们可以有9种搭配方案

外部不可见---言外之意就是子类的对象要想访问基类的相关成员,要受到访问限定符的限制。

总结:

无论是哪种继承方式,父类中的 private 成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,依据 min(父类成员权限, 子类继承权限),只有最终权限为 public 时,外部才能访问
在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

至于如何证明相关结论,这里给出一段代码,可自行验证

// 父类
class A
{
public:
int _a;
protected:
int _b;
private:
int _c;
};
 
// 子类
class B : public A
{
public:
B()
{
cout << _a << endl;
cout << _b << endl;
cout << _c << endl;
}
};
 
int main()
{
// 外部(子类对象)
B b;      
b._a;
}

3.基类和派生类的赋值转换

在继承中,允许将 子类 对象直接赋值给 父类,但不允许 父类 对象赋值给 子类 

有以下四个结论:

1.子类对象可以给父类对象赋值

2.子类对象可以给父类指针赋值

3.子类对象可以给父类引用赋值

4.父类不可以给子类赋值

那么子类是如何把值赋给父类的呢?

根据切片的形式,即简单的理解为把父类的相关成员函数切割出来,然后赋值给父类的成员。

4.继承的作用域

在继承体系中 基类 和 派生类 都有独立的作用域,如果子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问,这种情况叫 隐藏,也叫重定义。

函数重载是在同一个作用域下面,才构成函数重载。而隐藏即重定义就是说在两个不同的作用域定义了相同的成员变量。

看现象:

// 基类
class Person
{
protected:
string _name = "Edison"; // 姓名
int _num = 555; // 身份证号
};
 
// 派生类
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl;
}
protected:
int _num = 888; // 学号
};
 
int main()
{
Student s1;
s1.Print();
 
return 0;
}

s1里面打印_num是会优先打印派生类的,如果想打印出基类的_num,那么就需要用 基类 :: 基类成员显示的去访问

结论:

1.在继承中,基类和派生类是拥有不同的作用域

2.如果基类和派生类中有相同的定义,那么如果想访问基类中的可以使用基类::基类成员的方式

3.在继承中,函数构成隐藏只需要函数名相同即可。

4.最好不要在基类和派生类中出现对同一变量的定义

5.子类中的默认成员函数

派生类(子类)也是 ,同样会生成 六个默认成员函数(用户未定义的情况下)

不同于单一的 子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为 父类 进行考虑

直接给出结论:

1.派生类的构造函数被调用时,会自动调用基类的构造函数初始化基类的那一部分成员,如果基类当中没有默认的构造函数,则必须在派生类构造函数的初始化列表当中显示调用基类的构造函数。
2.派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造。
3.派生类的赋值运算符重载函数必须显示调用基类的赋值运算符重载函数完成基类成员的赋值。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
5.派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。
6.派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。

这里解释一下:为什么在析构的时候,要析构子类,在析构父类

  • 和前面的默认成员函数不同,在实现派生类的析构时,基类的析构不能显式调用
  • 这是因为,如果显示调用了基类的析构,就会导致基类成员的资源先被清理,如果此时派生类成员还访问了基类成员指向的资源,就会导致野指针问题
  • 因此,必须保证析构顺序为先子后父,保证数据访问的安全

6.继承与友元

友元关系不能继承,也就是说 基类友元不能访问子类私有和保护成员,只能访问自己的私有和保护成员。 

下面代码中,Display 函数是基类 Person 的友元,但是 Display 函数不是派生类 Student 的友元,也就是说 Display 函数无法访。

class Student;
 
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; // 无法访问
}
 
int main()
{
Person p;
Student s;
Display(p, s);
 
return 0;
}

7.继承与静态成员

如果基类中定义了static 静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个 static 成员实例

8.菱形继承

8.1 单继承

一个子类只有一个直接父类时称这个继承关系为单继承 

8.2 多继承

 一个子类有两个或以上直接父类时称这个继承关系为多继承

8.3 菱形继承

 C++ 支持多继承,即支持一个子类继承多个父类,使其基础信息更为丰富,但凡事都有双面性多继承 在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题

会出现如下情况:

此时会有一个问题,类D的实例化对象中有类B和类C,然而B类和C类都有A类,所以说D类对象中的A类成员就重复了!

具体示例如下:

class A
{
public:
int _a = 1;
};

class B :public A
{
public:
int _b = 2;
};

class C :public A
{
public:
int _c = 3;
};

class D :public B, public C
{
public:
int _d = 4;
};

int main()
{
D dem;
return 0;
}

现象如下:

在一个dem里面出现了两份_a,这就导致了数据冗余和二义性的问题。

修改代码如下:

int main()
{
D dem;
cout << dem._a << endl;
return 0;
}

这是因为有两个_a,当你没有明确指定使用哪一个时,编译器也无法确定。

解决办法:通过::限制访问域即可

修改代码如下:

int main()
{
D dem;
cout << dem.A::_a << endl;
return 0;
}

就不会报错了。能够精准的知道是访问那个_a。但是这只是解决了二义性的问题,没有解决数据冗余的问题,即一个对象里面存放了两个不同的_a

那么真正解决这个问题的是:虚继承。

ps: 虚继承是专门用来解决 菱形继承 问题的,与多态中的虚函数没有直接关系

虚继承:在菱形继承的腰部继承父类时,加上 virtual 关键字修饰被继承的父类 

修改代码如下

class A
{
public:
int _a = 1;
};

class B :virtual public A
{
public:
int _b = 2;
};

class C :virtual public A
{
public:
int _c = 3;
};

class D :public B, public C
{
public:
int _d = 4;
};

int main()
{
D dem;
cout << dem._a << endl;
return 0;
}

这是打印出的结果就是1.

那么虚继承是如何解决这个问题的呢?

  • 利用 虚基表 将冗余的数据存储起来,此时冗余的数据合并为一份
  • 原来存储 冗余数据 的位置,现在用来存储 虚基表指针

此时无论这个 冗余 的数据存储在何处,都能通过 基地址 + 偏移量 的方式进行访问 

看现象:

虚基表里面存放的都是一些偏移量,当访问这个公共的数据时,先访问续集表指针,再通过续集表指针里面的地址来找到该值。

9.总结

继承就说到这里了,继承在cpp中是非常关键的一个知识点,在后续的多态学习中非常重要,也是面试里面常被问到的一个知识点,希望各位阅读文章的小伙伴能够打好基础。

如有兴趣的小伙伴还想了解一下is-a和has-a的关系的话,可以阅读下面文章

【C++】继承最全解析(什么是继承?继承有什么用?)_类的继承有什么用-CSDN博客

本篇文章的撰写也参考了上述文章的相关知识与结构。


原文地址:https://blog.csdn.net/weixin_62196764/article/details/142713722

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!