广告位联系
返回顶部
分享到

C++继承与菱形继承的介绍

C语言 来源:互联网 作者:佚名 发布时间:2022-08-27 09:08:09 人浏览
摘要

继承的概念和定义 继承机制是面向对象程序设计的一种实现代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加其他的功能,在此基础上也就产生了一个新的

继承的概念和定义

继承机制是面向对象程序设计的一种实现代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加其他的功能,在此基础上也就产生了一个新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,是类设计层次的复用。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

//以下代码就是采用了继承机制的一个场景

class person

{

protected:

    char _name[28];

    int _age;

    char _id[30];

};

//继承是代码复用的一种重要手段

class student :public person

{

protected:

    char _academy[50]; //学院

};

继承的格式

在前面的例子中,person是基类,student是派生类,继承方式是public. 这是很容易记忆的,person是基础的类,student是在person这个类的基础之上派生出来的。这就非常地像父子关系,所以基类又可以称为父类,派生类又可为子类。子类的后面紧跟着:,是:后面这个类派生出来的。

继承关系和访问限定符

继承的几种方式和访问限定符是相似的。

三种继承方式:public继承、protected继承、private继承。

三种访问限定符:public访问、protected访问、private访问。

基类类成员的访问权限和派生类继承基类的继承方式, 关系到了基类被继承下来的类成员在派生类中的情况。ps:这句话起始很好理解地,就是这句话写起来就变得绕口和复杂了,哈哈哈????.

基类成员/继承方式 public继承 protected继承 private继承
public成员 在派生类中为public成员 在派生类中为protected成员 在派生类中为private成员
protected成员 在派生类中为protected成员 在派生类中为protected成员 在派生类中为private成员
private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

这里的不可见指的是:基类中的private成员也是被继承下来了的,只是在语法上,在派生类的类里和类外都不能够访问。

记住这个特殊的点,那么其他的就可理解为“权限问题”,这里“权限只能缩小,不能放大”。例如,基类的public成员以private继承方式继承下来,为“权限小的那个”,也就是继承下来后在派生类中是private成员。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

class person

{

protected:

    char _name[28];

    char _id[30];

private:

    int _age;

};

class teacher :public person

{

public:

    teacher()

        :_age(0) //基类的private成员在派生类里不能访问

    {

    }

protected:

    char _jodid[20]; //工号

};

int main(void)

{

    teacher t1;

    t1._age; //基类的private成员在类外不能访问

    return 0;

}

基类和派生类之间的赋值

派生类的对象可以赋值给其基类的对象、基类的指针、基类的引用。

就像上面这样,取基类需要被赋值的值过去即可。

派生类赋值给基类的对象、基类的指针、基类的引用。在派生类中取基类需要的,就像把派生类给切割了一样、所以这里有一个形象的称呼:切割/切片

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

class Person

{

protected:

    string _name; // 姓名

    string _sex; // 性别

    int _age; // 年龄

};

class Student : public Person

{

public:

    int _id; // 学号

};

int main(void)

{

    //可以将派生类赋值给基类的对象、指针、引用

    Person p;

    Student s;

    p = s;

    Person* Pptr = &s;

    Person& Refp = s;

    //注意不能将将基类对象给派生类对象

    //s = p;

    //允许将基类指针赋值给派生类指针,但是需要强制转换

    Student* sPtr = (Student*)Pptr;

    return 0;

}

【注意】

1、不允许基类对象赋值给派生类对象

2、允许基类指针赋值给派生类指针, 但是需要强制转化。这种转化虽然可以,但是会存在越界访问的问题。

继承中的作用域

基类和派生类都有独立的作用域。继承下来的基类成员在一个作用域,派生类的成员在另一作用域。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

//以下代码的运行结果是什么?

class Person

{

protected:

    string _name = "杨XX"; // 姓名

    int _num = 12138; // 身份证号

};

class Student : public Person

{

public:

    void Print()

    {

        cout <<_num << endl;

    }

protected:

    int _num = 52622; // 学号

};

void Test()

{

    Student s1;

    s1.Print();

};

基类中有一个_num 给了缺省值“12138”, 派生类中也有一个_name,给了缺省值“52622”,那么在派生类里直接使用_name,使用的具体是哪一个类里的?

使用的是派生类Student里的。

总结:基类和派生类中如果有同名成员,派生类将屏蔽基类对同名成员的直接访问,这种情况称为隐藏 , 或者称为重定义。

如果想要访问,则使用基类::基类成员显示的访问。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

class Person

{

protected:

    string _name = "杨XX"; // 姓名

    int _num = 12138; // 身份证号

};

class Student : public Person

{

public:

    void Print()

    {

        cout << "身份证号:" << Person::_num << endl;

        cout << "学号:" << _num << endl;

    }

protected:

    int _num = 52622; // 学号

};

void Test()

{

    Student s1;

    s1.Print();

};

int main(void)

{

    Test();

    return 0;

}

运行结果

我们已经了解了什么是隐藏。那么来看一下下面这些代码。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

//以下的两个函数构成隐藏还是重载?

class A

{

public:

    void func()

    {

        cout << "func()" << endl;

    }

};

class B : public A

{

public:

    void func(int num)

    {

        cout << "func(int num)" << endl;

    }

};

void Test()

{

    B b;

    b.func(10);

}

函数重载要求在同一作用域,而被继承下来的基类成员和派生类成员在不同的作用域,所以构成的是隐藏。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

```cpp

//以下代码的运行结果是什么?

class A

{

public:

    void func()

    {

        cout << "func()" << endl;

    }

};

class B : public A

{

public:

    void func(int num)

    {

        cout << "func(int num)" << endl;

    }

};

void Test()

{

    B b;

    b.func();

}

因为func()函数隐藏了,在派生类的作用域内没有func()函数,所以会出现编译报错。

派生类的默认成员函数

类有8个默认成员函数,这里只说重点的四个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数

如果我们不写派生类的构造函数和析构函数,编译器会做如下的事情:

1、基类被继承下来的部分会调用基类的默认构造函数和析构函数

2、派生类自己也会生成默认构造和析构函数,派生类自己的和普通类的处理一样

如果我们不写派生类的赋值构造函数和拷贝构造函数,编译器会做如下的事情

3、基类被继承下来的部分会调用基类的默认拷贝构造函数和赋值构造函数。

4、派生类自己也会生成默认赋值拷贝构造函数和赋值函数,和普通类的处理一样。

什么情况下需要自己写?

1、父类没有合适的默认构造函数,需要自己显示地写

2、如果子类有资源需要释放,就需要自己显示地写析构函数

3、如果子类存在浅拷贝的问题,就需要自己实现拷贝构造和赋值函数解决浅拷贝的问题。

如果需要自己写派生类的这几个重点成员函数,那么该如何写?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

//如果需要自己实现派生类的几个四个重点默认成员函数,需要如何实现?该注意什么?

class Person

{

public:

    Person(const char* name)

        :_name(name)

    {

        cout << "Person(const char* name)" << 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

{

protected:

    int _id; //学号

    int* _ptr = new int[10]; //给一个需要自己实现默认成员函数场景用以举例

};

1、实现派生类的构造函数:需要调用基类的构造函数初始化被继承下来的基类部分的成员。如果基类没有合适的默认构造函数,就需要在实现派生类构造函数的初始化列表阶段显示调用。

2、实现派生类的析构函数:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理被继承下来的基类成员。这样可以保证派生类自己的成员的清理先于被继承下来的基类成员。ps:析构函数名字会被统一处理成destructor(),所以被继承下来的基类的析构函数和派生类的析构函数构成隐藏。

3、实现派生类的拷贝构造函数:需要调用基类的拷贝构造函数完成被继承下来的基类成员的拷贝初始化。

4、实现派生类的operator=:需要调用基类的operator=完成被继承下来的基类成员的赋值。

5、派生类对象初始化先调用基类构造再调用派生类构造。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

class Student : public Person

{

public:

    Student(const char* name, int id)

        : Person(name)

        , _id(id)

    {

        cout << "Student()" << endl;

    }

    Student(const Student& s)

        : Person(s)

        , _id(s._id)

    {

        cout << "Student(const Student& s)" << endl;

    }

    Student& operator = (const Student& s)

    {

        cout << "Student& operator= (const Student& s)" << endl;

        if (this != &s)

        {

            Person::operator =(s);

            _id = s._id;

        }

        return *this;

    }

    ~Student()

    {

        cout << "~Student()" << endl;

    }

protected:

    int _id; //学号

};

菱形继承

继承可分为单继承和多继承。

单继承:一个派生类只有一个直接基类

多继承:一个派生类有两个或两个以上的直接基类。

而多继承中又存在着一种特殊的继承关系,菱形继承

它们之间的继承关系逻辑上就类似一个菱形,所以称为菱形继承。菱形继承相对于其他继承关系是复杂的。

B中有一份A的成员,C中也有一份A的成员,D将B和C都继承了,那么D中被继承下来的A的成员不就有两份了吗?不难看出,菱形继承有数据冗余和二义性的问题。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

class Person

{

public:

    string _name; // 姓名

};

class Student : public Person

{

public:

    int _num; //学号

};

class Teacher : public Person

{

public:

    int _id; // 职工编号

};

class Assistant : public Student, public Teacher

{

public:

    string _majorCourse; // 主修课程

};

int main()

{

    // 二义性、数据冗余

    Assistant a;

    a._id = 1;

    a._num = 2;

    // 这样会有二义性无法明确知道访问的是哪一个

    a._name = "peter";

    return 0;

}

上面的继承关系如下:

此时Assitant中有两份_name.存在数据冗余和二义性的问题。

二义性的问题是比较好解决的,使用::指定就可以了,但是并不能解决数据冗余的问题。

1

2

3

4

5

6

7

8

9

10

int main()

{

    // 二义性、数据冗余

    Assistant a;

    a._id = 1;

    a._num = 2;

    a.Student::_name = "小张";

    a.Teacher::_name = "张老师";

    return 0;

}

虚拟继承可以解决继承的数据冗余和二义性的问题。如上面所画的逻辑继承关系。在开始可能产生数据冗余和二义性的地方使用虚拟继承,即可解决,但是在其他地方不要去使用虚拟继承。

虚拟继承格式

虚拟继承解决数据冗余和二义性的原理

为了更好地研究,在这里给出一个比较简单的菱形继承体系

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

class A {

public:

    int _a;

};

class B : public A{

public:

    int _b;

};

class C : 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的数据可以看出数据的冗余。

现在增加虚拟继承机制,解决数据冗余和二义性。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

class A {

public:

    int _a;

};

class B : virtual public A {

public:

    int _b;

};

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中的地址0x00677bdc里有什么?C中的地址0x00677be4里有什么?

从内存窗口可看出,菱形虚拟继承,内存中只在对象组成的最高处地址保存了一份A,A是B、C公共的。而B和C里分别保存了一个指针,该指针指向一张表。这张表称为虚基表,而指向虚基表的指针称虚基指针。虚基表中保存的值,是到A地址的偏移量,通过这个偏移量就能够找到A了。

继承和组合的区分与联系

在没有学习继承之前,我们其实频繁地使用组合。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class head

{

private:

    int _eye;

    int _ear;

    int _mouth;

};

class hand

{

private:

    int _arm;

    int _fingers;

};

class Person

{

    //组合

    //一个人由手、头等组合

    hand _a;

    head _b;

};

  • 继承是一种is-a的关系, 每一个派生类是基类,例如,Student是一个Person, Teacher 是一个Person
  • 组合是一种has-a的关系,Person组合了head, hand, 每一个Person对象中都有一个head、hand对象。
  • 如果某种情况既可以使用继承又可以使用组合,那么优先使用对象组合,而不是类继承。

其余注意事项

  • 友元关系不能被继承,好比父亲的朋友不一定是你的朋友。
  • 如果基类中定义了静态成员,当这个基类被实例化后出现了一份,那么整个继承体系中都只有这一份实例。

版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://blog.csdn.net/qq_56870066/article/details/125536022
相关文章
  • C++中类的六大默认成员函数的介绍

    C++中类的六大默认成员函数的介绍
    一、类的默认成员函数 二、构造函数Date(形参列表) 构造函数主要完成初始化对象,相当于C语言阶段写的Init函数。 默认构造函数:无参的构
  • C/C++实现遍历文件夹最全方法总结介绍

    C/C++实现遍历文件夹最全方法总结介绍
    一、filesystem(推荐) 在c++17中,引入了文件系统,使用起来非常方便 在VS中,可以直接在项目属性中调整: 只要是C++17即以上都可 然后头文件
  • C语言实现手写Map(数组+链表+红黑树)的代码

    C语言实现手写Map(数组+链表+红黑树)的代码
    要求 需要准备数组集合(List) 数据结构 需要准备单向链表(Linked) 数据结构 需要准备红黑树(Rbtree)数据结构 需要准备红黑树和链表适配策略
  • MySQL系列教程之使用C语言来连接数据库

    MySQL系列教程之使用C语言来连接数据库
    写在前面 知道了 Java中使用 JDBC编程 来连接数据库了,但是使用 C语言 来连接数据库却总是连接不上去~ 立即安排一波使用 C语言连接 MySQL数
  • 基于C语言实现简单学生成绩管理系统

    基于C语言实现简单学生成绩管理系统
    一、系统主要功能 1、密码登录 2、输入数据 3、查询成绩 4、修改成绩 5、输出所有学生成绩 6、退出系统 二、代码实现 1 2 3 4 5 6 7 8 9 10 11
  • C语言实现共享单车管理系统

    C语言实现共享单车管理系统
    1.功能模块图; 2.各个模块详细的功能描述。 1.登陆:登陆分为用户登陆,管理员登陆以及维修员登录,登陆后不同的用户所执行的操作
  • C++继承与菱形继承的介绍

    C++继承与菱形继承的介绍
    继承的概念和定义 继承机制是面向对象程序设计的一种实现代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加
  • C/C++指针介绍与使用介绍

    C/C++指针介绍与使用介绍
    什么是指针 C/C++语言拥有在程序运行时获得变量的地址和操作地址的能力,这种用来操作地址的特殊类型变量被称作指针。 翻译翻译什么
  • C++进程的创建和进程ID标识介绍
    进程的ID 进程的ID,可称为PID。它是进程的唯一标识,类似于我们的身份证号是唯一标识,因为名字可能会和其他人相同,生日可能会与其他
  • C++分析如何用虚析构与纯虚析构处理内存泄漏

    C++分析如何用虚析构与纯虚析构处理内存泄漏
    一、问题引入 使用多态时,如果有一些子类的成员开辟在堆区,那么在父类执行完毕释放后,没有办法去释放子类的内存,这样会导致内存
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计