深入C++核心:空类背后隐藏的内存开销,你知道吗?

开发
本文让我们一起揭开C++空类的神秘面纱,探索这个看似简单却暗藏玄机的话题。

你有没有想过,一个空的购物袋到底有多重?🛍️

即使是空的,它也占据着一定的空间,对吧?在C++的世界里,我们也有类似的情况 - 空类(Empty Class)。看起来什么都没有,但它真的一点内存都不占吗?

让我们一起揭开C++空类的神秘面纱,探索这个看似简单却暗藏玄机的话题。相信我,这个旅程会很有趣!

最简单的空类

想象一下,你正在收拾房间,把所有东西都清空了。那么问题来了:一个空房间还占地方吗?

在C++中,我们也有类似的问题:一个空空如也的类会占用内存空间吗?让我们一起来探索这个有趣的谜题!

来看看最简单的空类:

class Empty {};

// 测试一下它的大小
cout << sizeof(Empty) << endl;  // 惊喜:输出是1!

咦?明明什么都没有,为什么还要占用1个字节呢?

这就像是你在小区里买房子 - 即使是空房子,也需要有门牌号,对吧!在C++中,每个对象都需要有自己的内存地址(相当于门牌号),所以编译器会给每个空类安排至少1个字节的空间。

为什么需要这一个字节?

这个1字节的存在有着非常重要的意义!它就像是每个对象的身份证:

Empty e1, e2;   // 创建两个空类对象

这1字节的主要作用是:

  • 确保每个对象都有唯一的内存地址
  • 使得对象可以被定位和区分
  • 满足C++标准要求的对象必须具有非零大小的规定

等等,这里可能有个疑问:既然是内存地址,为什么不是8字节(64位系统)或4字节(32位系统)呢?🤔

这里需要区分两个概念:

  • 对象本身占用的内存大小
  • 指向对象的指针的大小

让我们用代码来说明:

Empty e;              // 对象本身占 1 字节
Empty* ptr = &e;      // 指针占 8 字节(在64位系统上)

cout << sizeof(e) << endl;    // 输出:1
cout << sizeof(ptr) << endl;  // 输出:8

就像门牌号和GPS坐标的关系:

  • 门牌号(对象本身)只需要很小的空间就能标识这个位置
  • GPS坐标(指针)需要更多的数字来精确定位

编译器只需要1个字节就能区分不同的对象,而指向这些对象的指针则需要更大的空间来存储完整的内存地址。这是一个很巧妙的设计!

为什么对象必须有不同的地址呢?这关系到C++的一个基本原则:

Empty* ptr1 = &e1;
Empty* ptr2 = &e2;
assert(ptr1 != ptr2);  // 这个断言永远为真,因为每个对象必须有唯一地址

就像在一个小区里,即使是完全相同的两套房子,也必须有不同的门牌号。这个1字节就相当于对象的"门牌号",让系统能够准确找到并区分每一个对象!

虚拟继承大冒险

嘿!让我们来玩个有趣的游戏 - 建造属于我们的动物王国!

首先,我们需要一个动物祖先:

class Animal {};  // 🐾 万物之源!

然后,让我们召唤一些可爱的小动物:

class Cat : public virtual Animal {};  // 🐱 喵星来客
class Dog : public virtual Animal {};  // 🐕 汪星使者

咦?这些小可爱的"体重"是多少呢?让我们偷偷称一下:

cout << sizeof(Cat) << endl;   // 8字节!🎯

哇塞!一个空空的猫咪居然有8字节这么重!这是为啥呢?

原来啊,每个虚拟继承的小动物都带着一个神奇的指南针:

  • 这个指南针帮它们找到Animal祖先
  • 就像GPS定位器一样不能丢
  • 在64位系统上,这个指南针要占8个字节呢!

来看个更有意思的 - 动物园时间!

class Zoo {
    Cat kitty;    // 🐱 一只优雅的喵
    Dog puppy;    // 🐕 一只活泼的汪
};

猜猜动物园有多大?

cout << sizeof(Zoo) << endl;  // 16字节!🎯

哈!16字节 = 8(喵) + 8(汪) 🧮 就像两个小朋友各自背着自己的小书包!

小提示:

  • 虚拟继承虽然酷炫,但也要付出代价哦!
  • 如果你的程序想要"减肥",可要慎用这个功能!

看,C++也可以这么可爱对吧?记住:每个设计都像选择玩具一样,要想清楚它的代价哦!

多重继承的奇妙冒险

哈喽!今天我们要创造一个超级神奇的生物 - 既是猫又是狗的小可爱!

class CatDog : public Cat, public Dog {};  // 喵星汪星合体! 🐱🐕

猜猜这个小家伙有多重? 

cout << sizeof(CatDog) << endl;  // 哇塞!8字节耶! ⚖️

为啥是8字节呢?让我们来解剖一下这个有趣的现象:

(1) 首先,Cat类带着8字节:

  • 其中包含了指向Animal虚表的指针(在64位系统上是8字节)

(2) Dog类也带着8字节:

  • 同样包含指向Animal虚表的指针

(3) 但是这里有个巧妙的地方:

  • Cat和Dog都是虚继承自Animal
  • 它们共享同一个Animal基类实例
  • 在内存布局中,只需要保存一份Animal的虚表指针
  • 这就是为什么要减去4字节(32位系统)或8字节(64位系统)

(4) 所以最终的计算公式是:

  • 在32位系统:4 + 4 - 4 = 4字节
  • 在64位系统:8 + 8 - 8 = 8字节

就像两个小朋友(Cat和Dog)共用一本相册(Animal的信息)一样,没必要每人都带一本相同的!这就是C++编译器的智慧! 

小彩蛋:有些聪明的编译器会给它们做个"瘦身" 

  • 通过优化内存对齐和布局
  • 可能会得到更小的实际大小
  • 这种优化被称为"Empty Base Optimization"(EBO)

记住:不同编译器就像不同的魔法师,各有各的独门绝技!但只要理解了原理,你就能成为C++世界的小达人啦! 

更复杂的继承场景

先来个小提示:还记得虚继承是做什么的吗?

// 没有虚继承时的菱形继承问题
class Animal {};
class Bird :public Animal {};
class Fish :public Animal {};
class Flying_Fish :public Bird, public Fish {};  // 两份Animal!

// 使用虚继承解决
class Bird :virtualpublic Animal {};  // 只保留一份Animal
class Fish :virtualpublic Animal {};  // 但需要额外的内存开销

虚继承就像是给类们安排了一个共享的基类空间,解决了重复继承的问题。不过这个便利是要付出内存代价的!

让我们来玩个更刺激的游戏 - 多层继承大冒险!

先来个基础款超能力:

class SuperPower {
    virtual void activate() {}  // 激活超能力!✨
};

再来个进阶版超能力:

class MegaPower : public virtual SuperPower {
    virtual void powerUp() {}  // 能量加倍!💪
};

哎呀!这时候事情变得有趣了:

cout << sizeof(SuperPower) << endl;  // 8字节 - 因为虚函数表指针 🎯
cout << sizeof(MegaPower) << endl;   // 16字节 - 双倍的快乐!🎉

为啥是16字节呢?让我们拆开看看这个"超能力包裹":

  • 8字节用来存虚函数表指针
  • 8字节用来存虚基类指针
  • 就像双层汉堡一样,每层都很重要!

(1) 虚函数表指针(vptr,8字节)

  • 这是一个指向虚函数表(vtable)的指针
  • 虚函数表存储了所有虚函数的地址
  • 用于实现动态多态,使得程序能在运行时决定调用哪个版本的虚函数
  • 就像一本"说明书",告诉对象:"你的超能力们都藏在哪里"

(2) 虚基类指针(vbptr,8字节)

  • 这个指针指向虚基类表(virtual base table)
  • 用于在运行时定位虚基类的位置
  • 解决菱形继承问题,确保虚基类只有一个实例
  • 就像一个"导航仪",帮助对象找到它的祖先类

来个形象的比喻:

class MegaPower : public virtual SuperPower {
    virtual void powerUp() {}
};
  • 虚函数表指针就像游戏手柄,控制着不同的技能按键(虚函数)
  • 虚基类指针则像是族谱导航,帮助找到共同的祖先(虚基类)

来个更疯狂的 - 超级英雄时间!

class SuperHero : public virtual MegaPower {
    virtual void fly() {}  // 飞天遁地!✈️
};

猜猜看这位英雄的"体重"?

cout << sizeof(SuperHero) << endl;  // 16字节 - 咦,没变重诶!🤔

为什么没变重?因为:

  • 虚继承只需要一个虚表指针
  • 所有的虚函数共享同一个虚表
  • 这就是C++的魔法!

小贴士:记住这个公式

  • 普通类 + 虚函数 = 8字节(64位系统)
  • 加上虚继承 = 16字节
  • 再多继承也不会更重啦!(除非加了新成员)

就像叠叠乐游戏,有技巧才不会倒!

优化技巧与注意事项 

来看看如何给我们的类"减肥"吧! 

(1) 巧用空基类优化 (EBO) 

// 不好的写法 - 浪费内存
class MyClass {
    EmptyBase base;  // 占1字节
    int data;        // 占4字节,但可能因对齐变成8字节
};

// 聪明的写法 - 节省空间
class MyClass : private EmptyBase {
    int data;  // 只占4字节,EmptyBase不占额外空间
};

这就像是把空书包直接背在身上,而不是放在行李箱里! 📚

(2) 合理使用虚继承

// 需要虚继承时才用它
class Bird : virtual public Animal {};  // 🦅

// 普通情况用普通继承就好
class Cat : public Animal {};  // 🐱 省内存!

(3) 对齐小魔法

class SmartClass {
    char flag;     // 1字节
    int data;      // 4字节
    char status;   // 1字节
};  // 实际占12字节,因为对齐!

// 优化后:
class SmartClass {
    int data;      // 4字节
    char flag;     // 1字节
    char status;   // 1字节
};  // 现在只占8字节啦! 

把小件物品巧妙放置,就像俄罗斯方块一样! 

总结要点

让我们来个欢乐总结吧! 

(1) 空类的秘密

  • 空类占1字节 - 就像空房子也要有门牌号! 
  • 指针永远是固定大小(32位4字节/64位8字节) 

(2) 继承的趣事

class Empty {};           // 1字节
class Virtual {          // 8字节
    virtual void foo();  // 因为虚函数表指针
};

(3) 内存对齐小贴士

  • 就像叠积木,要整整齐齐! 
  • 合理布局可以省下不少空间 

(4) 实用建议

  • 不需要虚函数就别用 
  • 善用EBO来节省空间 
  • 把相同大小的成员放一起 

记住:优化很重要,但代码可读性更重要! 平衡最美! 

这就是C++的空类世界啦! 虽然看起来空空的,但学问可不少呢! 🌟

责任编辑:赵宁宁 来源: everystep
相关推荐

2022-10-11 08:14:14

多线程锁机制

2025-01-26 16:01:13

C++静态成员函数

2023-12-12 08:41:01

2021-09-13 19:28:42

JavaNetty开发

2022-09-29 15:32:58

云计算计算模式

2021-10-14 06:52:47

算法校验码结构

2024-09-18 07:00:00

消息队列中间件消息队列

2024-09-12 08:20:39

2022-02-25 08:13:03

物联网IOT

2024-04-07 00:00:00

ESlint命令变量

2024-05-28 09:12:10

2021-09-19 22:51:49

iPhone手机iOS

2020-05-15 14:34:16

C语言丹尼斯 · 里奇开发者

2022-12-06 10:04:59

5G网络C波段

2022-03-10 08:25:27

JavaScrip变量作用域

2019-12-12 09:23:29

Hello World操作系统函数库

2017-10-13 11:13:16

微信隐藏功能视频编辑

2024-11-01 10:48:01

C#WPF程序

2021-10-28 16:19:37

物联网人工智能IoT

2014-05-30 10:23:15

乐跑手环智能手环运动手环
点赞
收藏

51CTO技术栈公众号