C++私有继承与EBO深入分析讲解

目录
  • 私有继承本质不是继承
  • 空类大小
  • 空基类成员压缩
  • 总结

Hello!大家好呀,近期逗比老师的一个学生问了我这样一个问题:“C++里的私有继承到底有什么意义?”

不知道你有没有跟他一样的困惑。的确,我们在编写C++项目中,几乎是没有用过私有继承(这里包括protected继承和private继承),都是清一色的public继承。有的老师干脆直接告诉学生,你见到继承就是public,其他那俩是历史原因,当它不存在就好了。

这种说法呢,其实也有一定道理,但也不全对。对的部分在于:C++中,确实只有public继承才表示的OOP理论中的“继承”,而私有继承其实对应的是OOP理论中的“组合”关系,所以说“见到继承就写public”这话其实没毛病。然而不对的部分在于:私有继承是为了解决某些性能问题而存在的,我们知道通常表示组合的做法是成员对象,但在某些极端情况下,成员对象会出现一些性能问题,这时我们不得不用私有继承来代替。

私有继承本质不是继承

在此强调,这个标题中,第一个“继承”指的是一种C++语法,也就是class A : B {};这种写法。而第二个“继承”指的是OOP(面向对象编程)的理论,也就是A is a B的抽象关系,类似于“狗”继承自“动物”的这种关系。

所以我们说,私有继承本质是表示组合的,而不是继承关系,要验证这个说法,只需要做一个小实验即可。我们知道最能体现继承关系的应该就是多态了,如果父类指针能够指向子类对象,那么即可实现多态效应。

请看下面的例程:

class Base {};
class A : public Base {};
class B : private Base {};
class C : protected Base {};
void Demo() {
  A a;
  B b;
  C c;
  Base *p = &a; // OK
  p = &b; // ERR
  p = &c; // ERR
}

这里我们给Base类分别编写了A、B、C三个子类,分别是public、private个protected继承。然后用Base *类型的指针去分别指向a、b、c。发现只有public继承的a对象可以用p直接指向,而b和c都会报这样的错:

Cannot cast 'B' to its private base class 'Base'
Cannot cast 'C' to its protected base class 'Base'

也就是说,私有继承是不支持多态的,那么也就印证了,他并不是OOP理论中的“继承关系”,但是,由于私有继承会继承成员变量,也就是可以通过b和c去使用a的成员,那么其实这是一种组合关系。或者,大家可以理解为,把b.a.member改写成了b.A::member而已。

那么私有继承既然是用来表示组合关系的,那我们为什么不直接用成员对象呢?为什么要使用私有继承?这是因为用成员对象在某种情况下是有缺陷的。

空类大小

在解释私有继承的意义之前,我们先来看一个问题,请看下面例程

class T {};
// sizeof(T) = ?

T是一个空类,里面什么都没有,那么这时T的大小是多少?有的同学可能不假思索就会回答0。照理说,空类的大小就是应该是0,但如果真的设置为0的话,会有很严重的副作用,请看例程:

class T {};
void Demo() {
  T arr[10];
  sizeof(arr); // 0
  T *p = arr + 5;
  // 此时p==arr
  p++; // ++其实无效
}

发现了吗?假如T的大小是0,那么T指针的偏移量就永远是0,T类型的数组大小也将是0,而如果它成为了一个成员的话,问题会更严重:

struct Test {
  T t;
  int a;
};
// t和a首地址相同

由于T是0大小,那么此时Test结构体中,t和a就会在同一首地址。

所以,为了避免这种0长的问题,编译器会针对于空类自动补一个字节的大小,也就是说其实sizeof(T)是1,而不是0。

这里需要注意的是,不仅是绝对的空类会有这样的问题,只要是不含有非静态成员变量的类都有同样的问题,例如下面例程中的几个类都可以认为是空类:

class A {};
class B {
  static int m1;
  static int f();
};
class C {
public:
  C();
  ~C();
  void f1();
  double f2(int arg) const;
};

有了自动补1字节,T的长度变成了1,那么T*的偏移量也会变成1,就不会出现0长的问题。但是,这么做就会引入另一个问题,请看例程:

class Empty {};
class Test {
  Empty m1;
  long m2;
};
// sizeof(Test)==16

由于Empty是空类,编译器补了1字节,所以此时m1是1字节,而m2是8字节,m1之后要进行字节对齐,因此Test变成了16字节。如果Test中出现了很多空类成员,这种问题就会被继续放大。

这就是用成员对象来表示组合关系时,可能会出现的问题,而私有继承就是为了解决这个问题的。

空基类成员压缩

(EBO,Empty Base Class Optimization)

在上一节最后的历程中,为了让m1不再占用空间,但又能让Test中继承Empty类的其他内容(例如函数、类型重定义等),我们考虑将其改为继承来实现,EBO就是说,当父类为空类的时候,子类中不会再去分配父类的空间,也就是说这种情况下编译器不会再去补那1字节了,节省了空间。

但如果使用public继承会怎么样?

class Empty {};
class Test : public Empty {
  long m2;
};
// 假如这里有一个函数让传Empty类对象
void f(const Empty &obj) {}
// 那么下面的调用将会合法
void Demo() {
  Test t;
  f(t); // OK
}

Test由于是Empty的子类,所以会触发多态性,t会当做Empty类型传入f中。这显然问题很大呀!如果用这个例子看不出问题的话,我们换一个例子:

class Alloc {
public:
  void *Create();
  void Destroy();
};
class Vector : public Alloc {
};
// 这个函数用来创建buffer
void CreateBuffer(const Alloc &alloc) {
  void *buffer = alloc.Create(); // 调用分配器的Create方法创建空间
}
void Demo() {
  Vector ve; // 这是一个容器
  CreateBuffer(ve); // 语法上是可以通过的,但是显然不合理
}

内存分配器往往就是个空类,因为它只提供一些方法,不提供具体成员。Vector是一个容器,如果这里用public继承,那么容器将成为分配器的一种,然后调用CreateBuffer的时候可以传一个容器进去,这显然很不合理呀!

那么此时,用私有继承就可以完美解决这个问题了

class Alloc {
public:
  void *Create();
  void Destroy();
};
class Vector : private Alloc {
private:
  void *buffer;
  size_t size;
  // ...
};
// 这个函数用来创建buffer
void CreateBuffer(const Alloc &alloc) {
  void *buffer = alloc.Create(); // 调用分配器的Create方法创建空间
}
void Demo() {
  Vector ve; // 这是一个容器
  CreateBuffer(ve); // ERR,会报错,私有继承关系不可触发多态
}

此时,由于私有继承不可触发多态,那么Vector就并不是Alloc的一种,也就是说,从OOP理论上来说,他们并不是继承关系。而由于有了私有继承,在Vector中可以调用Alloc里的方法以及类型重命名,所以这其实是一种组合关系。

而又因为EBO,所以也不用担心Alloc占用Vector的成员空间的问题。

总结

总结下来,私有继承其实是表示组合关系的,它是当组合类为空类时,为了增强性能而提供的一种成员对象的代替方案。

好啦!相信大家已经明白私有继承的存在意义了,这里建议大家阅读一下STL源码,会看到绝大多数容器和分配器之间都是使用私有继承方式的。如果还有什么疑问欢迎评论区抛出!

到此这篇关于C++私有继承与EBO深入分析讲解的文章就介绍到这了,更多相关C++私有继承 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解C++编程中的私有继承和公有继承

    C++类的私有继承 在声明一个派生类时将基类的继承方式指定为private的,称为私有继承,用私有继承方式建立的派生类称为私有派生类(private derived class ), 其基类称为私有基类(private base class ). 私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,而在派生类外不能访问它们.私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们.一个基类成员在基类中的访问属性和在派生类中

  • C++私有继承(一)

    目录 1.私有继承介绍 2.语法 3.类声明代码 文章转自微信公众号:Coder梁(ID:Coder_LT) 1.私有继承介绍 我们可以将一个类作为另外一个类的成员,这样可以描述has-a的关系,即包含关系. 例如我们要创建一个Student类,它当中要包含一个string类型的name,和valarray类型的scores.这里的valarray是C++中的一个模板类,它可以理解成一个泛型的数组,有些类似于vector和array,但提供的功能更多.比如拥有min, size, max, su

  • C++私有继承(二)

    目录 1.访问基类方法 2.访问基类对象 3.访问基类的友元 文章转自微信 公众号:Coder梁(ID:Coder_LT) 我们继续上一篇文章来看私有继承. 1.访问基类方法 我们知道,在私有继承时,基类的公有对象以及保护对象会变成派生类的私有对象.我们可以在派生类方法当中使用它,但无法通过派生类对象直接调用,但无法访问基类的私有方法和对象. 这个概念我们很好理解,但具体到实现上,我们如何在派生类的方法当中调用基类的公有或者保护方法呢? 比如,在之前的类声明当中我们声明了一个Average方法,

  • C/C++ 公有继承、保护继承和私有继承的对比详解

    C/C++ 公有继承.保护继承和私有继承的区别 在c++的继承控制中,有三种不同的控制权限,分别是public.protected和private.定义派生类时,若不显示加上这三个关键字,就会使用默认的方式,用struct定义的类是默认public继承,class定义的类是默认private继承.这和Java有很大的不同,Java默认使用public继承,而且只有公有继承. 1.使用public继承时,派生类内部可以访问基类中public和protected成员,但是类外只能通过派生类的对象访问

  • C++ 中私有继承的作用

    C++ 中私有继承的作用 私有继承的 第一个规则:和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象转换成基类对象. 第二个规则: 从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员. 私有继承的含义:私有继承意味着 "用...来实现". 如果使类D私有继承于类B,这样做是因为你想利用类B中已经存在的某些代码,而不是因为类型B的对象和类型D的对象之间有什么概念上的关系. 因而,私有继承纯粹是一种实现技术. 私有继承意味着只是继承实

  • C++私有继承(三)

    目录 1.私有继承 2.保护继承 3.使用using重新定义访问权限 文章转自公众号:Coder梁(ID:Coder_LT) C++私有继承(一) C++私有继承(二) 1.私有继承 通过私有继承,我们可以实现一种has-a的关系. 但前文当中我们也曾说过,通过包含对象我们一样可以实现has-a的关系,那么在我们进行编码的时候,究竟应该使用哪一种呢? 根据C++ Primer中的阐述,大多数程序员会更倾向于使用包含,因为这更加容易理解.类声明当中包含对象,我们可以在成员函数当中直接使用,这显然更

  • C++私有继承与EBO深入分析讲解

    目录 私有继承本质不是继承 空类大小 空基类成员压缩 总结 Hello!大家好呀,近期逗比老师的一个学生问了我这样一个问题:“C++里的私有继承到底有什么意义?” 不知道你有没有跟他一样的困惑.的确,我们在编写C++项目中,几乎是没有用过私有继承(这里包括protected继承和private继承),都是清一色的public继承.有的老师干脆直接告诉学生,你见到继承就是public,其他那俩是历史原因,当它不存在就好了. 这种说法呢,其实也有一定道理,但也不全对.对的部分在于:C++中,确实只有

  • Python中关于面向对象私有属性方法的详细讲解

    目录 1.私有属性和私有方法 私有属性: 私有方法: 类部调用私有属性和私有方法 子类不能继承父类私有属性和方法 实例属性(对象属性) 通过实例(对象)去修改类属性 2.修改私有属性的值 3.类属性和实例属性 4.类方法和静态方法 类方法: 静态方法: 实例方法 5.__new__方法 6.单例模式 福利 1.私有属性和私有方法 封装的意义: 将属性和方法放到一起做为一个整体,然后通过实例化对象来处理: 隐藏内部实现细节,只需要和对象及其属性和方法交互就可以了: 对类的属性和方法增加 访问权限控

  • C++深入分析讲解类的知识点

    目录 知识点引入 类的初识 1.封装 2.权限 3.类的定义(定义类型) 4.类的成员函数与类中声明及类外定义 Person类的设计 设计立方体类 点Point和圆Circle的关系 知识点引入 C语言中 数据 和 方法 是独立: //c语言的思想:数据 方法 分开 //人 typedef struct { char name[32]; int age; }Person; //动物 typedef struct { char name[32]; int age; int type; }Dog;

  • Java深入分析讲解反射机制

    目录 反射的概述 获取Class对象的三种方式 通过反射机制获取类的属性 通过反射机制访问Java对象的属性 反射机制与属性配置文件的配合使用 资源绑定器 配合使用样例 通过反射机制获取类中方法 通过反射机制调用Java对象的方法 通过反射机制获取类中的构造方法 通过反射机制创建对象(调用构造方法) 通过反射机制获取一个类的父类和父接口 反射的概述 JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的

  • SpringBoot深入分析讲解监听器模式上

    目录 1.事件ApplicationEvent 2.监听器ApplicationListener 3.事件广播器ApplicationEventMulticaster 注:图片来源于网络 SpringBoot作为业内公认的优秀开源框架,它的监听器是如何实现呢?在这里首先对一些基础组件进行分析: 1.事件ApplicationEvent ApplicationEvent是一个抽象类,idea上展开其继承关系如图: 可见SpringBoot所定义的事件类型是极为丰富的. 2.监听器Applicati

  • React Hooks核心原理深入分析讲解

    目录 Hooks 闭包 开始动手实现 将useState应用到组件中 过期闭包 模块模式 实现useEffect 支持多个Hooks Custom Hooks 重新理解Hooks规则 React Hooks已经推出一段时间,大家应该比较熟悉,或者多多少少在项目中用过.写这篇文章简单分析一下Hooks的原理,并带大家实现一个简易版的Hooks. 这篇写的比较细,相关的知识点都会解释,给大家刷新一下记忆. Hooks Hooks是React 16.8推出的新功能.以这种更简单的方式进行逻辑复用.之前

  • RocketMq深入分析讲解两种削峰方式

    目录 何时需要削峰 通过消息队列的削峰方法有两种 消费延时控流 总结 何时需要削峰 当上游调用下游服务速率高于下游服务接口QPS时,那么如果不对调用速率进行控制,那么会发生很多失败请求 通过消息队列的削峰方法有两种 控制消费者消费速率和生产者投放延时消息,本质都是控制消费速度 通过消费者参数控制消费速度 先分析那些参数对控制消费速度有作用 1.PullInterval: 设置消费端,拉取mq消息的间隔时间. 注意:该时间算起时间是rocketMq消费者从broker消息后算起.经过PullInt

随机推荐