iOS底层原理02:内存对齐及结构体嵌套分析

467 阅读5分钟

什么是内存对齐

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐

为什么要内存对齐

  • 1、平台原因(移植原因)
  • 不是所有的硬件平台都能访问任意地址上的任意数据的;
  • 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 2、性能原因
  • 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
  • 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说,内存对齐就是拿空间换取时间的做法,目的是为了让CPU获取数据更高效,从而提升性能。

内存对齐规则

  • 1、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int4字节,则要从4的整数倍地址开始存储。)
  • 2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
  • 3、结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍不足的要补⻬。

知识点

各种数据类型在32位和64位操作系统下占用多少内存

内存对齐实例分析

结构体内存对齐实例

我们先来分析下面代码打印什么。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
struct Struct1 {
    int a;
    char b;
    double c;
    short d;
}s;

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
    
    NSLog(@"%lu",sizeof(s));
    
    appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

分析

struct Struct1 {
    int a;     //4 [0 3]
    char b;    //1 [4]
    double c;  //8 [8 15]
    short d;   //2 [16 17]
}struct1;

  • 1、int类型占用4个字节的内存,而且是结构体的第一个数据成员,占用位置从0开始,即 0-->3号位。
  • 2、char类型占用1个字节的内存,因此在4号位。
  • 3、double类型占用8个字节的内存,偏移从5-->12号位,遵循内存的对齐规则,5号位不是8的倍数,因此偏移从8号位开始,即 8-->15号位。
  • 4、short类型占用2个字节的内存,占用16,17号位。
  • 5、从分析看出,结构体最大的属性8个字节,结构体内部需要的大小为17个字节,从内存对齐的第三点(结构体的总大小,必须是其内部最大成员的整数倍),因此struct1的内存大小为24个字节。

代码运行结果:

结构体嵌套分析

我们再来分析下面代码打印什么。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

struct A {
    int a;
    char b;
    double c;
};

struct B {
    short a;
    struct A b;
}b;

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
        MyNSLog(@"%lu",sizeof(b));
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

分析struct A

struct A {
    char a;       // 1  [0]
    int b;        // 4  [4 7]
    double c;     // 8  [8 15]
};

  • 1、char类型占用1个字节的内存,而且是结构体的第一个数据成员,占用位置从0开始,因此占0号位。
  • 2、int类型占用4个字节的内存,遵循内存对齐规则1,因此偏移从4-->7
  • 3、double类型占用8个字节的内存,遵循内存的对齐规则1,因此偏移从8号位开始,即 8-->15号位。
  • 4、从分析看出,结构体structA 最大的属性占8个字节,结构体内部需要的大小为15个字节,从内存对齐的第三点(结构体的总大小,必须是其内部最大成员的整数倍),因此struct1的内存大小为16个字节

分析struct B

struct B {
    short a;      // 2 [0 1]
    struct A b;  // [8 23] 最大属性占8个字节,大小为16字节
}b;

  • 1、short类型占用2个字节的内存,而且是结构体的第一个数据成员,占用位置从0开始,因此占0-->1号位。
  • 2、通过分析struct A得知,b最大属性8个字节,大小为16字节,偏移从8号位开始,即8-->23号位。
  • 3、因此Struct B的内存大小为8的整数倍,等于24字节。

代码运行结果:

结构体内部成员的顺序对于内存的影响

从上面的两个实例,我们可以清楚了解结构体的内存使用情况,但也可以清晰看到有些内存空间并没有被使用。假如我们把Struct1的成员调换成下面顺序,又会是怎样的情况呢?

struct Struct1 {
    char a;       
    short b;     
    int c;        
    double d;     
}s;

分析:

struct Struct1 {
    char a;       //1  [0]
    short b;      //2  [2 3]
    int c;        //4  [4 7]
    double d;     //8  [8  15]
}s;

相信聪明的你此刻已经知道Struct1占用的内存大小了,没错就是16字节。 由此可见,对于成员相同的结构体,成员的顺序,对结构体所占空间的大小是有影响的,所以,我们不但要了解内存对齐,还是正确的利用内存对齐。