广告位联系
返回顶部
分享到

C++内存对齐的实现方法

C语言 来源:互联网 作者:佚名 发布时间:2023-11-03 23:00:27 人浏览
摘要

内存对齐的基本原则: 结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身大小的整数倍开始存储(特别注意64位机器的

内存对齐的基本原则:

  • 结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身大小的整数倍开始存储(特别注意64位机器的指针大小为8个字节)。
  • 如果一个结构A里有结构体成员B,则结构体B要从其内部"最宽基本类型成员”的整数倍地址开始存储(如struct a里存有struct b,b里有char, int, double等元素,那b应该从8的整数倍位置开始存储)。
  • 结构体的总大小为结构体的有效对齐值的整数倍,结构体的有效对齐值的确定:
    • 当未明确指定时,以结构体或结构体所包含结构体成员中最长的成员长度为其有效值。
    • 当用#pragma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值。
    • 当用__attribute__ ((packed))指定长度时,强制按照此值为结构体的有效对齐值。
    • 不管# pragma pack和__attribute__如何指定,结构体内部成员的自对齐仍然按照其自身的对齐值。
  • union以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。

空类/静态成员

程序 1

1

2

3

4

5

6

class A{

};

 

int main() {

    cout << sizeof(A) << endl;    // 1

}

对于一个什么都没有的空类,实际并不是空的,因为有默认的函数,具体可以参考 (待填入网址),大小是 1,这是因为需要有一个地址,C++ 不允许两个不同的对象有相同的地址,所以 C++ 中空的类和结构体大小都是 1。

程序 2

1

2

3

4

5

6

7

8

9

10

11

12

class A{

    A(){}

    ~A(){}

    void print() { printf("print()\n"); }

    void foo() { printf("print()\n"); }

 

    static void sprint() { printf("sprint()\n"); }

};

 

int main() {

    cout << sizeof(A) << endl;    // 1

}

这个类的大小仍然是1,成员函数、静态成员函数、静态成员变量都是不占用类的内存的,这是因为这些东西都是类的,而不是每个对象分别存储。static变量就是存储在全局静态区。

需要注意的是,子类继承空类后,子类如果有自己的数据成员,而空基类的1个字节并不会加到子类中去。

程序 3

1

2

3

4

class Empty {};

struct D : public Empty {

    int a;

};

sizeof(D)为4。

再来看另一种情况,一个类包含一个空类对象数据成员,则空类对象的大小仍为1。

程序 4

1

2

3

4

5

class Empty {};

class HaveAnInt {

    int x;

    Empty e;

}

在大多数编译器中,你会发现 sizeof(HaveAnInt) 输出为8。这是由于,Empty类的大小虽然为1,然而为了内存对齐,编译器会为HaveAnInt额外加上一些字节,使得HaveAnInt被放大到足够又可以存放一个int。

内置类型数据成员

程序 1

1

2

3

4

5

6

7

class Data

{

    char c;

    int a;

};

  

cout << sizeof(Data) << endl;

程序 2

1

2

3

4

5

6

7

class Data

{

    char c;

    double a;

};

  

cout << sizeof(Data) << endl;

显然程序 1 输出的结果为 8,程序 2 输出的结果为 16 .

程序 1 最大的数据成员是4bytes,1+4=5,补齐为4的倍数,也就是8。而程序 2 为8bytes,1+8=9,补齐为8的倍数,也就是16。

程序 3

1

2

3

4

5

6

7

8

class Data

{

    char c;

    int a;

    char d;

};

  

cout << sizeof(Data) << endl;

程序 4

1

2

3

4

5

6

7

8

class Data

{

    char c;

    char d;

    int a;

};

  

cout << sizeof(Data) << endl;

程序 3 运行结果为 12,程序 4 运行结果为 8

class中的数据成员放入内存的时候,内存拿出一个内存块来,数据成员们排队一个一个往里放,遇到太大的成员时,不是将其劈成两半能放多少就放多少,而是等下一个内存块过来。这样的话,就可以理解为什么程序 3 和程序 4 两段代码输出结果不一样了,因为程序 3 是
1 + (3) + 4 + 1 + (3) = 12,而程序 4 是1 + 1 + (2) + 4 = 8。括号中为补齐的bytes。

结构体数据成员

在默认条件下,内存对齐是以class中最大的那个基本类型为基准的,如果class中的数据成员包含其他class,则递归的取其中最大的基本类型来参与比较。

程序 1

1

2

3

4

5

6

7

8

9

10

11

12

13

class BigData

{

    char array[33];

};

  

class Data

{

    BigData bd;

    int integer;

    double d;

};

  

cout << sizeof(BigData) << "   " << sizeof(Data) << endl;

程序 2

1

2

3

4

5

6

7

8

9

10

11

12

class BigData

{

    char array[33];

};

  

class Data

{

    BigData bd;

    double d;

};

  

cout << sizeof(BigData) << "   " << sizeof(Data) << endl;

程序 1 和程序 2 运行结果均为:33 48

程序 1 和程序 2 中内存对其的基准均为8字节,BigData的大小均为33。在程序 1 中,BigData接下来是个int(4bytes),能够放下,这时候内存块还剩3bytes,而接下来是个double(8bytes),放不下,所以要等下一个内存快到来。因此,程序 1 的Data的size = 33 + 4 + (3) + 8 = 48,同理程序 2 应该是
33 + (7) + 8 = 48。

程序 3

1

2

3

4

5

6

7

8

9

10

11

12

13

 class A {                                                                                         

 public:                                                                                           

     double len;                                                                                   

     char str[33];                                                                                 

 };                                                                                                

                                                                                                    

 class B {                                                                                         

 public:                                                                                                                                          

     A a;                                                                                          

     int b;                                                                                        

 };

 

cout << sizeof(A) << "  " << sizeof(B) << endl;

以上代码输出的结果为: 48 56
不同于程序 1 和程序 2 ,程序 3 中的class A实际会占用41字节,但会发生8字节对齐,所以大小为48字节。对于class B,成员b的起始位置已发生8字节对齐,而class B整体还会发生8字节对齐,所以最终大小为56。

虚函数

C++ 的类中如果有虚函数,类内就会有一个虚函数表的指针 _vptr,指向自己的虚函数表,vptr 一般都是在类的最前边(取决于编译器的实现)。

1

2

3

4

5

6

7

class A {

public:

    A(){}

    virtual ~A(){}

    virtual void foo(){}

    virtual void print() {}

};

由于只是存一个指向虚函数表的指针,所以不管有多少个虚函数,都是 4 字节大小(32位下,任何指针大小都是 4,64位下,任何指针大小都是 8),比如上面这个类 A,size 就是 4。

需要注意的是就是,对于没有 override 的虚函数,基类和子类中 _vptr 指向的虚函数表中,这个虚函数的地址是一样的,也就是上边的 foo() 函数,而对于重写了的或者默认重写的析构函数来说,_vptr 指向的虚函数表中,函数地址是不一样的(当然两个类的 _vptr 地址也是不一样的,这是肯定的),这就能窥探到多态的实现了。

继承

不同的编译器对继承后类的大小的计算方式不同,有的是先继承后对齐,有的是先对齐后继承。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class A

{

    int i;

    char c1;

}

 

class B:public A

{

    char c2;

}

 

class C:public B

{

    char c3;

}

sizeof(C)结果是多少呢,gcc和vs给出了不同的结果,分别是8、16。

  • gcc中:C相当于把所有成员i、c1、c2、c3当作是在一个class内部,(先继承后对齐)
  • vs中:对于A,对齐后其大小是8;对于B,c2加上对齐后的A的大小是9,对齐后就是12;对于C,c3加上对齐后的B大小是13,再对齐就是16 (先对齐后继承)

内存对齐的意义

  • 效率原因:经过内存对齐之后,CPU的内存访问速度大大提升。
  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

比较两个结构体可以使用memcmp(void*, void*)吗?

不可以,memcmp函数是逐个字节进行比较的,而struct存在内存对齐,内存对齐时补的字节内容是垃圾值,所以无法比较。


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • C++特殊类设计概念与示例介绍

    C++特殊类设计概念与示例介绍
    一、设计模式概念 设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。 使用设计模式的目的:为了代码可重用
  • C++内存对齐的实现方法
    内存对齐的基本原则: 结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身
  • 更优雅的C++字符串格式化实现方法介绍
    在用C++编写代码时,经常需要用到字符串拼接及格式化,尤其是在拼写sql语句时,目前大部分sql拼接方式都是通过ostringstream流一点一点拼接
  • C++模拟实现vector

    C++模拟实现vector
    一、迭代器 定义 vector类型的迭代器就是原生态的指针,对T*进行重命名即可 1 2 typedef T* iterator; typedef const T* const_iterator; 普通迭代器 1 2 3
  • C++模拟实现vector的方法教程
    一、迭代器 定义 vector类型的迭代器就是原生态的指针,对T*进行重命名即可 1 2 typedef T* iterator; typedef const T* const_iterator; 普通迭代器 1 2 3
  • C++实现读写ini配置文件的代码
    1.概述 配置文件的读取是每个程序必备的功能,配置文件的格式多种多样,例如:ini格式、json格式、xml格式等。其中属ini格式最为简单,且
  • C++20中的span容器及用法总结
    一.span容器 span是 C++20 中引入的一个新的标准容器,它用于表示连续的一段内存区间,类似于一个轻量级的只读数组容器。 span是一个轻量级
  • C++20中的std::span介绍
    span就是一个连续对象存储的观察者。类似std::string_view是string的观察者。 连续的存储,不一定是数组。例如: 1 2 3 4 5 6 7 8 zero(char (arr) [10]
  • C++11之std::future对象的使用以及说明

    C++11之std::future对象的使用以及说明
    std::future介绍 在前面几篇文章中基本都用到thread对象,它是C++11中提供异步创建多线程的工具。 但是我们想要从线程中返回异步任务结果,
  • C语言中#define在多行宏定义出错的原因及分析
    C语言中#define在多行宏定义出错的原因 1.第一种错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #includestdio.h #define echange(a,b) {\/*宏定义中允许包含多行命
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计