结构体成员的内存分布与对齐
马国峻
maguojun2005@sina.com
我们先看一道IBM和微软的笔试题:
IBM笔试题:
struct{
short a1;
short a2;
short a3;
}A;
struct{
long a1;
short a2;
}B;
sizeof( A)=6, sizeof(B)=8,为什么?
注:sizeof(short)=2,sizeof(long)=4
微软笔试题:
struct example1
{
short a ;
long b;
};
struct example2
{
char c;
example1struct1;
short e;
};
int main(int argc, char* argv[])
{
example2e2;
intd=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
return 0;
}
输出结果?
要能清除的分析上面的问题就要搞清楚结构体变量的成员在内存里是如何分布的、成员先后顺序是怎样的、成员之间是连续的还是分散的、还是其他的什么形式?其实这些问题既和软件相关又和硬件相关。所谓软件相关主要是指和具体的编程语言的编译器的特性相关,编译器为了优化CPU访问内存的效率,在生成结构体成员的起始地址时遵循着某种特定的规则,这就是所谓的 结构体成员“对齐”;所谓硬件相关主要是指CPU的“字节序”问题,也就是大于一个字节类型的数据如int类型、short类型等,在内存中的存放顺序,即单个字节与高低地址的对应关系。字节序分为两类:Big-Endian和Little-Endian,有的文章上称之为“大端”和“小端”,他们是这样定义的:
Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
Intel、VAX和Unisys处理器的计算机中的数据的字节顺序是Little-Endian,IBM 大型机和大多数Unix平台的计算机中字节顺序是Big –Endian。
关与Big-Endian和Little-Endian问题本文暂不做详细讨论,本文将以小端机(此处为intel x86架构的计算机)、OS:WindowsXp和VC++6.0编译器来详细讨论结构体成员的“对齐”问题。
前面说了,为了优化CPU访问内存的效率,程序语言的编译器在做变量的存储分配时就进行了分配优化处理,优化规则大致原则是这样:
对于n字节的元素(n=2,4,8,...),它的首地址能被n整除,这种原则称为“对齐”,如WORD(2字节)的值应该能被2整除的位置,DWORD(4字节)应该在能被4整除的位置。
对于结构体来说,结构体的成员在内存中顺序存放,所占内存地址依次增高,第一个成员处于低地址处,最后一个成员处于最高地址处,但结构体成员的内存分配不一定是连续的,编译器会对其成员变量依据前面介绍的 “对齐”原则进行处理。对待每个成员类似于对待单个n字节的元素一样,依次为每个元素找一个适合的首地址,使得其符合上述的“对齐”原则。通常编译器中可以设置一个对齐参数n,但这个n并不是结构体成员实际的对齐参数,VC++6.0中结构体的每个成员实际对齐参数N通常是这样计算得到的N=min(sizeof(该成员类型),n)(n为VC++6.0中可设置的值)。
成员的内存分配规律是这样的:从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址x,该条件是x %N =0,并且整个结构的长度必须为各个成员所使用的对齐参数中最大的那个值的最小整数倍,不够就补空字节。
结构体中所有成员的对齐参数N的最大值称为结构体的对齐参数。
VC++6.0中n 默认是8个字节,可以修改这个设定的对齐参数,方法为在菜单“工程”的“设置”中的“C/C++”选项卡的“分类”中 “CodeGeneration ”的“Structmember alignment”中设置,1byte、2byte、4byte、8byte、16byte等几种,默认为8byte
也可以程序控制,采用指令:#pragma pack(xx)控制
如#pragma pack(1),1字节对齐,#pragma pack(4),4字节对齐
#pragma pack(16),16字节对齐
接下来我们将分不同的情况来详细讨论结构体成员的分布情况,顺便提醒一下,常见类型的长度:Int4byte,Short 2byte,Char 1byte,Double 8byte,Long 4byte
让我们先看下例:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
int i; //4byte
};
int main(int argc, char* argv[])
{
A strua;
printf("%len:d\n",sizeof(A));
printf("%d,%d,%d,%d",&strua.c,&strua.d,&strua.s,&strua.i);
return 0;
}
1)n设置为8byte时
结果:len:24,
1245032,1245040,1245048,1245052
内存中成员分布如下:
strua.c分配在一个起始于8的整数倍的地址1245032(为什么是这样读者先自己思考,读完就会明白),接下来要在strua.c之后分配strua.d,由于double为8字节,取N=min(8,8),8字节来对齐,所以从strua.c向后找第一个能被8整除的地址,所以取1245032+8得1245040, strua.s 为2byte小于参数n,所以N=min(2,8),即N=2,取2字节长度对齐,所以要从strua.d后面寻找第一个能被2整除的地址来存储strua.s,由于strua.d后面的地址为1245048可以被2整除,所以strua.s紧接着分配,现在来分配strua.i,int为4byte,小于指定对齐参数8byte,所以N=min(4,8)取N=4byte对齐,strua.s后面第一个能被4整除地址为1245048+4,所以在1245048+4的位置分配了strua.i,中间补空,同时由于所有成员的N值的最大值为8,所以整个结构长度为8byte的最小整数倍,即取24byte其余均补0.
于是该结构体的对齐参数就是8byte。
2)当对齐参数n设置为16byte时,结果同上,不再分析
3)当对齐参数设置为4byte时
上例结果为:Len:20
1245036,1245040,1245048,1245052
内存中成员分布如下:
Strua.c起始于一个4的整数倍的地址,接下来要在strua.c之后分配strua.d,由于strua.d长度为8byte,大于对齐参数4byte,所以N=min(8,4)取最小的4字节,所以向后找第一个能被4整除的地址来作为strua.d首地址,故取1245036+4,接着要在strua.d后分配strua.s,strua.s长度为2byte小于4byte,取N=min(2,4)2byte对齐,由于strua.d后的地址为1245048可以被2
整除,所以直接在strua.d后面分配,strua.i的长度为4byte,所以取N=min(4,4)4byte对齐,所以从strua.s向后找第一个能被4整除的位置即1245048+4来分配和strua.i,同时N的最大值为4byte,所以整个结构的长度为4byte的最小整数倍16byte
4)当对齐参数设置为2byte时
上例结果为:Len:16
1245040,1245042,1245050,1245052
Strua.c分配后,向后找一第一个能被2整除的位置来存放strua.d,依次类推
5)1byte对齐时:
上例结果为:Len:15
1245040,1245041,1245049,1245051
此时,N=min(sizeof(成员),1),取N=1,由于1可以整除任何整数,所以各个成员依次分配,没有间空,如下图所示:
6)当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是将数组的每个元素当一个成员来分配,其他分配规则不变,如将上例的结构体改为:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
char szBuf[5];
};
对齐参数设置为8byte,则,运行结果如下:
Len:24
1245032,1245040,1245048,1245050
Strua 的s分配后,接下来分配Strua 的数组szBuf[5],这里要单独分配它的每个元素,由于是char类型,所以N=min(1,8),取N=1,所以数组szBuf[5]的元素依次分配没有间隙。
7)当结构中有成员不是一个完整的类型单元,如int或short型,而是该类型的一段时,即位段时,如
struct A
{
int a1:5;
int a2:9;
char c;
int b:4;
short s;
};
对于位段成员,存储是按其类型分配空间的,如int型就分配4个连续的存储单元,如果是相邻的同类型的段位成员就连续存放,共用存储单元,此处如a1,a2将公用一个4字节的存储单元,当该类型的长度不够用时,就另起一个该类型长度的存储空间。有位段时的对齐规则是这样:同类型的、相邻的可连续在一个类型的存储空间中存放的位段成员作为一个该类型的成员变量来对待,不是同类型的、相邻的位段成员,分别当作一个单独得该类型的成员来对待,分配一个完整的类型空间,其长度为该类型的长度,其他成员的分配规则不变,仍然按照前述的对齐规则进行。
对于struct A,VC++6.0中n设置为8时,sizeof(A)=16,内存分布:
又如:
struct B
{
int a:5;
int b:7;
int c:6;
int d:9;
char e:2;
int x;
};
Vc++6.0的对齐参数设置为8、16、4字节对齐时,sizeof(A)=12内存分布为:
(灰色部分未使用)
当对齐参数设置为2字节时:(灰色部分未使用)sizeof(A)=10
又如intel的笔试题:
#include “stdafx.h”
#include <iostream.h>
struct bit
{
int a:3;
int b:2;
int c:3;
};
int main(int argc, char* argv[])
{
bit s;
char *c = (char*)&s;
*c = 0x99;
cout << s.a <<endl <<s.b<<endl<<s.c<<endl;
return 0;
}
Output:?
运行的结果是 1 -1 -4
结构bit的成员在内存中由低地址到高地址顺序存放,执行*c=0x99;后成员的内存分布情况为:
8)当结构体成员是结构体类型时,那么该过程是个递归过程,且把该成员作为一个整体来对待,如(微软笔试题):
struct example1
{
short a ;
long b;
};
struct example2
{
char c;
example1struct1;
short e;
};
int main(int argc, char* argv[])
{
example2e2;
intd=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
return 0;
}
8byte对齐时,结果为:
8,16,4
内存分布为:
因为example1的对齐参数为4,分配完c后要接着分配struct1,这时的对齐参数为min(struct1的对齐参数,指定对齐参数),开始分配struct1,在struct1的成员分配过程中又是按照前述的规则来分配的。
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再透明了。
一、内存对齐的原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
二、对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragmapack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
对齐步骤:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragmapack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2颗推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
备注:数组成员按长度按数组类型长度计算,如char t[9],在第1步中数据自身长度按1算,累加结构体时长度为9;第2步中,找最大数据长度时,如果结构体T有复杂类型成员A的,该A成员的长度为该复杂类型成员A的最大成员长度。
三、试验
我们通过一系列例子的详细说明来证明这个规则吧!
我试验用的编译器包括GCC3.4.2和VC6.0的C编译器,平台为Windows XP + Sp2。
我们将用典型的struct对齐来说明。首先我们定义一个struct:
#pragma pack(n) /* n = 1, 2, 4, 8, 16 */
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack(n)
首先我们首先确认在试验平台上的各个类型的size,经验证两个编译器的输出均为:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
我们的试验过程如下:通过#pragmapack(n)改变“对齐系数”,然后察看sizeof(structtest_t)的值。
1、1字节对齐(#pragma pack(1))
输出结果:sizeof(structtest_t) = 8 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(1)
struct test_t {
int a; /* 长度4< 1 按1对齐;起始offset=0 0%1=0;存放位置区间[0,3] */
char b; /* 长度1= 1 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* 长度2> 1 按1对齐;起始offset=5 5%1=0;存放位置区间[5,6] */
char d; /* 长度1= 1 按1对齐;起始offset=7 7%1=0;存放位置区间[7] */
};
#pragma pack()
成员总大小=8
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 1) = 1
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 8 /* 8%1=0 */ [注1]
2、2字节对齐(#pragma pack(2))
输出结果:sizeof(structtest_t) = 10 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(2)
struct test_t {
int a; /* 长度4> 2 按2对齐;起始offset=0 0%2=0;存放位置区间[0,3] */
char b; /* 长度1< 2 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* 长度2= 2 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d; /* 长度1< 2 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
};
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 2) = 2
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 10 /* 10%2=0 */
3、4字节对齐(#pragma pack(4))
输出结果:sizeof(structtest_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(4)
struct test_t {
int a; /* 长度4= 4 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
char b; /* 长度1< 4 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* 长度2< 4 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d; /* 长度1< 4 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
};
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 4) = 4
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */
4、8字节对齐(#pragma pack(8))
输出结果:sizeof(structtest_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(8)
struct test_t {
int a; /* 长度4< 8 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
char b; /* 长度1< 8 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* 长度2< 8 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d; /* 长度1< 8 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
};
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 8) = 4
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */
5、16字节对齐(#pragma pack(16))
输出结果:sizeof(structtest_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(16)
struct test_t {
int a; /* 长度4< 16 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
char b; /* 长度1< 16 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* 长度2< 16 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d; /* 长度1< 16 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */
};
#pragma pack()
成员总大小=9
2) 整体对齐
整体对齐系数 =min((max(int,short,char), 16) = 4
整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */
如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。 其实字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则: 1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除; 2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;例如上面第二个结构体变量的地址空间。 3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
sizeof用法总结
在VC中,sizeof有着许多的用法,而且很容易引起一些错误。下面根据sizeof后面的参数对sizeof的用法做个总结。
A. 参数为数据类型或者为一般变量。例如sizeof(int),sizeof(long)等等。这种情况要注意的是不同系统系统或者不同编译器得到的结果可能是不同的。例如int类型在16位系统中占2个字节,在32位系统中占4个字节。
B. 参数为数组或指针。下面举例说明.
char a[50];//sizeof(a)=4*50=200; 求数组所占的空间大小
char new_int[50];
char *a=new_int[50];
// sizeof(a)=4; a为一个指针,sizeof(a)是求指针的大小,在32位系统中,当然是占4个字节。
//sizeof(*a)=1;这个表示求指针变量的数据类型所占空间大小
//sizeof(new_int)=50*1表示求取指针指向数组所占空间的大小
C. 参数为结构或类。Sizeof应用在类和结构的处理情况是相同的。但有两点需要注意,第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。
第二、没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一
个实例在内存中都有唯一的地址。
下面举例说明,
Class Test{int a;staticdouble c};//sizeof(Test)=4.
Test *s;//sizeof(s)=4,s为一个指针。
Class test1{};//sizeof(test1)=1;
D. 参数为其他。下面举例说明。
int func(char s[5]);
{
cout<
//数的参数在传递的时候系统处理为一个指针,所
//以sizeof(s)实际上为求指针的大小。
return 1;
}
sizeof(func(“1234”))=4//因为func的返回类型为int,所以相当于
//求sizeof(int).
以上为sizeof的基本用法,在实际的使用中要注意分析VC的分配变量的分配策略,这样的话可以避免一些错误。
sizeof操作综述
sizeof是C关键字,sizeof()是单目表达式,其基本用法是求一个数据类型或表达式所占存储空间的字节数.由于C可以定义很复杂的数据结构,sizeof的求值也会有各种复杂的计算.因此很多面试题通常用人工计算sizeof()的结果,以检验开发者对C语言各个方面的掌握程度.
首先要确认一点,sizeof()的结果依赖于CPU的字长和操作系统本身的设置.如常用的32位CPU的字长就是32bit,一般Windows操作系统下,这样整数(int)和长整数(long)均为32bit即4字节长,而short型为2字节长.但是dos下,Turbo C的sizeof(int)=sizeof(short)=2.(待求证)
在64位的CPU下,字长为64bit,但是基本类型int很多时候为保持软件兼容性,仍然是4个byte.只在Windows下的_int64和Linux 的long long 类型才是64bit,即8个字节.
因此,求sizeof()结果,必须要首先明确CPU的类型,有时还要确定操作系统.如果是嵌入式软件开发人员的测试,没有指出环境可以认为是有错的.
下列各种类型,如果没有特别指明,都是32 bit CPU,Windows操作系统.
sizeof()样例
基本类型
sizeof()主要能对两类对象进行操作,
l sizeof(数据类型)
数据类型可以是基本数据类型,如char,int,也可以是复合或自定义的数据类型,如结构等.
l sizeof(表达式)
表达式,首先要对表达式求值.然后再计算结果的数据类型的宽度.如果是常量表达式,还需要牵涉到常量会被默认转换类型的问题.
l sizeof 表达式
用于表达式,sizeof操作可以写成这样 sizeof var_name
如int aa;printf(“%d/n”,sizeof aa);
在32位CPU下,假设Windows(Linux也一样),将会有如下基本数据类型取值
sizeof(char) = 1 ; sizeof(unsigned char)=1
sizeof(short)= 2 ; sizeof(unsigned short)=2
sizeof(int)= 4 ; sizeof(unsigned int)= 4
sizeof(long)= 4 ; sizeof(unsigned long)= 4
sizeof(void *)= 4 ; //所有的指针都是4Byte宽度.
sizeof(float) =4;sizeof(double) = 8;
表达式的求类型宽度是以表达式结果类型为准
int aa; sizeof(aa) = 4;
如果是常量表达式,则有一个默认类型转换问题
所有整数常量默认转换为 整数, 所以sizeof(20) = 4
所有小数默认为转换为 double型,所以sizeof(20.1) = 8
不同体系结构的sizeof取值
对于不同字长的CPU和操作系统,用sizeof()对同一类型的取值有不同结果.特别是在嵌入式开发领域,经常要面对不同环境.因此在不同环境下同一类型宽度取值,必须有所了解.
除了上节的32位CPU的提示以外.以下在几种特殊环境的各种类型的取值
l 8位单片机
开发环境:Keil μVision3 v3.53
CPU: 基于8051的AtmelAT89S52
单片机无法直接对类型进行sizeof操作,因此采用第二种操作,即对变量进行sizeof操作
sizeof (char) = 1
sizeof (short) =2
sizeof (int) = 2
sizeof (long) = 4
sizeof(float) = 4
sizeof(double) = 4
sizeof(char*) = 3 //实测结果
l 64位CPU
C 编程语言并没有提供一种机制来添加新的基本数据类型,因此在新的CPU下,C语言只能修改相应基本数据类型(即修改long之类数据宽度).或者添加新的基本类型(如增加long long类型)
. C/C++仅仅定义了这些基本数据类型之间的关系,并没有定义严格定义它们的字长。在不同的平台上,根据编译器不同的实现,各种基本类型(integer,Longpointer)有不同字长宽度.
各个产商的编译器根据自己的需要选择不同字长模式.现在已知的编译模型是
64位平台上的字长模型有 LP64,ILP64,LLP64
32位平台上的字长模型有ILP32和LP32。
字长模型里L指Long,I指Integer,P指Pointer. LP64表示在这个模式下,Long 和Pointer宽度为64bit,而integer为32bit,基余模式依次型推.
| LP32 | ILP32 | LP64 | LLP64 | ILP64 |
char | 8 | 8 | 8 | 8 | 8 |
short | 16 | 16 | 16 | 16 | 16 |
int | 16 | 32 | 32 | 32 | 64 |
long | 32 | 32 | 64 | 32 | 64 |
long long(int 64) | n/a | n/a | n/a | 64 | n/a |
指针(pointer) | 32 | 32 | 64 | 64 | 64 |
64 bitUnix/Linux 采用LP64模型,即int仍为32bit,long和long long 为64位.
Win32 采用ILP32模型,即int,long,pointer 均为32bit
Win64 采用LLP64 模型,即 int,long 为32bit , int64,pointer为64bit
不同模型下,均有如下规则:
sizeof(char) <=sizeof(short) <= sizeof(int) <= sizeof(long) = sizeof(size_t)
LP64,LLP64的函数返回值均为64bit
静态/动态空间
对于静态数组,sizeof取值的其静态空间的长度
char ary[100] ; sizeof(ary)=100*sizeof(char)
对动态分配的空间,结果值实际上是指针变量,因此指针变量在32位CPU下是4
char *p=malloc(100); sizeof(p)=4
字符串的情况有一些特殊.在初始化时,除了字符串本身内容以外,还要包括最后一个0字符的空间
charary[]=”hello”;sizeof(ary)=6 ;
char ary[]=”hello”表示静态分配相应空间,即hello 5个字符+最后结束符
char * ary=”hello”; 这个ary是一个指向一个常量地址的空间的指针,因此实际上是一个地址值,所以sizeof(ary)= 4;
指针
指针的重点并不在于sizeof()本身,而在于识别出复杂的指针类型.
复杂的指针定义里,函数指针,指向数组的指针和指针数组的定义大同小异.很容易错误识别.这样也造成sizeof()的错误.
当然,从软件工程角度来说,任何复杂,难懂的指针定义是严重不可取的,你有N种清晰,简单的方法达到同样的效果.所以这一些复杂指针,那他们只存在于面试题之中吧,不要在你程序中出现.不仅会让其它人很难理解和维护程序,甚至也让你本人过一段时间都看不懂你的自己写的程序了.
首先一个指针的宽度一样,无论它指向哪种类型,32位CPU下.
sizeof(void *)=sizeof(int *)=sizeof(char*)=sizeof(short *) = sizeof(long *)= 4;
l 函数指针
当然一个函数指针也是一样,函数指针变量定义的特点是指针变量和*要合并在一个括号,这个括号前是函数返回值,后面是参数定义(一定是带一对括号)
char (* func1)(int ,char *); 表示func1是一个函数指针,指向的函数定义 charfunc(int ,char *),这样sizeof(func1)= 4;
函数指针定义这样写主要为不和返回值为指针的函数声明冲突.上述定义没有括号含义就变了,表示声明一个返回值为char *的函数func1
char * func1(int ,char *);
当然,如果你强行对这个func1取sizeof(),它的值也是 4,因为函数名相当于一个地址常量.
l 数组指针
指向一个数组的指针的格式与函数指针定义很相似,数组指针名和*要合在一个括号里,括号前是数组类型.括号后是用[]表示数组维数
char (* p)[10]; 表示是指向一个一维字符数组的指针,这个数组的维数是10,所以sizeof仍然是4,因为它仍然是一个指针变量.(sizeof(p)=4)
用括号也是为了防止数组指针定义与指针数组的定义冲突.象把上述定义的括号去掉,含义完全变了.表示一个一维数组,数组项是char *,维数是 10,
char * p[10];
这个按照数组的计算法则,sizeof(p)=sizeof(char *)*10=40;
l 混和定义
混和定义就是把指针数组,函数指针,数组指针全部合在一起定义,这样面试题你完全可以拒绝作答,完全是茴香豆的茴字的几种写法的问题.当然你想挑战一下自已,你可试着算一下下面的定义
char (*(*x())[])()
x右边是()是函数,右边是*是函数指针,再到右边[],返回的是数组,再看左边是指针,指针数组。再看右边()函数,左边是char,返回值。
总的来说是一个返回值是返回char型的指针函数数组的函数指针。
char (*(*x[3])())[5]
定义一个包含3个返回数组指针的函数指针的数组
char*(*(**(*(*(*x[5])(int,float))[][12])(double))(short,long))[][173]
上面sizeof(x)是多少?
复合数据类型
复合数据类型一般是指结构或联合.
l 结构的求sizeof基本原则是各成员总尺寸之和.联合的sizeof值是最大成员的尺寸
如下结构
struct aa{ int a; short b; short c; };sizeof(struct aa)=8;
如下联合
union bb{ int a;short b,char c;}; sizeof(union bb)=4;
因为最大联合成员尺寸是int a,即4
l 空结构(即没有任何成员的结构)的sizeof=1,因为编译器要保存保证结构体的每一个实例在内存中都有独一无二的地址。,即便是一个空结构.编译器也是自动分配一个字节空间上来
struct cc{}; sizeof(struct cc)=1;
但是结构或联合牵涉字节对齐的问题.这样会造成不同宽度的字节对齐造成不同结果.关于什么叫字节对齐和采用字节对齐的原因,请参见相关章节.
以下均是在32位CPU下,基于4对齐的结果.
l 复合结构字节对齐有如下原则:字节对齐取宽度最大成员,如成员包含int,long则对齐采用4,如果最大数据宽度是short,则字节对齐采用2,如果只包含char,则字节对齐采用1.
如下定义: struct aa{ int a;char b;short c;} ;sizeof(struct aa) = 8;
最大长度成员是int ,采用4对齐,a占用四, short b+char c= 3不足4,编译器会在b,c之间补齐一个字符,补成4个字节,因此最终结果是sizeof(int)+sizeof(char)+pading+sizeof(short)=8
将上述定义换一下成员位置
structB{char b ;int a;short c;}; sizeof(struct B)=12;
最大长度成员是int ,采用4对齐,char b占一个位置,后续的int a 如果紧贴前面定义,将不能对齐,所以补3个字节,而后面的short c 占两个字节,也补成4个字节对齐
struct C{ char a[20];short b; }; sizeof(C ) =22;注意这里最大尺寸是short,因此采用2对齐,所以总尺寸是 22.
位域
位域是C语言定义一种特殊用法,即可以把某一种数据类型中的几位取出来使用,通用常用底层编程.并出现在结构定义当中.
位域的优点是定义清晰,操作某一位方便.在底层设置寄存器或者在网络包设置数据位时方便和清晰易懂.但一般在编程已经不太推荐使用,因为从某一个数中定义几位位域,牵涉到数字字节序的问题.同样位置的定义,要在不同字节序下定义二次.
以下是一些位域的定义,有两种定义方法
l struct bs
{int a:7;int b:2;int c:1;int d};
上述是连续定义同一类型位域
l struct bs2
{ int a:7,
b:2,
c:1;
int d;
};
在同一数据里分别定义多个位域,每个用,隔开.
位域的sizeof()结果值有几种计算方法
l 所有同一类位域定义之和没有超过这种类型最大尺寸,则分配时,要分配整个数据类型的尺寸.
如 structb1{ int a;2 ;int c}; 虽然a只分配了2个位,但分配空间还是分配一个完整的int,这样sizeof(struct b1)=8;
Structb2{int a:1;int b:2:int c:4;int a}; 这里a,b,c虽然是分独立声明三个位域,但是是同一类型,并且总和并没有超过32,因此这三个合并成一个int 分配,这样
sizeof(struct b2)=8;
l 如果位域之和超过数据宽度总和,则自动多分配一个数据空间
struct b3{ int a:31;int b:4;int c} ;
sizeof(b3) = 12; 因a+b的宽度超过32bit,编译器会多分配一个空间来容纳多余的位.
要提一句,后面几个情况是属于非常少见的情况,一般应用根本不推荐这样复杂的位域.一般即便是不用完一个数据空间,也要完整用的完,即形式始下
struct b4{int a:3,resver:29; int c};
而且在位域的在内存的分布,似乎不是标准C的定义内容,因此即便在同一操作系统,不同编译器有可以能产生不同结果
如下列定义中,用VC++ 6.0和DevCPP计算结果不一样(待求证,有做过测试通知我一声)
struct aa{
int a:1;
int b:1;
};
VC++ 6.0把a,b和空间合并到一个int中,因此结果是4,DevCpp是把a,b分成两个int,结果是8,(看资料得出,待求证)