30.空类型所占字节数

30.面向对象关联知识点-空类型所占字节数


30.1 知识点

知识回顾:C++ 基础中的结构体的内存占用相关知识点

  • 使用 sizeof 可以有效地获取结构体的大小,包括成员变量和填充字节(为了满足特定的字节对齐要求)

  • 什么是字节对齐(内存对齐)

    • 在计算机内存中,数据结构的起始地址需要满足特定的规则,以提高内存访问的效率

    • 字节对齐确保数据在内存中的存储位置符合其类型的对齐要求,从而避免因不对齐访问而导致的性能损失或潜在的硬件异常

    • 每种数据类型通常都有一个对齐要求

      • int 可能要求 4 字节对齐
      • double 可能要求 8 字节对齐
      • char 通常要求 1 字节对齐
    • 为了满足对齐要求,编译器会在结构体成员之间插入填充字节。这些填充字节不会被我们直接访问,但它们确保每个成员的地址都是对齐的

    • 合理的字节对齐可以提高 CPU 访问内存的效率。现代 CPU 通常会在特定的字节边界上进行读取和写入操作,对齐访问通常比非对齐访问快得多

  • 对于结构体大小而言

    • 结构体的字节占用大小通常是其最大成员对齐要求的倍数

    • 编译器可能会在成员之间插入填充字节,以满足对齐要求。这些填充字节通常会放在成员之间或结构体末尾

    • 规则:

      • 每个成员变量的起始地址必须是其类型大小的倍数
      • 整个结构体的大小是最大成员对齐要求的倍数
      • 结构体嵌套时,外部结构体需要考虑内部结构体成员的最大对齐数
  • 内存对齐的目的是提高内存访问性能,但会带来内存浪费的问题

  • 结构体中变量顺序会影响内存占用大小,应按照以下规则排列变量:

    • 按大小降序排列成员,占用空间大的类型放在前面
    • 减少不必要的成员变量
    • 使用更小的数据类型
    • 等等…(其他优化手段以后再学)

类对象的内存占用

  • 类对象的内存占用计算规则与结构体一致
  • 同样可以通过 sizeof 获取其占用的字节数
  • 注意:包含虚函数时,需要加上虚函数表指针的大小

空类型所占字节数

  • 如果一个类型(类或结构体)中不声明任何成员变量,那么它相当于是一个空类型

  • 使用 sizeof 计算其占用内存空间,将会得到 1 个字节

    • 主要原因:确保对象具有唯一地址
    • C++ 中的对象需要内存地址,即使对象没有任何成员变量,它仍然需要一个唯一的内存地址
  • 注意:当空类型被继承时,只要子类中有成员变量,则会优化掉空类型的空间,不会额外增加 1 字节

#pragma once
class X
{
};
#pragma once
#include "X.h"
class Y : public X
{
public:
    int i;
};
#pragma once
class A
{
public:
    virtual void Test3()
    {

    }
    void Test()
    {

    }

    void Test2()
    {

    }
};
#pragma once
#include "A.h"
class B : public A
{
public:
    int i = 0;
};
cout << sizeof(X) << endl;  // 1    X 没有任何成员,仍然需要一个唯一的内存地址
cout << sizeof(Y) << endl;  // 4    Y 有一个 int 成员,占用 4 字节

cout << sizeof(A) << endl;  // 8    A 存在虚函数,默认会有虚函数表指针
cout << sizeof(B) << endl;  // 16   B 有一个 int 成员(4 字节),加上虚函数表指针(8 字节),对齐后取最大对齐数为 16

A a;
A a2;

cout << &a << endl;   // 例如:0000008514B6F918
cout << &a2 << endl;  // 例如:0000008514B6F938
  • 输出示例

    • sizeof(X) 输出 1
    • sizeof(Y) 输出 4
    • sizeof(A) 输出 8
    • sizeof(B) 输出 16
    • 两个空对象的地址不同,证明它们各自占有唯一地址

30.2 知识点代码

X.h

#pragma once
class X
{
};

Y.h

#pragma once
#include "X.h"
class Y :
    public X
{
public:
        int i;
};

A.h

#pragma once
class A
{
public:
    virtual void Test3()
    {

    }
    void Test()
    {

    }

    void Test2()
    {

    }
};

B.h

#pragma once
#include "A.h"
class B :
    public A
{
public:
    int i = 0;
};

Lesson30_面向对象关联知识点_空类型所占字节数.cpp

#include <iostream>
using namespace std;
#include "X.h"
#include "Y.h"
#include "A.h"
#include "B.h"
int main()
{
    #pragma region 知识回顾 C++基础中的结构体的内存占用相关知识点
    //使用 sizeof 可以有效地获取结构体的大小,包括成员变量和填充字节(为了满足特定的字节对齐要求)

    //什么是字节对齐(内存对齐) 
    // 在计算机内存中,数据结构的起始地址需要满足特定的规则
    // 以提高内存访问的效率
    // 字节对齐确保数据在内存中的存储位置符合其类型的对齐要求
    // 从而避免因不对齐访问而导致的性能损失或潜在的硬件异常
    // 
    // 每种数据类型通常都有一个对齐要求,即它在内存中存储时必须位于某个特定的字节边界上
    // 比如:
    //     int可能要求4字节对齐
    //     double可能要求8字节对齐
    //     char通常要求1字节对齐
    // 为了满足对齐要求,编译器会在结构体成员之间插入填充字节。
    // 这些填充字节不会被我们直接访问,但它们确保每个成员的地址都是对齐的
    // 合理的字节对齐可以提高 CPU 访问内存的效率。
    // 现代 CPU 通常会在特定的字节边界上进行读取和写入操作,对齐访问通常比非对齐访问快得多
    // 
    //对于结构体大小而言
    //它的字节占用大小通常是其最大成员对齐要求的倍数
    //编译器可能会在成员之间插入填充字节,以满足对齐要求
    //填充字节通常会放在结构体的成员之间或结构体的末尾
    //规则:
    //1.每个成员变量的起始地址必须是其类型大小的倍数
    //2.整个结构体的大小是最大成员对齐要求的倍数
    //3.结构体嵌套时,外部结构体需要考虑内部结构体成员变量的字节最大数

    //内存对齐的目的是提高内存的访问性能
    //但是会带来浪费内存的问题
    //结构体中变量类型的不同顺序会带来不同的内存占用大小
    //因此我们应该按照以下的规则去排列变量
    //1.按照大小降序排列成员,占用越大空间的变量类型放在前面
    //2.减少不必要的成员变量
    //3.使用更小的数据类型
    //等等(有些手段以后再学)
    #pragma endregion

    #pragma region 知识点一 类对象的内存占用
    //类对象的内存占用计算规则和结构体一致
    //同样可以通过sizeof获取其占用的字节数

    //注意:
    // 包含虚函数时,需要加上虚函数表指针的大小
    #pragma endregion

    #pragma region 知识点二 空类型所占字节数
    //如果一个类型(类或结构体)中不声明任何成员变量
    //那么它相当于是一个空类型
    //这时我们单独用sizeof计算其占用内存空间,将会得到1个字节
    //主要原因:
    //确保对象具有唯一地址
    //	C++中的对象需要内存地址,即使对象没有任何成员变量,它仍然需要一个唯一的内存地址
    //注意:
    //当空类型被继承时,只要子类中有成员变量,则会优化掉空类型的空间,不会加1

    cout << sizeof(X) << endl;//1 X没有任何成员 仍然需要一个唯一的内存地址 1个字节
    cout << sizeof(Y) << endl;//4 Y有一个int成员 4个字节 

    cout << sizeof(A) << endl;//8 A存在虚函数 默认会有虚函数表指针
    cout << sizeof(B) << endl;//16 B有一个int成员4个字节 B虚函数指针8字节因为内存对齐取最大 所以是16
    A a;
    A a2;

    cout << &a << endl;//0000008514B6F918
    cout << &a2 << endl;//0000008514B6F938

    #pragma endregion
}

30.3 练习题

以下两个结构体 A 和 B 在内存占用上是否一样?为什么?

struct Empty {};

struct A {
    Empty e;
    int x;
};

struct B : Empty {
    int x;
};

参考答案: 不相同

原因分析:

  1. Empty 是一个空类型,但 作为成员变量 时,仍然会占用 1 个字节的空间。这是为了确保每个对象都有唯一的地址,防止多个成员变量的地址重叠,违反了 C++ 对象模型的基本原则。

  2. 结构体 A 的内存占用为 8 字节

    • Empty e 占 1 字节;
    • int x 占 4 字节;
    • 为满足内存对齐的要求,整体需要补齐为 8 字节(通常 4 字节对齐);
    • 所以最终 sizeof(A) = 8
  3. 结构体 B 的内存占用为 4 字节

    • Empty 被作为基类继承,由于空基类优化(Empty Base Optimization,EBO),不会占用空间;
    • int x 占 4 字节;
    • 所以 sizeof(B) = 4

结论:
结构体 A 和 B 在内存占用上不一样,A 为 8 字节,B 为 4 字节。
这种差异主要是因为结构体成员需要保留空间,而空基类可以通过编译器优化掉空间。

如下代码中,输出结果是什么?解释你的答案。

class Empty {};

int main() {
    Empty arr[10];
    std::cout << sizeof(arr) << std::endl;
}

参考答案: 输出结果为 10

原因分析:

  1. Empty 是一个空类,但在 C++ 中,一个对象必须拥有唯一的地址。为了满足这个要求,空类型对象的大小被规定为 1 字节。

    • sizeof(Empty) = 1
  2. arr[10] 是一个 Empty 类型的数组,长度为 10。

  3. 因此 sizeof(arr) = 10 * sizeof(Empty) = 10 * 1 = 10

结论:
即使是空类类型数组,也必须分配每个元素独立的地址,最终 sizeof(arr)10。这也是 C++ 对内存模型严谨定义的体现。



转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏