C++ 内存布局与字节序详解:类大小、结构体对齐、大小端与字节序转换

ops/2024/11/29 0:14:24/

文章目录

  • 一、如何计算一个类的大小?
  • 二、sizeof 计算的几个关键因素:
    • 2.1 特殊情况(空类、静态成员变量):
    • 2.2 示例
  • 三、结构体对齐
    • 3.1 alignas 关键字:
    • 3.2 C++ 结构体对齐举例
    • 3.3 offsetof 宏
      • 3.3.1 如何理解偏移量?
  • 四、一些问题
    • 4.1 什么是内存对齐,结构体是如何进行内存对齐的?
    • 4.2 如何让结构体按照指定的默认对齐数进行对齐?
    • 4.3 如何知道结构体某个成员相对于起始位置的偏移量
    • 4.4 什么是大小端?如何测试一个机器是大端还是小端?有没有什么需要考虑大小端的场景
      • 4.4.1 什么是大小端(Endianness)?
      • 4.4.2 如何测试一个机器是大端还是小端?
      • 4.4.3 需要考虑大小端的场景
      • 4.4.4 如何进行字节序转换?


无意间刷到一些关于类大小的内容,正好写一篇文章作为另一篇的跳转,下面我们直接进入正题:

一、如何计算一个类的大小?

在 C++ 中,计算一个类的大小通常是通过 sizeof 运算符来完成的。sizeof 运算符可以用来计算一个类、结构体、数组或基本数据类型的内存占用大小。

如果要计算一个类的大小,需要考虑下面的因素:

  1. 类的成员变量

    • 类的大小由它的成员变量、继承的基类、以及内存对齐方式共同决定。成员变量包括基本类型、指针、对象、数组等。
  2. 内存对齐(Padding)

    • 由于大多数系统的内存访问效率较高时需要遵守对齐规则,因此类的大小可能会比成员变量的总和大一些。
    • 编译器通常会对类的成员进行内存对齐,使得每个成员变量的地址都满足特定的对齐要求。为了实现对齐,编译器可能会在成员变量之间插入空白(填充字节),即 “padding”。
  3. 虚函数

    • 如果类有虚函数,那么类通常会有一个虚函数表(vtable),这会占用额外的空间。
  4. 继承

    • 如果类继承了其他类(基类),则继承的成员也会计入该类的大小。

一般我们可以通过sizeof 直接计算类的大小,比如:

cout << sizeof(MyClass) << endl;

这会返回类 MyClass 在内存中的大小,单位是字节。


二、sizeof 计算的几个关键因素:

  • 内存对齐:编译器通常会在成员之间插入填充字节,以使每个成员按照其类型的对齐要求进行对齐。一般来说,int 的对齐要求是 4 字节,double 的对齐要求是 8 字节,char 对齐要求是 1 字节。

  • 虚函数:如果类中有虚函数,类会有一个虚函数表指针(vptr),这会占用额外的空间。在 64 位系统中,这通常是 8 字节。

  • 继承:如果类继承了其他类,继承的成员会增加类的大小。例如,子类继承了父类的成员变量和虚函数表指针。

2.1 特殊情况(空类、静态成员变量):

  • 空类:对于没有成员的空类,sizeof 计算结果通常为 1。原因是即使没有成员,C++ 也要求每个对象在内存中有一个唯一的地址。因此,空类的大小是 1 字节。

  • 类的静态成员变量:静态成员变量属于类本身,而不是类的每个实例。因此,静态成员变量的大小不计算在每个实例的大小中。只有实例化对象时,类的非静态成员变量的大小才会计算。


2.2 示例

#include <iostream>
using namespace std;// 类 A:包含基本成员变量
class A {
public:int x;     // 4 字节char y;    // 1 字节
};class B {
public:int x;     // 4 字节char y;    // 1 字节double z;  // 8 字节
};class C {
public:virtual void foo() {} // 有虚函数int x;    // 4 字节double y; // 8 字节
};class D {
public:static int staticVar; // 静态成员变量int x;                // 4 字节char y;               // 1 字节
};// 基类 E,含有一个虚函数
class E {
public:virtual void bar() {}int x;
};// 子类 F,继承自基类 E
class F : public E {
public:double y;char z;
};int main() {cout << "Size of A: " << sizeof(A) << " bytes" << endl;  // 计算类 A 的大小cout << "Size of B: " << sizeof(B) << " bytes" << endl;  // 计算类 B 的大小cout << "Size of C: " << sizeof(C) << " bytes" << endl;  // 计算类 C 的大小cout << "Size of D: " << sizeof(D) << " bytes" << endl;  // 计算类 D 的大小cout << "Size of E: " << sizeof(E) << " bytes" << endl;  // 计算基类 E 的大小cout << "Size of F: " << sizeof(F) << " bytes" << endl;  // 计算子类 F 的大小return 0;
}int D::staticVar = 10; // 静态成员变量的定义

代码解析和类大小的计算:

  1. 类 A

    class A {
    public:int x;     // 4 字节char y;    // 1 字节
    };
    
    • xint 类型,通常占 4 字节。
    • ychar 类型,占 1 字节。
    • 由于内存对齐规则,x 需要在 4 字节边界上对齐,所以 y 后面会有 3 字节的填充字节。最终,类 A 的大小为 8 字节
  2. 类 B

    class B {
    public:int x;     // 4 字节char y;    // 1 字节double z;  // 8 字节
    };
    
    • xint 类型,占 4 字节。
    • ychar 类型,占 1 字节。
    • zdouble 类型,占 8 字节。
    • y 后面会有 3 字节的填充,以满足 double 类型的 8 字节对齐要求。所以,类 B 的大小为 16 字节(4 + 1 + 3 填充 + 8 字节)。
  3. 类 C(有虚函数):

    class C {
    public:virtual void foo() {} // 有虚函数int x;    // 4 字节double y; // 8 字节
    };
    
    • C 中有一个虚函数,因此会为类对象添加一个虚函数表指针(vptr),通常占 8 字节(在 64 位系统上)。
    • x 占 4 字节,y 占 8 字节。
    • C 的总大小将是 24 字节(8 字节虚函数表指针 + 4 字节 x + 8 字节 y)。
  4. 类 D(含有静态成员变量):

    class D {
    public:static int staticVar; // 静态成员变量int x;                // 4 字节char y;               // 1 字节
    };
    
    • 静态成员变量 staticVar 并不属于类的每个实例,它是在类加载时分配内存,而不是为每个对象单独分配内存,因此它不会影响类对象的大小。
    • 只有 x(4 字节)和 y(1 字节),以及填充字节(3 字节),所以类 D 的大小为 8 字节
  5. 类 E(基类):

    class E {
    public:virtual void bar() {}int x;  // 4 字节
    };
    
    • E 含有一个虚函数,所以它也有一个虚函数表指针(vptr),通常占 8 字节。
    • x 占 4 字节。
    • E 的总大小为 12 字节(8 字节虚函数表指针 + 4 字节 x)。
  6. 类 F(继承自基类 E):

    class F : public E {
    public:double y; // 8 字节char z;   // 1 字节
    };
    
    • F 继承了类 E 的虚函数表指针(8 字节)和成员变量 x(4 字节)。
    • ydouble 类型,占 8 字节。
    • zchar 类型,占 1 字节。
    • 为了满足对齐要求,z 后面会有 7 字节的填充,以保证类的大小是 8 字节对齐的。
    • F 的总大小为 24 字节(8 字节虚函数表指针 + 4 字节 x + 8 字节 y + 1 字节 z + 7 字节填充)。

输出示例:

Size of A: 8 bytes
Size of B: 16 bytes
Size of C: 24 bytes
Size of D: 8 bytes
Size of E: 12 bytes
Size of F: 24 bytes

总结
在计算类的大小时,主要受到以下因素的影响:

  1. 成员变量的类型和对齐要求:编译器会根据成员的类型插入必要的填充字节,以确保成员按其对齐要求正确对齐。
  2. 虚函数:如果类中有虚函数,那么类的大小会包括虚函数表指针(vptr),通常占用 8 字节(在 64 位系统中)。
  3. 静态成员变量:静态成员变量不占用对象内存,因此它们不影响对象的大小。
  4. 继承:子类继承父类时,除了父类的成员和虚函数表指针外,还会增加自己独立的成员。

三、结构体对齐

关于类与结构体的对齐规则可以看下面的文章:

结构体的内存对齐(规则、存在原因、默认对齐数的修改等+实例分析)

上面的文章是关于C语言的struct的结构体对齐,而C++类与struct的结构体对齐规则与C语言是一致的,需要注意的就是C++的一些新内容、新特性:


3.1 alignas 关键字:

C++11 引入了 alignas 关键字,可以明确指定类或结构体的对齐要求。这样可以影响类或结构体的对齐方式。

示例:

struct A {char a;     // 1 字节int b;      // 4 字节
};alignas(16) struct B {char a;     // 1 字节int b;      // 4 字节
};

通过 alignas(16),结构体 B 的起始地址会对齐到 16 字节边界,从而可能导致更多的填充字节。


3.2 C++ 结构体对齐举例

对于下面代码的四个类,我们进行画图分析:

#include <iostream>
using namespace std;class Base {
public:virtual void func() {}  // 虚函数int baseData;           // 基类数据成员
};class Derived : public Base {
public:double derivedData;     // 派生类数据成员
};class VirtualBase {
public:virtual void vfunc() {} // 虚函数char vBaseData;         // 虚基类数据成员
};class MultipleInheritance : public Derived, public VirtualBase {
public:float multipleData;     // 多重继承类的数据成员
};int main() {cout << "Size of Base: " << sizeof(Base) << endl;cout << "Size of Derived: " << sizeof(Derived) << endl;cout << "Size of VirtualBase: " << sizeof(VirtualBase) << endl;cout << "Size of MultipleInheritance: " << sizeof(MultipleInheritance) << endl;return 0;
}

我们通过一个示例图分析class Base 结构体对齐后的大小,

  1. Base 类

有一个虚函数 func() 和一个 int baseData。由于虚函数,编译器会为它添加一个虚函数表指针 vptr。假设:

  • vptr 需要 8 字节。
  • int baseData 占用 4 字节。

由于 int 通常要求 4 字节对齐,Base 类的内存布局可能是:

  • 8 字节的 vptr,紧接着 4 字节的 int baseData,因此 Base 的总大小为 16 字节。
    在这里插入图片描述

其他的以此,我们分别进行分析:


  1. Derived

Derived 类继承自 Base,它有一个额外的 double derivedData。由于 double 通常要求 8 字节对齐,因此 Derived 类的布局会受到影响:

  • Base 部分(16 字节)已经包含虚函数表指针和 baseData
  • double derivedData 需要 8 字节,并且它的对齐要求是 8 字节。

因此,Derived 类的总大小是:

  • 16 字节(来自 Base 类的部分) + 8 字节(double derivedData)= 24 字节

  1. VirtualBase

VirtualBase 类有一个虚函数 vfunc() 和一个 char vBaseData。虚继承会引入虚基类指针。假设:

  • vptr 需要 8 字节(虚函数表指针)。
  • char vBaseData 占用 1 字节。

由于虚继承,VirtualBase 可能还需要填充字节来满足对齐要求,通常会填充 7 字节。

因此,VirtualBase 类的总大小为:

  • 8 字节(vptr) + 1 字节(vBaseData) + 7 字节填充 = 16 字节

  1. MultipleInheritance
    MultipleInheritance 类同时继承自 DerivedVirtualBase,因此它会包含两个虚函数表指针(一个来自 Base,一个来自 VirtualBase),以及来自这两个父类的数据成员。
  • Derived 类的部分:包含一个 8 字节的虚函数表指针和 double derivedData(8 字节),总计 24 字节。
  • VirtualBase 类的部分:包含一个 8 字节的虚函数表指针和 char vBaseData(1 字节),总计 16 字节。
  • MultipleInheritance 类自身的数据成员 multipleDatafloat 类型,4 字节)。

因为虚继承,MultipleInheritance 类还需要额外的填充字节来确保对齐,因此其总大小可能为:

  • 24 字节(来自 Derived)+ 16 字节(来自 VirtualBase)+ 4 字节(multipleData)+ 填充字节 = 48 字节

输出示例

Size of Base: 16
Size of Derived: 24
Size of VirtualBase: 16
Size of MultipleInheritance: 48

3.3 offsetof 宏

如果要知道一个结构体成员相对于结构体起始位置的偏移量,可以使用 offsetof 宏;offsetof 宏返回结构体成员相对于结构体起始地址的字节偏移量。

std::size_t offsetof(type, member); // #include <cstddef>
  • type 是结构体的类型。
  • member 是结构体的成员名称。

例子

假设有一个结构体:

#include <iostream>
#include <cstddef>  // 引入 offsetof 宏所在的头文件struct MyStruct {char a;     // 1 字节int b;      // 4 字节short c;    // 2 字节double d;   // 8 字节
};int main() {std::cout << "Offset of 'a': " << offsetof(MyStruct, a) << std::endl;  // 输出 0std::cout << "Offset of 'b': " << offsetof(MyStruct, b) << std::endl;  // 输出 4std::cout << "Offset of 'c': " << offsetof(MyStruct, c) << std::endl;  // 输出 8std::cout << "Offset of 'd': " << offsetof(MyStruct, d) << std::endl;  // 输出 16return 0;
}

3.3.1 如何理解偏移量?

偏移量是一个成员相对于结构体起始地址的字节数。在内存中,结构体成员是按特定顺序排列的,但为了满足对齐要求,编译器可能会插入填充字节。因此,实际偏移量不一定是成员的声明顺序所决定的,而是取决于对齐要求和编译器的布局规则。

应用场景

offsetof 宏在调试、内存管理、序列化和二进制协议处理中非常有用。例如,如果你需要通过指针操作结构体中的成员,或者在内存中对齐结构体成员时,知道偏移量可以帮助你正确计算成员的位置。

注意事项

  • offsetof 宏通常只用于已知结构体类型和成员名称的情况。
  • 使用 offsetof 获取偏移量时,不会引起任何实际的内存访问操作,它只是计算符号信息。

四、一些问题

4.1 什么是内存对齐,结构体是如何进行内存对齐的?

内存对齐(Memory Alignment)是指将数据按照其数据类型的要求存储在内存中的方式。由于不同的数据类型对内存存储有不同的要求,内存对齐旨在提高访问效率,确保数据按正确的边界存储,从而避免性能损失或者硬件异常。

对于第二问,上面的内容以及进行了解释。


4.2 如何让结构体按照指定的默认对齐数进行对齐?

在上文进行了 介绍,可以使用alignas关键字:alignas(16) struct B {}


4.3 如何知道结构体某个成员相对于起始位置的偏移量

在上文进行了介绍,我们可以使用 offsetof 宏;offsetof(MyStruct, a)


4.4 什么是大小端?如何测试一个机器是大端还是小端?有没有什么需要考虑大小端的场景

4.4.1 什么是大小端(Endianness)?

大小端是指在多字节数据类型(如 intfloatdouble 等)在内存中的存储顺序。它决定了数据的各个字节在内存中是如何排列的。

  • 大端(Big-endian):高位字节存储在低地址,低位字节存储在高地址。
  • 小端(Little-endian):低位字节存储在低地址,高位字节存储在高地址。

例如,假设有一个 32 位的整数 0x12345678,它占用 4 个字节。

  • 大端存储:内存中按顺序存储为 12 34 56 78,其中 12 存储在最小地址(低地址),78 存储在最大地址(高地址)。
  • 小端存储:内存中按顺序存储为 78 56 34 12,其中 78 存储在最小地址(低地址),12 存储在最大地址(高地址)。

示意图表

地址低字节端顺序(小端)高字节端顺序(大端)
0x10000x780x12
0x10010x560x34
0x10020x340x56
0x10030x120x78

4.4.2 如何测试一个机器是大端还是小端?

要测试一个机器的字节序,我们可以通过构造一个多字节数据类型,查看其数据在内存中的存储顺序。常用的方法是使用一个 2 字节(或 4 字节)数据,并检查它们在内存中的存储顺序。

示例:通过整数判断大小端

我们通过下面的代码可以测试机器的字节序:

#include <iostream>int main() {// 采用一个 2 字节的整数unsigned int num = 0x12345678;  // 0x12 34 56 78(假设大端)unsigned char* bytePtr = reinterpret_cast<unsigned char*>(&num);// 检查存储顺序if (bytePtr[0] == 0x78) {std::cout << "This machine is Little Endian." << std::endl;} else if (bytePtr[0] == 0x12) {std::cout << "This machine is Big Endian." << std::endl;}return 0;
}

4.4.3 需要考虑大小端的场景

  1. 网络协议(如 TCP/IP)
    网络协议通常使用 大端 字节序(也称为网络字节序)。例如,IP 地址、端口号等数据在传输时是以大端方式发送的。因此,在网络通信时,可能需要进行字节序转换(即大端和小端之间的转换)。

    • 例子:在将数据发送到网络时,主机可能是小端系统,但数据需要以大端顺序发送。你可以使用像 htonl()(主机字节序到网络字节序转换)和 ntohl()(网络字节序到主机字节序转换)这样的函数来处理。
  2. 文件存储和二进制协议
    在读写二进制文件时,文件的字节顺序可能与系统的字节序不同。若文件是在不同的机器上生成的(可能有不同的字节序),在读取时需要考虑字节序转换。

    • 例子:某些二进制文件格式(如图像文件、音频文件等)可能规定了字节顺序,跨平台操作时需要进行字节序转换。
  3. 多平台开发
    在进行跨平台开发时,必须特别注意字节序的问题。例如,编写需要在不同架构之间共享数据的程序时(如跨操作系统通信、数据交换等),需要确保字节序一致,否则会导致数据解释错误。

  4. 硬件与嵌入式开发
    某些硬件设备和嵌入式系统可能使用特定的字节序。例如,某些 ARM 处理器是支持大端和小端模式的,可以通过配置寄存器来选择字节序。在与硬件进行通信时,必须确保正确处理字节序问题。

  5. 数据库和其他持久化存储
    数据库或持久化存储系统可能会以特定字节序存储数据(如在多平台数据库同步时)。在实现数据存储和读取时,正确处理字节序非常重要。


4.4.4 如何进行字节序转换?

在实际开发中,通常使用以下函数进行字节序转换:

  • htonl()(host to network long):将 32 位整数从主机字节序转换为网络字节序。
  • htons()(host to network short):将 16 位整数从主机字节序转换为网络字节序。
  • ntohl()(network to host long):将 32 位整数从网络字节序转换为主机字节序。
  • ntohs()(network to host short):将 16 位整数从网络字节序转换为主机字节序。

http://www.ppmy.cn/ops/137514.html

相关文章

界面控件Kendo UI for Angular中文教程:如何构建带图表的仪表板?(二)

Kendo UI for Angular ListView可以轻松地为客户端设置一个带有图表列表的仪表板&#xff0c;包括分页、按钮选项、数字或滚动&#xff0c;以及在没有更多项目要显示时的通知等。Kendo UI for Angular是专用于Angular开发的专业级Angular组件。telerik致力于提供纯粹的高性能An…

C++编程库与框架实战——sqlite3数据库

一,SQLite数据库简介 SQLite是可以实现类似于关系型数据库中各种操作的事务性SQL数据库引擎。 SQLite可以为应用程序提供存储于本地的嵌入式数据库,帮助应用程序实现轻量级的数据存储。 SQLite是一个库文件,并不是单独的进程,它可以静态或动态链接到C++应用程序中,然后…

Python编程整理汇总(基础汇总版)

1. 基础语法 1.1 变量与数据类型 整数&#xff1a;a 10 浮点数&#xff1a;b 3.14 字符串&#xff1a;c "Hello, World!" 布尔值&#xff1a;d True 列表&#xff1a;e [1, 2, 3, 4, 5] 元组&#xff1a;f (1, 2, 3) 字典&#xff1a;g {"name&qu…

如何自动下载和更新冰狐智能辅助?

冰狐智能辅助的版本更新非常快&#xff0c;如果设备多的话每次手工更新会非常麻烦&#xff0c;现在分享一种免费的自动下载和安装冰狐智能辅助的方法。 一、安装迅雷浏览器 安装迅雷浏览器1.19.0.4280版本&#xff0c;浏览器用于打开冰狐的官网&#xff0c;以便于从官网下载a…

API设计与开发

7. API设计与开发 API&#xff08;应用程序编程接口&#xff09;是前后端通信的桥梁&#xff0c;良好的API设计能够提升应用的可用性、可维护性和扩展性。以下内容将深入探讨RESTful API原则、GraphQL的基本概念以及使用Postman进行API测试的方法。 7.1 理解RESTful API原则 …

C++设计模式之组合模式在解决层次性问题中的好处

采用组合模式在处理层次型问题时&#xff0c;会带来以下重要好处&#xff1a; 简化客户端操作&#xff1a; 客户端代码可以统一地处理单个对象和组合对象&#xff0c;而无需区分它们。这意味着客户端可以使用相同的操作来对待所有对象&#xff0c;无论它们是简单的叶子节点还是…

数据结构2:顺序表

目录 1.线性表 2.顺序表 2.1概念及结构 2.2接口实现 1.线性表 线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串 线性表在逻辑上是线性结构&#xff0c;也就说…

UPLOAD LABS | UPLOAD LABS 靶场初识

关注这个靶场的其它相关笔记&#xff1a;UPLOAD LABS —— 靶场笔记合集-CSDN博客 0x01&#xff1a;UPLOAD LABS 靶场简介 UPLOAD LABS 靶场是一个专门用于学习文件上传漏洞攻击和防御的靶场。它提供了一系列文件上传漏洞的实验环境&#xff0c;用于帮助用户了解文件上传漏洞的…