C/C++ 从sizeof运算符到内存对齐

前两天看到了一个有关sizeof运算符和内存对齐的问题,然后花了半天时间一直在查资料和思考。现在准备来填上这个坑了。

1. sizeof运算符

1.1 基本知识点

  • sizeof运算符返回对象类型所占的字节数。它有下面的使用形式:

    1
    sizeof(type);  //只返回类型所占的字节数,并不实际计算运算对象的值
  • sizeof的结果部分地依赖于其作用的类型:

    • char或者类型为char的表达式执行的结果为1。
    • 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
    • 对指针执行sizeof运算得到指针本身所占空间的大小。
    • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针本身不需要有效。
    • 对数组执行sizeof运算得到整个数组占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将结果求和。注意,虽然数组名表示的是指向数组首元素的指针,但是sizeof并不会把数组转换成指针处理的。
    • string或者vector对象执行sizeof运算只返回该类型固定部分的大小(这里涉及了stringvector的实现,暂不说明),不会计算对象中的元素占用了多少空间。

1.2 计算数组大小

  • 因为对数组执行sizeof运算得到整个数组占空间的大小,所以可以用数组的大小除以单个元素的大小得到数组元素的个数。

    1
    2
    3
    4
    5
    6
    #include <iostream>
    int main(){
    int x[10];
    std::cout << "x的大小为:" << sizeof(x) / sizeof(*x) << std::endl;
    return 0;
    }

    结果如下

  • 注意,这种方法只对数组有效。对下面的情况是无效的。

    1
    2
    3
    4
    5
    6
    #include <iostream>
    int main(){
    int x[10];int *p = x;
    std::cout << sizeof(p) << std::endl << sizeof(*p) << std::endl;
    return 0;
    }

    这里,sizeof(p)得到的是p这个指针的大小,在64位系统就是8;sizeof(*p)得到的是首元素类型所占的字节数,对int类型来说就是4。

1.3 计算字符指针指向字符串的大小

  • 虽然不能直接对指针做sizeof运算来得到所指向的整个数组的大小,但是我们可以通过指针来一个个访问里面的元素,只要这个数组设置了一个结束标志,那么我们就可以通过递归的方法来求它的大小。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    int GetSize(const char* x){
    if(*x == '\0')
    return 0;
    return GetSize(x + 1) + 1;
    }
    int main(){
    char x[10] = "12345678";
    char *p = x;
    std::cout << GetSize(p) << std::endl;
    return 0;
    }

    运行结果是

2. 结构体(类)与内存对齐

  • 在我们定义抽象数据类型时会用到struct或者class,我们经常把前面那个叫做结构体,而把后面那个叫做。其实这两个在本质上都是相同的,唯一的一点小小的区别就是:

    structclass的默认访问权限不同,struct默认成员都是public的,而class默认成员都是private的。

    因此为了简便,后面只用struct来举例说明。

2.1 只含基本类型的结构体(类)所占空间的大小

2.1.1 内存对齐的引入

  • 对于一个有明确元素个数的数组来说,它所占空间的大小就是元素个数 * 元素类型所占空间的大小的值。但是对于结构体来说也是这样的吗?看看下面的案例代码吧。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>
    int main(){
    struct test{
    int a;
    short b;
    char c;
    };
    std::cout << "sizeof(test): " << sizeof(test) << std::endl;
    return 0;
    }

    我们知道,int类型所占空间的大小为4,short为2,char为1。可是,组合得到的结构体的大小却是8,而不是简单的4 + 2 + 1 = 7

  • 这种现象发生的原因就是编译器在做内存对齐。为什么要弄这种东西呢?

    大部分的参考资料都是如是说的:

    1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

    简单来说就是把每个数据单元安排在合适的位置上,来让程序可以安全、迅速的运行。

2.1.2 内存对齐的基本规则

  • 首先介绍的是对齐系数

    每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1, 2, 4, 8, 16来改变这一系数,其中的n就是你要指定的“对齐系数”。

    对于64位操作系统来说,大部分编译器默认的对齐系数都是8

  • 下面来看看内存对齐具体的规则。它可以简单的表达为下面的几条。

    • 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在偏移量为0的地方,以后每个数据成员的对齐按照对齐系数指定的数值和这个数据成员自身长度中,比较小的那个进行偏移量为的整数倍
    • 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照对齐系数指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行偏移量为的整数倍
  • 现在结合代码来实地的分析一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    using namespace std;
    int main(){
    struct test1{
    //默认对齐系数是 8
    int a; //a是首元素,类型大小是 4,所以 a 所占空间的大小是 4
    short b; //类型大小是 2,但是前面已经放了 a,所以偏移量是 4
    char c; //类型大小是 1,但前面有 a 和 b,所以偏移量为 4 + 2 = 6
    }; //整个结构体最大成员的类型大小是4,所以整体的偏移量是 大于 6 的最小的 4 的倍数,即 8
    test1 t;
    cout << "t.a的起始地址:" << &(t.a) << endl;
    cout << "t.b的起始地址:" << &(t.b) << endl;
    cout << "t.c的起始地址:" << (void *)&(t.c) << endl;
    cout << "sizeof(test1): " << sizeof(t) << endl;
    return 0;
    }

    运行结果如下

2.2 成员顺序对整体大小的影响

  • 根据2.1.2节中的内存对齐的规则,可以知道结构体内成员的出现顺序对整体大小有很大的影响。保证结构体整体所占空间最小,也就是保证浪费的多余偏移量最小。所以我们在设计结构体时,应该按照成员所占空间大小从大到小依次排列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <iostream>
    using namespace std;
    int main(){
    struct test1{
    double a; // 偏移量:0,大小:8
    int b; // 偏移量:8,大小:4
    char c; // 偏移量:12,大小:1
    };

    struct test2{
    int a; // 偏移量:0,大小:4
    double b; // 偏移量:8,大小:8
    char c='1'; // 偏移量:16,大小:1
    };

    struct test3{
    char a; // 偏移量:0,大小:1
    double b; // 偏移量:8,大小:8
    int c; // 偏移量:16,大小:4
    };
    cout << "sizeof(test1): " << sizeof(test1) << endl;
    cout << "sizeof(test2): " << sizeof(test2) << endl;
    cout << "sizeof(test3): " << sizeof(test3) << endl;
    return 0;
    }

    代码运行结果如下

2.3 含有复合类型的结构体所占空间的大小

  • 对于是复合类型的数据成员来说,它的字节数是就是它本身的大小,偏移量为的整数倍

  • 对于整个结构体来说,它的字节数依然按照前面的算法进行计算。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <iostream>
    using namespace std;
    int main(){
    struct test1{
    char a;
    short b;
    int c;
    };

    struct test2{
    char a;
    test1 b;
    };

    test2 t;
    cout << "&t.a: " << (void *)&(t.a) << endl << "&t.b: " << &(t.b) << endl;
    cout << "sizeof(test2.b): " << sizeof(t.b) << endl;
    cout << "sizeof(test2): " << sizeof(t) << endl;

    return 0;
    }

------------- 本文结束 感谢您的阅读 -------------

本文标题:C/C++ 从sizeof运算符到内存对齐

文章作者:Perry

发布时间:2019年09月25日 - 01:13

最后更新:2019年09月25日 - 01:37

原始链接:https://perry96.com/archives/227be1bc.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%