目录

  • 继承
    • 继承的概念
    • 继承方式
    • 基类与派生类的赋值转换
    • 作用域与隐藏
    • 派生类的默认成员函数
    • 友元与静态成员
      • 友元
      • 静态成员
  • 多继承
    • 菱形继承
    • 虚继承
  • 继承和组合
    • 什么是组合
    • 如何选择组合和继承

继承

继承的概念

继承,是面向对象三大特性之一,是可以使代码复用的最重要的手段之一。我们可以在保持原有结构的基础上,在对类的功能进行进一步的拓展,使得创建和维护一个类变得更加的高效和简单
当创建一个类时,我们可以继承一个已有类的成员和方法,并且在原有的基础上进行提升,这个被继承的类叫做基类,而这个继承后新建的类叫做派生类。

继承的方法很简单

class [派生类名] : [继承类型] [基类名]

例如:

class Human
{public:Human(string name = "张三", int age = 18) : _name(name), _age(age){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name;int _age;
};class Student : public Human
{public:Student(string stuNum = "123456") : Human(), _stuNum(stuNum){}void Print(){Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum;
};int main()
{Human h1;Student s1;h1.Print();cout << endl;s1.Print();return 0;
}


这里的派生类Student就复用了Human的方法和成员,并在里面增加了新内容。


继承方式


继承的方式和类的访问限定符一样,分为public(公有继承),private(私有继承), protected(保护继承)三种。
不同的继承方式,在派生类中继承下来的基类成员的访问权限也不一样。

主要特点就是,继承之后的成员访问权限是选取继承方式和原有权限中最私密的那个。(除原私有成员会不可见)

原基类的private成员无论以什么方式继承下来后都是不可见的。不可见并不是没有继承,而是在派生类中被隐藏了,无法访问。


基类与派生类的赋值转换

派生类可以赋值给基类的对象、指针或者引用,这样的赋值也叫做对象切割。
例如上面的Human类和Student类

从这幅图可以看出来,当把派生类赋值给基类时,可以通过切割掉多出来的成员如_stuNum的方式来完成赋值。
但是基类对象如果想赋值给派生类,则不可以,因为他不能凭空多一个_stuNum成员出来。
但是基类的指针却可以强制类型转换赋值给派生类对象, 如:

int main()
{Human h1;Student s1;Human* hPtr1 = &s1;//指向派生类对象Human* hPtr2 = &h1;//指向基类对象Student* pPtr = (Student*)hPtr1;//没问题Student* pPtr = (Student*)hPtr2;//有时候没有问题,但是会存在越界风险return 0;
}
  • 派生类可以赋值给基类的对象、指针或者引用
  • 基类对象不能赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才
    是安全的,否则会存在越界的风险。这里基类如果是多态类型,可以使用RTT的dynamic_cast来
    进行识别后进行安全转换。

作用域与隐藏

基类和派生类都具有他们各自的作用域,那如果出现同名的成员,此时会怎么样呢?这里就要牵扯到一个概念——隐藏

隐藏隐藏,也叫做重定义,当基类和派生类中出现重名的成员时,派生类就会将基类的同名成员给隐藏起来,然后使用自己的。(但是隐藏并不意味着就无法访问,可以通过声明基类作用域来访问到隐藏成员。)

class Human
{public:Human(string name = "张三", int age = 18) : _name(name), _age(age){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name;int _age;
};class Student : public Human
{public:Student(string stuNum = "123456") : Human(), _stuNum(stuNum){}void Print(){Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum;
};

例如这里的Print就构成了隐藏

同时,这里还有个需要注意的问题,在基类与派生类中,同名的方法并不能构成重载,因为处于不同的作用域中。而只要满足方法名相同,就会构成隐藏。


派生类的默认成员函数

在每一个类中,都会有6个默认的成员函数,这些函数即使我们自己不去实现,编译器也会帮我们实现。
之前有写过
类的默认六个成员函数

class Human
{public:Human(){cout << "Human 构造函数" << endl;}~Human(){cout << "Human 析构函数" << endl;}protected:string _name;int _age;
};class Student : public Human
{public:Student(){cout << "Student 构造函数" << _name << endl;}~Student(){cout << "Student 析构函数" << endl;}
protected:string _stuNum;
};int main()
{Student s1;return 0;
}


可以看到,调用派生类的默认成员函数时都会调用基类的默认构造函数。

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函 数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类 对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类析构

这里还有个需要注意的地方,在派生类的析构函数中不需要自己去调用基类的析构函数,编译器会在派生类析构函数结束后自动调用。

 ~Student(){//~Human();//报错, 这里基类的析构函数会被隐藏cout << "Student 析构函数" << endl;}

同时,在派生类中,基类的析构函数会被隐藏,虽然它们这里的名字不同,但是为了实现多态, 它们都会被编译器重命名为destructor。


友元与静态成员

友元

友元关系是不会继承的,可以这样理解,你长辈的朋友并不是你的朋友,之前的关系不会继承。所以基类的友元不能访问子类的私有和保护成员

静态成员

无论继承了多少次,派生了多少子类,静态成员在这整个继承体系中有且只有一个。静态成员不再单独属于某一个类亦或者是某一个对象,而是属于这一整个继承体系


多继承

如果一个子类同时继承两个或以上的父类时,此时就是多继承。

多继承虽然能很好的继承多个父类的特性,达到复用代码的效果,但是他也有着很多的隐患,例如菱形继承的问题,这也就是为什么后期的一些语言如java把多继承去掉的原因。


菱形继承

那么, 什么是菱形继承呢?这里就举一个例子
为了方便观看我就只保留成员变量

class Human
{public:int _age;
};class Student : public Human
{public:int _stuNum;
};class Teacher : public Human
{public:int _teaNum;
};

这里有着人类,学生类,老师类。在学校中,还同时具有老师和学生这两个属性的人,也就是助教,助教可能是本科的老师助理,同时也是研究生或者博士生在读。所以我们可以让他同时继承teacher类和student类

class Assistant : public Teacher, public Student
{};

按照道理来说,各个类的大小应该是这样的。human类4个字节,teacher和student都是8个字节,而assistant是12个字节。
但是实际上assistant却是16字节

这就是菱形继承的数据冗余和二义性问题的体现。
这里的teacher和student都从human中继承了相同的成员_age。但是assistant再从teacher和student继承时,就分别把这两个_age都给继承了过来,导致这里有了两个一样的成员

这就是数据冗余问题。

倘若我们想要给那个_age赋值

因为里面存在两个一样的_age,这是编译器就会报错通知我们指定的不够明确。我们还需要指定作用域

这也就是二义性问题。

int main()
{Human h;Teacher t;Student s;Assistant a;cout << " Human: " << sizeof(h) << " Teacher: " << sizeof(t) << " Student: " << sizeof(s) << " Assistant: " << sizeof(Assistant) << endl;
}

那么怎样才能解决这个问题呢,那就得用到虚继承来解决。


虚继承

解决这个问题很简单,当多个类继承同一个类时,就在继承这个类时,为其添加一个虚拟继承的属性。
如:

class Student : virtual public Human
{public:int _stuNum;
};class Teacher : virtual public Human
{public:int _teaNum;
};


这时就可以看到,它只继承了一次。
接下来看看大小

按照道理来说,他应该是12字节,为什么呢?

这里就牵扯到了C++的对象模型

因为我还没有详细的了解C++的对象模型,只是草草的读了几篇博客和看了一点点的C++对象模型中的其中一小节。所以这里引用一下别的大佬的博客,我再稍微总结一下。
从内存布局看C++虚继承的实现原理

这里多出来的8个字节,其实是两个虚基表指针。
因为这里Human中的_age是teacher和student共有的,所以为了能够方便处理,在内存中分布的时候,就会把这个共有成员_age放到对象组成的最末尾的位置。然后在建立一个虚基表,这个表记录了各个虚继承的类在找到这个共有的元素时,在内存中偏移量的大小,而虚基表指针则指向了各自的偏移量。

这里打个比方:

通过这个偏移量,他们能够找到自己的_age的位置。

为什么需要这个偏移量呢?

int main()
{Assistant a;Teacher t = a; Student s = a;return 0;
}

拿这个举例子,当把对象a赋值给t和s的时候,因为他们互相没有对方的_stuNum和_teaNum,所以他们需要进行对象的切割,但是又因为_age存放在对象的最尾部,所以只有知道了自己的偏移量,才能够成功的在切割了没有的元素时,还能找到自己的_age。

从这里就可以看出来,多继承存在着很多缺点,尤其是菱形继承,他在底层引出了很多复杂的模型和概念,这也是为什么很多面向对象语言都删除了多继承的原因,因为他很难掌控。所以在使用是需要非常谨慎,尽量不要使用多继承,更不要构造出菱形继承。


继承和组合

什么是组合

那除了继承还有什么好的代码复用方式吗?那答案肯定是有的,就是组合
组合是什么呢?组合就是将多个类组合在一起,实现代码复用。

继承和组合又有什么区别呢?这里就举几个例子

1.继承
继承是一种is a的关系,就是基类是一个大类,而派生类则是这个大类中细分出来的一个子类,但是他们本质上其实是一种东西。

如:

class Human
{public:int _age;
};class Student : public Human
{public:int _stuNum;
};

学生也是人,所以他可以很好的继承人的所有属性,并在其实增加学生独有的属性。

2.组合
组合是一种has a的关系,就是一种包含关系,比如对象a是对象b的成员,那么他们的关系就是对象b的组成中包含了对象a,对象a是对象b中的一部分,对象b包含对象a。
例如:

class Study
{public:void ToStudy(){cout << "Study" << endl;}
};class Student : public Human
{public:Study _s;int _stuNum;
};

这里的Student类中包含了一个Study类,学习是学生中非常重要的一部分,并且是不可缺少的一部分,每个学生都需要学习,学习是学生的本职。


如何选择组合和继承

如果综合考虑的话,其实应该多使用组合,因为在组合中,几个类的关联不大,你是我的一部分,所以我也只需要用到你那部分的某个功能,我并不需要了解你的实现和细节,只需要你开放对应的接口即可,并且如果我要修改,只修改那一部分功能即可。所以这就导致了组合的依赖关系弱,耦合度低,十分符合软件工程的低耦合,高类聚。这样保证了代码具有良好的封装性和可维护性。

那么继承呢?继承的依赖关系就非常的强,耦合度非常高。因为你要想在子类中修改和增加某些功能,就必须要了解父类的某些细节,并且有时候甚至会修改到父类,父类的内部细节在子类中也一览无余,严重的破坏了封装性。并且一旦基类发生变化时,牵一发而动全身,所有的派生类都会有影响,这样的代码维护性会非常的差,因为很难在不了解具体细节的情况下能够不影响到子类的实现。但是继承也不是无用的,很多关系都很适合用继承,并且多态的实现也需要用到继承,这个还是得参考具体场景。

但是大部分场景下,如果继承和组合都可以选择,那就优先选择组合。

这个大佬讲的也非常好,可以借鉴学习一下
优先使用对象组合,而不是类继承

C++ 面向对象(一)继承:继承、对象切割、菱形继承、虚继承、继承与组合相关推荐

  1. Python零基础速成班-第9讲-Python面向对象编程(上),对象和类、初始化、继承、重写、多态、类方法、组合

    Python零基础速成班-第9讲-Python面向对象编程(上),对象和类.初始化.继承.重写.多态.类方法.组合 学习目标 修饰器 面向对象编程:对象和类.初始化.继承.重写.多态.类方法.组合 课 ...

  2. C++ 继承 | 对象切割、菱形继承、虚继承、对象组合

    文章目录 继承 继承的概念 继承方式及权限 using改变成员的访问权限 基类与派生类的赋值转换 回避虚函数机制 派生类的默认成员函数 友元与静态成员 多继承 菱形继承 虚继承 组合 继承 继承的概念 ...

  3. 【C++】继承详解,菱形继承问题

    继承体系 概念 继承方式 赋值兼容规则 同名隐藏 子类对象的构造过程 菱形继承 概念 是面向对象程序设计是代码可以复用的最重要的手段,它允许程序员在保持原有的类的特性的基础下进行拓展,增加功能. 这样 ...

  4. 面向对象的三个基本特征是:封装、继承、多态及其五大原则

    阅读数:634 面向对象的三个基本特征是:封装.继承.多态 封装 封装最好理解了.封装是面向对象的特征之一,是对象和类概念的主要特性. 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方 ...

  5. c 语言中的菱形继承,C++中的菱形继承深入分析

    菱形继承 class Person { int _AA; }; class Student:public Person { int _BB; }; class Teacher :public Pers ...

  6. JS高级之ES6类与对象、静态成员、类的继承

    一.类与对象 什么是类? 好比:人类.动物类 就是一个群体的统称 类里描述这一类群体,有哪些特征和行为,所谓的特征对应到代码中就是属性,行为对应到代码中就是方法 类理解为是一套描述数据的模板,但是没有 ...

  7. 再论JavaScript原型继承和对象继承

    JavaScript的原型继承是老生常谈.由于原型即prototype本身也是对象,所以"原型"继承可认为是一种特殊的"对象式"继承."对象式&quo ...

  8. java写一个外网访问的接口_【JAVA基础】一个案例搞懂类、对象、重载、封装、继承、多态、覆盖、抽象和接口概念及区别(中篇)...

    0 前言 初学JAVA时,总会对一些概念一知半解,相互混淆,不明其设计的用意,如类.对象.重载.封装.继承.多态.覆盖.抽象类.接口概念.为便于理解和巩固,本文将基于一个案例及其变形,展现各个概念的定 ...

  9. 面向对象的故事~数据底层操作告诉了我们接口,抽象类,继承与多态性的使用~续(TestBase继承ITest是多余的?)...

    在我上一篇文章发表后,收到了很多博友的回复,其中有一位博友提了一个问题,TestBase 继承了ITest是多余的,我认为,我有必要再写一篇文章来说明一下,TestBase为什么要继承ITest,当然 ...

最新文章

  1. php中htpt,PHP中的HTTP协议
  2. Python 工匠:在边界处思考
  3. python基础知识资料-Python学习--最完整的基础知识大全
  4. js实现旋转木马轮播图
  5. HDU多校4 - 6992 Lawn of the Dead(线段树+模拟)
  6. 嵌入式系统Linux内核开发工程师必须掌握的三十道题
  7. html5 网络断开,html5 – websocket不断断开连接
  8. python 用if判断一个数是不是整数_Python基础教程07-函数和模块的使用
  9. Java对象内存布局
  10. 随想录(vc仿真下的嵌入式开发)
  11. Activiti流程实例管理
  12. UEFI开发探索22 – 环境搭建3(使用UDK2018搭建)
  13. Android 安卓动画 补间动画 - 平移动画
  14. eJOI2019 简要题解
  15. 二进制转十进制python程序_二进制转换(使用Python实现十进制转换器)
  16. 樱花漫地集于我心,蝶舞纷飞祈愿相随 発生:genesis 发生:genesis
  17. 你玩的英雄在比赛中发挥如何呢?
  18. [bzoj1226]学校食堂Dining
  19. 手把手教你制作智能桌宠(小可爱哦!)
  20. 一文搞懂Nginx如何配置Http、Https、WS、WSS!

热门文章

  1. 函数基本使用-函数的定义以及调用执行线路图
  2. oracle字段重复新增错误,Oracle 判断表或字段是否存在新增/修改表结构可重复执行sql...
  3. java i o流异常问题_第四章 Java的I/O流和异常处理
  4. mysql linux-syn25000是什么_当MySQL数据库遇到Syn Flooding
  5. 深入理解 CAP 定理
  6. 下载视频的一种简便方法
  7. 一步一步学习 iOS 6 编程(第四版)正式发布!
  8. 2012.5.2 学习记录:RadGrid单元格操作
  9. D3D API - D3DXCreateRenderToSurface渲染到纹理
  10. 牛客 - Alice and Bob(尺取+二分)