2
3

c/c++计算数据类型长度问题讲解(sizeof函数)

---恢复内容开始---

-----------------------计算数据类型长度问题---------------------------------------

在c/c++学习中,我们不可避免的会接触到数据存储问题,而计算机中用sizeof函数来计算数据存储需要的长度。

(1)基本数据类型

基本数据类型在不同编译器中的大小
  32位 64位 是否变化
bool 1    
char 1 1 没有变化
* 4 8 变化
short int  2 2 没有变化
int 4 4 没有变化
unsigned int 4 4 没有变化
float 4 4 没有变化
double 8 8 没有变化
long 4 8 变化
unsigned long 4 8 变化
long long 8 8 没有变化
string 32    
void 1 1 没有变化

除了*与long随操作系统子长变化而变化外。其它的都固定不变(32位和64相比)

bool 1个字节  char 1个字节      int 4个字节   float 4个字节       doubl 8个字节   long long 8个字节

oc中:

64位系统下(本机不是32位系统的没法測试):

nsstring    8位

nsinteger  8位(有符号的)

NSUInteger  8位(无符号的,没有负数)

在oc中非常多变量其实是指针,所以64位系统下非常多都是8位的。

顺便提下nsnumber,NSInteger是基础类型,可是NSNumber是一个类。假设想要在NSMutableArray或者

NSMutableDictionary里存储一个数值,直接用NSInteger是不行的。要先转换成nsnumber类。

需要注意的是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值应该是多少呢?

*********************************************************

void foo3(char a3[3])

{

int c3 = sizeof( a3 ); // c3 ==4

}

void foo4(char a4[])

{

int c4 = sizeof( a4 ); // c4 ==4

}

*********************************************************

也许当你试图回答c4的值时已经意识到c3答错了,是的,c3!=3。

这里函数参数a3已不再是数组类型,而是蜕变成指针。相当于char* a3,为什么仔细想想就不难明白。

我们调用函数foo3时,程序会在栈上分配一个大小为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,字节对其前面已经讲过)。

注意:

1、类的对象(即实例)所占用的空间大小只取决于该对象中数据成员所占用的空间,而与成员函数无关。函数的代码是存储在对象空间之外的。

2、不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储,即都不占用对象的存储空间。

3、不论是否用inline(内置)声明函数,成员函数的代码段都不占用对象的存储空间。用inline声明的作用是在调用该函数时,将函数的代码段复制插入到函数调用点,若不用inline声明,在调用该函数时,流程转去函数代码段的入口地址,在执行完该函数代码段后,流程返回函数调用点。inline与成员函数是否占用对象的存储空间无关,他们不属于同一个问题,不应该搞混。

不过需要注意的是,如果类中存在静态成员变量,结果又会是什么样子呢?

***************************************************************

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。

常见sizeof面试片段:

面试官:定义一个空的类型,里面没有任何成员变量和成员函数。对该类型求sizeof,得到的结果是多少?

应聘者:答案是1.

面试官:为什么不是0?

应聘者:空类型的实例中不包含任何信息,本来求sizeof应该是0,但是我们声明该类的实例的时候,他必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存由编译器决定。在Visual Studio中,每个空类型的实例占用1字节的空间。

面试官如果在该类型中添加一个构造函数和析构函数,再对该类型求sizeof,得到的结果又是多少?

应聘者:和前面一样,还是1.调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不因为这两个函数而在实例内添加任何额外的信息。

面试官:那如果析构函数标记为虚函数呢?

应聘者:C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4字节,因此求sizeof得到4;在64位的机器上,一个指针占8字节,因此求sizeof得到8.

posted @ 2018-12-06 21:09  华丽的双鱼  阅读(5937)  评论(0编辑  收藏  举报