首先强调一点,sizeof
是C语言的关键字和操作符,不是一个函数,它一般用来计算目标空间占用为多少个字节,目标可以是类型或者变量,当目标是类型的时候:
sizeof(int); // 4 // 实际上编译器获取的是int的空间占用属性 int->空间占用; // 4
当目标是变量时:
int x = 5; sizeof(x); // 4 // 等价于 x = { 空间占用: 4, 存储格式: int存储格式, 读取格式: int读取格式, 存储空间指针: 指向第一步分配好的空间, } x->空间占用
上文提到过,变量的属性都是继承自格式的属性,所以sizeof(x)
和sizeof(int)
一定是相等的。
sizeof
在使用中有很多陷阱,其中最混乱的就是涉及到指针时的结果,搞清这个问题只要记住一句话:sizeof计算时不会溯源,溯源的意思是根据目标指针寻找指向的内存,通过下面几个例子说明:
1. 普通变量
char a = 'c'; sizeof(a); // 1 seizof(&a); // 8
a
的格式是char
,&a
的格式是char *
,指针的含义通俗来讲就是一个存储地址,任何类型的存储地址格式都是一样的,所以任意类型的指针大小都是一样的,这一点很重要,正是因为指针类型大小的高度统一,才为后来的类型转换提供了无限的可能。
2. 结构体
typedef struct { char a; char b; }Test; Test test; Test * p = (Test *)malloc(sizeof(Test)); ① sizeof(Test); // 2 ② sizeof(test); // 2 ③ sizeof(p); // 8 ④ sizeof(*p); // 2 ⑤ sizeof(&p); // 8
①和②同普通类型一样,就是返回的结构体格式的空间占用,变量test
继承自Test
格式,二者必然相等。③式正是上文说到过的sizeof计算时不会溯源,变量p
指向一块手动开辟的内存,这块内存的大小为2,但是sizeof
只会访问p->空间占用
,不会去访问计算p
指向的内存块,所以得到的是指针类型的空间占用。④式中*p
即访问的内存块,所以会返回内存块的大小2。⑤式是一个二重指针,即指针的指针,上文提到过任意类型的指针大小都一样,所以无论多少重指针,它的类型都是指针,大小也就都一样。
除了上面的问题,使用sizeof操作结构体时还有一个无法避免的问题——内存对齐,执行内存对齐主要是为了加快cpu的访问速度,也有一些考虑到平台移植的因素,先来熟悉下内存对齐的规则:
结构((或联合)的数据成员,第一个数据成员放在偏移量为0的地方,以后每个数据成员的offset必须是
#pragma pack(n)
指定的n
和数据成员(如果是复合类型则为成员的子成员长度)的自身长度中较小的那个数的倍数。
数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack(n)
指定的数值和结构(或联合)最大数据成员(如果是复合类型则为成员的子成员长度)长度中,比较小的那个进行。
typedef struct { int x; dpuble y; char z[10]; } Example; sizeof(Example); // 32
根据第一条规则,第一个数据成员x
放在偏移量为0的地方,所以【0 - 3】存放x,数据成员y
存放时,起始偏移为4,但是4不是8的倍数,这时候【4 - 7】就被废弃了,y
从8开始存放,占据【8 - 15】,数据成员z
存放时,起始偏移为16,16是1的倍数可以存放,所以z
占据【16 - 25】,此时一共占据了26个字节,根据第二条规则,执行自身对齐,最大的数据成员为y
,所以结构体长度应为8的倍数,故【26 - 31】被废弃,总长度32字节。
当程序中使用了#pragma pack(n)
时,会对对齐结果产生一定的影响:
#pragma pack(4) typedef struct { int x; dpuble y; char z[10]; }Example; sizeof(Example); // 24
【0 - 3】依旧存放数据成员x
,存放数据成员y
时,y
的长度为8,指定的n
为4,所以对齐取4,起始偏移为4,是4的倍数,所以【4 - 9】存放y
,存放数据成员z
时,起始偏移为10,n > 1
,所以【10 - 19】存放z
,此时一共占据20字节,根据第二条规则,n < 8
,自身对齐时使用4,故【20 - 23】被废弃,总长度24字节。
当
#pragma pack(1)
指定为1时,相当于不执行内存对齐。
3. 数组
char a[10]; char * p1 = a; char (* p2)[10] = a; ① sizeof(a); // 10 ② sizeof(p1); // 8 ③ sizeof(p2); // 8 ④ sizeof(*p1); // 1 ⑤ sizeof(*p2); // 10
①式返回数组a
的空间占用,回顾前文知道该值为1 * 10
,这里再强调一次,变量a
的类型为char[10]
,并非char
。②式和③式就是普通的指针类型,所以sizeof
返回的都是8,其中p2
是一个比较特殊的指针,这里说它特殊只是语法特殊,使用方法和普通指针无异,它们的格式结构如下:
p1 = { 目标格式: char格式, 空间占用: 指针->空间占用, // 8 存储格式: 指针->存储格式, 读取格式: 指针->读取格式, 存储空间指针: 指向编译器为指针类型分配的8个字节(变量a的首地址), } p2 = { 目标格式: char[10]格式, 空间占用: 指针->空间占用, // 8 存储格式: 指针->存储格式, 读取格式: 指针->读取格式, 存储空间指针: 指向编译器为指针类型分配的8个字节(变量a的首地址), }
从上面可以看出来,p1
和p2
除了目标格式不同,其它无异。前面介绍数组时,提到过a == &a[0]
,取址符返回的就是a->存储空间指针
,通过这可以判断p1 == p2
,并且二者都是数组a
的首地址,这里假设a
的首地址为0,当使用*
解析p1
和p2
地址时得到的边界地址计算如下:
// p1 首地址 = p1->存储空间指针指向的内存块中存储的值(a的首地址0) 尾地址 = 首地址 + p1->目标格式->空间占用(就是char的空间占用1) // *p1得到的内存块为【0 - 1】 // p2 首地址 = p2->存储空间指针指向的内存块中存储的值(a的首地址0) 尾地址 = 首地址 + p2->目标格式->空间占用(即char[10]的空间占用 1 * 10 = 10) // *p2得到的内存块为【0 - 10】
所以④式返回1,⑤式返回10。