02-结构体和OC类的内存对齐

简介: 02-结构体和OC类的内存对齐

获取内存大小相关的三个函数

sizeof:

sizeof是C/C++中的一个操作符(operator),简单的说其作用就是返回一个对象或者类型所占的内存字节数。sizeof的计算发生在编译时刻,所以它可以被当作常量表达式使用;最新的C99标准规定sizeof也可以在运行时刻进行计算,但在没有完全实现C99标准的编译器中就行不通了;所以我们最好还是认为sizeof是在编译期执行的,这样不会带来错误,让程序的可移植性强些;


class_getInstanceSize:

获取实例对象中成员变量的内存大小;


malloc_size:是为其实际分配了多少空间;

通过下面的代码看一下:

#import <objc/runtime.h>
#include <malloc/malloc.h>
struct NSObject_IMPL {
    Class isa;
};
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};
@interface Person : NSObject
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"%lu", sizeof(struct Person_IMPL));
        NSLog(@"%ld", class_getInstanceSize([p class]));
        NSLog(@"%lu", malloc_size((__bridge const void *)(p)));
    }
    return 0;
}

打印结果

8
8
16

sizeof:计算的就是某个类型所占用的空间大小,也就是结构体Person_IMPL所占用的内存大小;如果直接sizeof(p)则为p这个指针所占用的大小,无论Person里面有多少个成员变量,sizeof(p)的大小都是8个字节;

class_getInstanceSize:通过源码可知调用顺序如下:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

接下来调用cls->alignedInstanceSize:




uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}

接下来调用word_align内联函数:

#ifdef __LP64__
#   define WORD_MASK 7UL
#else
#   define WORD_MASK 3UL
#endif
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

由word_align函数可知,计算的最终结果始终是8的倍数(8,16,24...);malloc_size:为实际分配的大小,始终是16字节对齐,16的倍数,具体可参考libmalloc源码;内存对齐

基本概念

  • 数据类型自身的对齐值:对于char型数据,其自身对齐值为1,对于short型为2,对于int, float类型,其自身对齐值为4,对于double型,其自身对齐值为8,单位字节,具体也可参考类型表;
  • 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值;
  • 指定对齐值:#pragma pack (value)时的指定对齐值value,OC上对齐值为8;
    数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

对齐规则

  • 数据成员的对齐规则可以理解为min(m, n)的公式, 其中m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件m整除n(即 m % n == 0), n从m位置开始存储, 反之继续检查m+1能否整除n, 直到可以整除, 从而就确定了当前成员的开始位置。

  • 数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8;
  • 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

各种类型内存占用对照表:分析下面这两个结构体所占用的内存大小


struct Struct1 {    char a;     //1
    double b;   //8
    int c;      //4
    short d;    //2
};//24
struct Struct2 {
    double b;   //8
    int c;      //4
    short d;    //2
    char a;     //1
};//16
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Struct1 = %lu - Struct2 = %lu",sizeof(struct Struct1), sizeof(struct Struct2));
    }
    return 0;
}

打印结果:


Struct1 = 24 - Struct2 = 16

从定义的变量和类型来看两者是一致的,只是顺序不一样,为什么占用的内存大小不一致呢?根据内存对齐规则来进行分析:

Struct1:

char a:从0开始,占1个字节,此时min(0, 1),0除以1可以整除,故0存储a;

double b:从1开始,占8个字节,此时min(1, 8),1除以8不可以整除,直到min(8, 8)才可以,故b的内存空间8-15,来存储b;

int c:从16开始,占4个字节,此时min(16, 4);可以整除,故16-19存储c;

short d:从20开始,占2个字节,此时min(20, 2),可以整除,故20-21存储d;

又因为Struct1最大的变量占用8个字节,因需8字节对齐,21距离最近的8的倍数的值为24,所以Struct1占用了24字节的大小;

Struct2:

double b:从0开始,占8个字节,此时min(0, 8),可以整除,故0-7存储b;

int c:从8开始,占4个字节,此时min(8, 4);可以整除,故8-11存储c;

short d:从12开始,占2个字节,此时min(12, 2),可以整除,故12-13存储d;

char a:从14开始,占1个字节,此时min(14, 1),可以整除,故14存储a;

又因为Struct2最大的变量占用8个字节,因需8字节对齐,14距离最近的8的倍数的值为16,所以Struct2占用了16字节的大小;

探索OC类定义成员变量和属性顺序不同导致占用分配大小的不同?

OC类定义成员变量:

@interface Person : NSObject {
    @public
    char a;     //1
    double b;   //8
    int c;      //4
    short d;    //2
}
@end
@implementation Person
@end
@interface Person2 : NSObject {
    @public
    double b;   //8
    int c;      //4
    short d;    //2
    char a;     //1
}
@end
@implementation Person2
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%ld, %ld", class_getInstanceSize([Person class]), class_getInstanceSize([Person2 class]));
    }
    return 0;
}

运行结果:


32, 24

相比于Struct1和Struct2多了一个isa指针,这里就不再描述计算细节了,我们会发现只是顺序不同,定义成员变量的时候,占用的内存也不相同;

OC类定义属性:


@interface Person : NSObject
@property(assign, nonatomic) char a;
@property(assign, nonatomic) double b;
@property(assign, nonatomic) int c;
@property(assign, nonatomic) short d;
@end
@implementation Person
@end
@interface Person2 : NSObject
@property(assign, nonatomic) double b;
@property(assign, nonatomic) int c;
@property(assign, nonatomic) short d;
@property(assign, nonatomic) char a;
@end
@implementation Person2
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%ld, %ld", class_getInstanceSize([Person class]), class_getInstanceSize([Person2 class]));
    }
    return 0;
}

运行结果:


24, 24

从运行结果可以看出,当定义属性的时候,两者虽然顺序不同,占用的内存空间却是相同的。

我们查看一下底层实现:


xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

struct Person_IMPL {
  struct NSObject_IMPL NSObject_IVARS;
  char _a;
  short _d;
  int _c;
  double _b;
};

struct Person2_IMPL {
  struct NSObject_IMPL NSObject_IVARS;
  char _a;
  short _d;
  int _c;
  double _b;
};

我们会发现,当转换成c++实现的时候,顺序自动又调整好了,故当我们再获取的时候,两者的值是相同的(当定义成员变量的时候、内部未自动调整顺序,自行尝试),苹果内部对内存做了优化,对属性进行了重排;总结

  • sizeof:某个类型所占用的内存大小;
  • class_getInstanceSize:采用8字节对齐的方式排列,分配的大小为8的整数倍,为OC实例对象至少需要的内存空间;
  • malloc_size:采用16字节对齐方式排列,实际分配内存的大小为16的整数倍,为分配给OC对象的实际内存大小;
  • 对于OC类,成员变量的定义顺序影响内存分配的大小,而属性的定义顺序不会影响内存的分配,通常我们可以把占用字节数较高的类型放在后面;
相关文章
|
25天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
92 13
|
25天前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
54 11
|
26天前
|
编译器 Go
探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?
在 Go 语言中,内存对齐是优化内存访问速度的重要概念。通过调整数据在内存中的位置,编译器确保不同类型的数据能够高效访问。本文通过示例代码展示了两个结构体 `A` 和 `B`,尽管字段相同但排列不同,导致内存占用分别为 40 字节和 48 字节。通过分析内存布局,解释了内存对齐的原因,并提供了优化结构体字段顺序的方法,以减少内存填充,提高性能。
39 3
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
80 4
|
1月前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
26 3
|
1月前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
1月前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
2月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
2月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(三)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
2月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(一)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作