c/c++之计算数据类型长度问题讲解(sizeof函数)
计算数据类型长度问题
在c/c++学习中,我们不可避免的会接触到数据存储问题,而计算机中用sizeof函数来计算数据存储需要的长度。
(1)基本数据类型
在32位编译器下
sizeof(char)结果为1 表示存储一个char类型变量需要1个字节大小的位置
sizeof( short ) 结果为2
sizeof(unsigned int ) 结果为4
sizeof( int ) 结果为4
sizeof( float ) 结果为4
sizeof( long) 结果为4
sizeof( unsigned long) 结果为4
sizeof( char */int */等指针变量) 结果为4
sizeof( double ) 结果为8
sizeof( long) 结果为4
sizeof(string)结果为32
需要注意的是sizeof(void)在有点编译器下结果为1,有的则编译不通过。
(2)单独函数所需要的存储长度
此时注意的是函数返回类型,无论函数里面包含了什么内容,sizeof(函数())返回大小为函数返回类型所需的大小。注意返回类型为void的情况。
int fun() {} sizeof(fun())=4
(3)数组大小计算
数组的sizeof值等于数组所占用的内存字节数,如:
char a1[]= "abc";
inta2[3];
sizeof(a1 ); // 结果为4,字符末尾还存在一个null终止符
sizeof(a2 ); // 结果为3*4=12(依赖于int)
一些朋友刚开始时把sizeof当作了求数组元素的个数,现在,你应该知道这是不对的。那么应该怎么求数组元素的个数呢?
easy,通常有下面两种写法:
int c1 =sizeof( a1 ) / sizeof( char ); // 总长度/单个元素的长度
int c2 =sizeof( a1 ) / sizeof( a1[0]); // 总长度/第一个元素的长度
写到这里,提一问,下面的c3,c4值应该是多少呢?
*********************************************************
voidfoo3(char a3[3])
{
int c3 = sizeof( a3 ); // c3 ==
}
voidfoo4(char a4[])
{
int c4 = sizeof( a4 ); // c4 ==
}
*********************************************************
也许当你试图回答c4的值时已经意识到c3答错了,是的,c3!=3。
这里函数参数a3已不再是数组类型,而是蜕变成指针。相当于char* a3,为什么仔细想想就不难明白。
我们调用函数foo1时,程序会在栈上分配一个大小为3的数组吗?不会!
数组是“传址”的,调用者只需将实参的地址传递过去,所以a3自然为指针类型(char*),c3的值也就为4。
(4)结构体
结构体的问题涉及到内存对齐的问题,我们需要好好了解一下:
struct s
{
char a;
int b;
double c;
}
sizeof(s)此时的大小为16,而不是1+4+8=13,这就是传说中的内存对齐问题。计算机组成原理教导我们,这样有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上。以此类推,这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
但是内存对齐到底是怎么一回事我们接下来慢慢了解:
字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
结构体大小的计算方法和步骤:
i. 将结构体内所有数据成员的长度值相加,记为 sum_a ;
ii. 将各数据成员为了内存对齐,按各自对齐模数而填充的字节数累加到sum_a上,记为sum_b。
对齐模数是 #pragma pack 指定的数值与该数据成员自身长度相比较得到的数值较小者。该数据相对起始位置应该是对齐模数的整数倍。
iii. 将和 sum_b 向结构体模数对齐。
该模数则是 #pragma pack 指定的数值与结构体内最大的基本数据类型成员长度相比较得到的数值较小者。结构体的长度应该是该模数的整数倍。
计算结构体所需内存大小需要分两种情况:
1) 在没有#pragmapack宏的情况下:
例子1:
内存分配状态为:
对于结构体的第一个成员 a,起始位置为0x…38 (也为 4 的倍数),所占内存为 0x…38 ~ 0x…3b,共占4个字节;
对于结构体的第二个成员 b,自身长度为1,对齐模数也为1,所以内存分配可以紧接着a的结尾位置 0x…3b,所以起始位置为 0x…3c,共占1个字节;
对于结构体的第三个成员 c,自身长度为2,对齐模数也为2,所以起始位置距离a 的起始位置应该是2的倍数,所以 0x…3d处只距离5,不符合要求,所以空着,继续往下找,而在 0x…3e处满足要求,所以可以作为c的起始位置,共占2个字节;
此时3个成员及其中间空着的位置所占内存单元总和为8,而结构体内最大的基本数据成员是 a,其长度为4,所以结构体模数为 4,而8是4的倍数,满足要求,故不再加内存。
例子2:
与例子1相比,三个类型的声明顺序变了:
内存分配状态为:
要注意的是,对 a而言,对齐模数为 4,所以当 b的起始位置在0x7f…830之后,0x7f…831、0x7f…832、0x7f…833的位置距离起始位置0x7f…830分别是1,2,3,都不是 4 的倍数,所以那三个位置都空着,直到0x7f…834才满足要求,所以作为 a 的起始位置。当最后一个成员 c 占的内存末尾在0x7f…839时,所有数据成员及其之间的空位所占内存单元总和为10,而结构体模数为4,10不是4的倍数,所以要扩大到12才满足要求,此时又多了2个空位置,就是0x7f…83a和0x7f…83b。
例子3:
当结构体中有数组时:
内存分配状态为:
亦即相同类型数据的数组之间多分配的空间会被相邻数组的元素所占用。
2) 在没有#pragmapack宏的情况下:
方法类似,只是模数可能会按上面说的规则而有所变化。
内存分配状态为:
注意,当没有#pragma pack(2)时,成员a要确定自身的q起始位置,是以自身的长度4为对齐模数,但有了#pragma pack(2),则将括号里的2与a的长度4比较,2为较小者,所以以2为a的对齐模数,即地址从0x7f…839往下找到0x7f…83a时,已经距离结构体的起始位置0x7f…838为2,是2的倍数,满足要求(虽然不是4的倍数),可以作为a的起始位置。而最后,所有数据成员及其之间的空位所占内存单元总和为8,因为2和4(结构体中最大的数据成员长度)的较小者为2,而8是2的倍数,所以刚好满足要求,不用在分配空位置,所以结构体总长度即为8。
(5)类的存储大小计算
类的sizeof值等于类中成员变量所占用的内存字节数。如:
****************************************************************
class a
{
public:
int b;
float c;
char d;
};
int main(void)
{
a object;
cout << "sizeof(object)is " << sizeof(object) << endl;
return 0 ;
}
***************************************************************
输出结果为12(我的机器上sizeof(float)值为4,字节对其前面已经讲过)。
不过需要注意的是,如果类中存在静态成员变量,结果又会是什么样子呢?
***************************************************************
class a
{
public:
static int a;
int b;
float c;
char d;
};
int main()
{
a object;
cout << "sizeof(object) is " << sizeof(object)<< endl;
return 0 ;
}
**************************************************************
16?不对。结果仍然是12.
因为在程序编译期间,就已经为static变量在静态存储区域分配了内存空间,并且这块内存在程序的整个运行期间都存在。
而每次声明了类a的一个对象的时候,为该对象在堆上,根据对象的大小分配内存。
如果类a中包含成员函数,那么又会是怎样的情况呢?看下面的例子
*************************************************************
class a
{
public:
static int a;
int b;
float c;
char d;
int add(int x,int y)
{
return x+y;
}
};
int main()
{
a object;
cout << "sizeof(object)is " << sizeof(object) << endl;
b = object.add(3,4);
cout << "sizeof(object)is " << sizeof(object) << endl;
return 0 ;
}
***************************************************************
结果仍为12。
因为只有非静态类成员变量在新生成一个object的时候才需要自己的副本。
所以每个非静态成员变量在生成新object需要内存,而function是不需要的。
注:c++中的多态和虚继承也是非常重要的东西,不过比较复杂,编译器不同,细节也有所不同。
/////////////////////////////////////////////////////////////////////////////////////////////////
如下一段代码:
#pragma pack(4)
class testb
{
public:
int aa;
char a;
short b;
char c;
};
int nsize = sizeof(testb);
这里nsize结果为12,在预料之中。
现在去掉第一个成员变量为如下代码:
#pragma pack(4)
class testc
{
public:
char a;
short b;
char c;
};
int nsize = sizeof(testc);
按照正常的填充方式nsize的结果应该是8,为什么结果显示nsize为6呢?
事实上,很多人对#pragma pack的理解是错误的。
#pragma pack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragmapack指定值之间,较小的那个进行。
具体解释
#pragma pack(4)
class testb
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以
这个成员按一字节对齐,放在偏移[4]的位置。
short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以
放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};
这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(testb)是12。
如果
#pragma pack(2)
class testb
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以
这个成员按一字节对齐,放在偏移[4]的位置。
short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以
放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};
//可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。所以 sizeof(testb)是10。
最后看原贴:
现在去掉第一个成员变量为如下代码:
#pragma pack(4)
class testc
{
public:
char a;//第一个成员,放在[0]偏移的位置,
short b;//第二个成员,自身长2,#pragmapack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
char c;//第三个,自身长为1,放在[4]的位置。
};
//整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果
是6所以sizeof(testc)是6。