[C++]深入理解sizeof-使用规则及陷阱分析

说明:

sizeof在笔试面试的时候频频地出现,这也是对基础的一个考查。关于sizeof的文章很多,但感觉大家都没有好好总结下,本着“先行先赢”和“为人民服务”的精神,查找引用参考了很多文章,在这里总结一下,有错误或者遗漏的地方还得请高手多多指教,也不要因这这些问题误导别人,希望以后大家在学习的过程中也能节省些时间。

概要

sizeof是C语言的一种单目操作符(但有人也不这么以为,认为它是一种特殊的宏),如C语言的其他操作符++、--等。它并不是函数(这是必须的)。sizeof操作符以字节形式给出了其操作数的存储大小。操作数可以是一个表达式或括在括号内的类型名。操作数的存储大小由操作数的类型决定,简单的说其作用就是返回一个对象或者类型所占的内存字节数。

sizeof的使用方法

1、用于数据类型(包括自定义类型)

sizeof使用形式:sizeof(type) 
数据类型必须用括号括住。如sizeof(int)。

2、用于变量

sizeof使用形式:sizeof(var_name)或sizeof var_name

变量名可以不用括号括住。如sizeof (var_name),sizeof var_name等都是正确形式。带括号的用法更普遍,大多数程序员采用这种形式。

[注意]sizeof操作符不能用于函数类型,不完全类型或位字段。不完全类型指具有未知存储大小的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。 
如sizeof(max)若此时变量max定义为int max(),sizeof(char_v) 若此时char_v定义为char char_v [MAX]且MAX未知,sizeof(void)都不是正确形式。

sizeof深入理解及陷阱分析

数据类型的sizeof

(1)应用

  • 固有类型:

32位C++中的基本数据类型,也就char,short int(short),int,long int(long),float,double, long double 
大小分别是:1,2,4,4,4,8, 10。

  • 自定义类型:

typedef short WORD; 
typedef long DWORD;

(2)举例及陷阱

  • e.g. 1.1
// 32位机上int长度为4 
cout<<sizeof(int)<<endl;  
// == 操作符返回bool类型,相当于 cout<<sizeof(bool)<<endl; 
cout<<sizeof(1==2)<<endl;  

在编译阶段已经被翻译为:

cout<<4<<endl; 
cout<<1<<endl;
  • e.g. 1.2
int a = 0; 
cout<<sizeof(a=3)<<endl; 
cout<<a<<endl;

输出是4,0。而不是期望中的4,3。

就在于sizeof在编译阶段处理的特性。由于sizeof不能被编译成机器码,所以sizeof作用范围内,也就是()里面的内容也不能被编译,而是被替换成类型。=操作符返回左操作数的类型,所以a=3相当于int,而代码也被替换为:

int a = 0; 
cout<<4<<endl; 
cout<<a<<endl; 

所以,sizeof是不可能支持链式表达式的,这也是和一元操作符不一样的地方。

结论:不要把sizeof当成函数,也不要看作一元操作符,把他当成一个特殊的编译预处理。

  • e.g. 1.3
int i = 2; 
cout<<sizeof(i)<<endl; // sizeof(object)的用法,合理 
cout<<sizeof i<<endl; // sizeof object的用法,合理 
cout<<sizeof 2<<endl; // 2被解析成int类型的object, sizeof object的用法,合理 
cout<<sizeof(2)<<endl; // 2被解析成int类型的object, sizeof(object)的用法,合理 
cout<<sizeof(int)<<endl;// sizeof(typename)的用法,合理 
cout<<sizeof int<<endl; // 错误!对于操作符,一定要加()

可以看出,加()是永远正确的选择。

结论:不论sizeof要对谁取值,最好都加上()。

  • e.g. 1.4
cout<<sizeof(unsigned int) == sizeof(int)<<endl; // 相等,输出 1

unsigned影响的只是最高位bit的意义,数据长度不会被改变的。

结论:unsigned不能影响sizeof的取值。

  • e.g. 1.5
typedef short WORD; 
typedef long DWORD; 
cout<<(sizeof(short) == sizeof(WORD))<<endl; // 相等,输出1 
cout<<(sizeof(long) == sizeof(DWORD))<<endl; // 相等,输出1

结论:自定义类型的sizeof取值等同于它的类型原形。

  • e.g. 1.6
cout << sizeof(2 + 3.14) << endl;

3.14的类型为double,2也会被提升成double类型,所以等价于 sizeof( double );

所以最后的结果是:8

 

函数类型的sizeof

sizeof也可以对一个函数调用求值,其结果是函数返回类型的大小,函数并不会被调用,并且返回值不能是void(当然也就是没有返回值)。

C99标准规定,函数、不能确定类型的表达式以及位域(bit-field)成员不能被计算sizeof值。

  • e.g. 2.1
int f1(){return 0;}; 
double f2(){return 0.0;} 
void f3(){} 
cout<<sizeof(f1())<<endl; // f1()返回值为int,因此被认为是int 
cout<<sizeof(f2())<<endl; // f2()返回值为double,因此被认为是double 
cout<<sizeof(f3())<<endl; // 错误!无法对void类型使用sizeof 
cout<<sizeof(f1)<<endl;  // 错误!无法对函数指针使用sizeof  

 

指针类型的sizeof

学过数据结构的你应该知道指针是一个很重要的概念,它记录了另一个对象的地址。既然是来存放地址的,那么它当然等于计算机内部地址总线的宽度。所以在32位计算机中,一个指针变量的返回值必定是4(注意结果是以字节为单位),可以预计,在将来的64位系统中指针变量的sizeof结果为8。

  • e.g. 3.1
char* pc = "abc";    
int* pi;    
string* ps;    
char** ppc = &pc;    
void (*pf)();// 函数指针  
   
sizeof( pc ); // 结果为4    
sizeof( pi ); // 结果为4    
sizeof( ps ); // 结果为4    
sizeof( ppc ); // 结果为4    
sizeof( pf );// 结果为4 

可以看到,不管是什么类型的指针,大小都是4的,因为指针就是32位的物理地址。

结论:只要是指针,大小就是4。(64位机上要变成8也不一定)。

 

数组类型的sizeof

数组类型的sizeof常常就和char*, char[],这些东西在一起就会误导人,这个到最后再讨论,先从例子看起。

  • e.g. 4.1
char a[] = "abcdef"; 
int b[20] = {3, 4}; 
char c[2][3] = {"aa", "bb"}; 
cout<<sizeof(a)<<endl; // 7 
cout<<sizeof(b)<<endl; // 80 
cout<<sizeof(c)<<endl; // 6

数组a的大小在定义时未指定,编译时给它分配的空间是按照初始化的值确定的,也就是7,注意:在最后还有一个“\0”也算一位。所以是6+1=7。

本人在VS2005上得到b的结果是80,可以看出,数组的大小就是他在编译时被分配的空间,也就是各维数的乘积*数组元素的大小。

c是多维数组,占用的空间大小是各维数的乘积,也就是6。

结论:数组的大小是各维数的乘积*数组元素的大小。

  • e.g. 4.2
int *d = new int[10]; 
cout<<sizeof(d)<<endl; // 4

d是我们常说的动态数组,但是他实质上还是一个指针,所以sizeof(d)的值是4。

  • e.g. 4.3
double* (*a)[3][6]; 
cout<<sizeof(a)<<endl;  // 4 
cout<<sizeof(*a)<<endl;  // 72 
cout<<sizeof(**a)<<endl; // 24 
cout<<sizeof(***a)<<endl; // 4 
cout<<sizeof(****a)<<endl; // 8

这个相对麻烦些,得先了解下数组指针和指针数组的知识。

a是一个很奇怪的定义,他表示一个指向 double*[3][6]类型数组的指针。既然是指针,所以sizeof(a)就是4。

既然a是执行double*[3][6]类型的指针,*a就表示一个double*[3][6]的多维数组类型,因此sizeof(*a)=3*6*sizeof(double*)=72。

同样的,**a表示一个double*[6]类型的数组,所以sizeof(**a)=6*sizeof(double*)=24。

***a就表示其中的一个元素,也就是double*了,所以sizeof(***a)=4。

至于****a,就是一个double了,所以sizeof(****a)=sizeof(double)=8。

 

向函数传递数组的sizeof

首先,我们要明确数组作为参数被传给函数时传的是指针而不是数组,传递的是数组的首地址。也可以说是它退化成了一个指针。

  • e.g. 5.1
#include <iostream> 
using namespace std; 
int Sum(int i[]) 
{ 
int sumofi = 0; 
for (int j = 0; j < sizeof(i)/sizeof(int); j++) //实际上,sizeof(i) = 4 
{ 
  sumofi += i[j]; 
} 
return sumofi; 
} 
int main() 
{ 
int allAges[6] = {21, 22, 22, 19, 34, 12}; 
cout<<Sum(allAges)<<endl; 
system("pause"); 
return 0; 
}

Sum的本意是用sizeof得到数组的大小,然后求和。但是实际上,传入自函数Sum的,只是一个int 类型的指针,所以sizeof(i)=4,而不是24,所以会产生错误的结果。解决这个问题的方法使是用指针或者引用。

使用指针的情况:

int Sum(int (*i)[6]) 
{ 
int sumofi = 0; 
for (int j = 0; j < sizeof(*i)/sizeof(int); j++) //sizeof(*i) = 24 
{ 
  sumofi += (*i)[j]; 
} 
return sumofi; 
} 
int main() 
{ 
int allAges[] = {21, 22, 22, 19, 34, 12}; 
cout<<Sum(&allAges)<<endl; 
system("pause"); 
return 0; 
}

在这个Sum里,i是一个指向i[6]类型的指针,注意,这里不能用int Sum(int (*i)[])声明函数,而是必须指明要传入的数组的大小,不然sizeof(*i)无法计算。但是在这种情况下,再通过sizeof来计算数组大小已经没有意义了,因为此时大小是指定为6的。

使用引用的情况和指针相似:

int Sum(int (&i)[6]) 
{ 
int sumofi = 0; 
for (int j = 0; j < sizeof(i)/sizeof(int); j++) 
{ 
  sumofi += i[j]; 
} 
return sumofi; 
} 
int main() 
{ 
int allAges[] = {21, 22, 22, 19, 34, 12}; 
cout<<Sum(allAges)<<endl; 
system("pause"); 
return 0; 
}

这种情况下sizeof的计算同样无意义,所以用数组做参数,而且需要遍历的时候,函数应该有一个参数来说明数组的大小,而数组的大小在数组定义的作用域内通过sizeof求值。

因此上面的函数正确形式应该是:

#include <iostream> 
using namespace std; 
int Sum(int *i, unsigned int n) 
{ 
int sumofi = 0; 
for (int j = 0; j < n; j++) 
{ 
  sumofi += i[j]; 
} 
return sumofi; 
} 
int main() 
{ 
int allAges[] = {21, 22, 22, 19, 34, 12}; 
cout<<Sum(i, sizeof(allAges)/sizeof(int))<<endl; 
system("pause"); 
return 0; 
}
结构体的sizeof

结构体的sizeof相对还是麻烦一些,写了一篇专门的文章仅供参考:

链接:http://pppboy.blog.163.com/blog/static/30203796201082494026399/

类的sizeof

[提示]这个可以看看孙鑫的那个《深入浅出MFC》的第一章的那个C++基础部分,就有这个类的大小的一个例子很不错。

先从例子看起吧。

关于虚继承也有个总结:

http://pppboy.blog.163.com/blog/static/30203796201041005953744/

  • e.g. 7.1
    //空类,相当于一个空结构体 
    class A 
    { 
    }; 
    //和结构体相同 
    class B  
    { 
        int a; 
        double b; 
    }; 
    //有一个非虚函数 
    class C 
    { 
        int a; 
        double b; 
         
        void f(){} 
    }; 
    //有1个虚函数 
    class D 
    { 
        int a; 
        double b; 
        virtual void vf(){} 
    }; 
     
    //有2个虚函数 
    class E 
    { 
        int a; 
        double b; 
        virtual void vf(){} 
        virtual void vf2(){} 
    }; 
    cout << "A: " << sizeof(A) << endl; 
    cout << "B: " <<  sizeof(B) << endl; 
    cout << "C: " <<  sizeof(C) << endl; 
     cout << "D: " <<  sizeof(D) << endl; 
     cout << "E: " <<  sizeof(E) << endl;

在8字节对齐的情况下结果为:

A: 1 
B: 16 
C: 16 
D: 24 
E: 24 
请按任意键继续. . .

可见:

(1)类的大小和结构体大小一样

(2)非虚拟成员函数不占大小

(3)如果有虚拟函数,则会多占用4个字节(虚拟函数表)

  • e.g. 7.2
    class A 
    { 
        int a; 
        double b; 
        void f(){} 
        virtual void vf(){} 
    }; 
    class B :public A 
    { 
    }; 
    class C : public virtual A 
    { 
    }; 
    class D  
    { 
        char a; 
    }; 
    class E  
    { 
        char a; 
        virtual void vfd(){} 
    }; 
     
   
    class F1 : public A, public D 
    { 
    }; 
    class F2 : public A, public E 
    { 
    }; 
    class G1 : public virtual A, public virtual D 
    { 
    }; 
    class G2 : public virtual A, public virtual E 
    { 
    }; 
    cout << "A: " << sizeof(A) << endl; 
    cout << "B: " <<  sizeof(B) << endl; 
    cout << "C: " <<  sizeof(C) << endl; 
     cout << "D: " <<  sizeof(D) << endl; 
     cout << "E: " <<  sizeof(E) << endl; 
    cout << "F1: " <<  sizeof(F1) << endl; 
    cout << "F2: " <<  sizeof(F2) << endl; 
    cout << "G1: " <<  sizeof(G1) << endl; 
    cout << "G2: " <<  sizeof(G2) << endl;

在8字节对齐的情况下,结果为:

A: 24 
B: 24 
C: 32 
D: 1 
E: 8 
F1: 32 
F2: 32 
G1: 33 
G2: 40 
请按任意键继续. . .

结论:

(1)在继承的时候,单一继承和多重继承的大小一样,也就相当于子类有一个自己的虚函数表,这个虚函数表是自己重新定义的,相当于它把父亲的虚函数表改成自己的,并且只有一个。

(2)在有虚继承时,涉及虚指针的问题,也就是说,他会用父类的虚函数表,如果父类没有,那么他也就没有这个父类的虚函数表,相当于他不具有自己独立的虚函数表,而是用它父亲的,有几个父亲有表,那么它都可以随时用它父亲的,也算是他自己的大小。

  • e.g. 7.3

父类指针指向子类的问题

    class Base 
    { 
        int a; 
        virtual void vf(){}; 
    }; 
    class Derived : public Base 
    { 
        char a; 
    }; 
    Base* pBase = new Derived; 
    Derived* pDerived = new Derived; 
    cout << "Base:     " << sizeof(Base) << endl;//8 
    cout << "Derived:  " << sizeof(Derived) << endl;//12 
    cout << "pBase:    " << sizeof(pBase) << endl;//指针大小都为4 
    cout << "*pBase:   " << sizeof(*pBase) << endl;//8 
    cout << "pDerived: " << sizeof(pDerived) << endl;//指针大小都为4 
    cout << "*pDerived:" << sizeof(*pDerived) << endl;//12

//在8字节对齐时的结果为:

Base:     8 
Derived:  12 
pBase:    4 
*pBase:   8 
pDerived: 4 
*pDerived:12 
请按任意键继续. . .

结论:sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸。它返回的只是类型的大小(可以这么理解)

 

字符串的sizeof和strlen

这个在实际的应用中是最容易忽悠人的,应该好好滴留意一下。

  • e.g. 8.1
char a[] = "abcdef"; 
char b[20] = "abcdef"; 
string s = "abcdef"; 
cout<<strlen(a)<<endl;  // 6,字符串长度 
cout<<sizeof(a)<<endl;  // 7,字符串容量 
cout<<strlen(b)<<endl;  // 6,字符串长度 
cout<<strlen(b)<<endl;  // 20,字符串容量 
cout<<sizeof(s)<<endl;  // 12, 这里不代表字符串的长度,而是string类的大小 
cout<<strlen(s)<<endl;  // 错误!s不是一个字符指针。 
a[1] = '\0'; 
cout<<strlen(a)<<endl;  // 1 
cout<<sizeof(a)<<endl;  // 7,sizeof是恒定的

strlen是寻找从指定地址开始,到出现的第一个0之间的字符个数,他是在运行阶段执行的

而sizeof是得到数据的大小,在这里是得到字符串的容量。所以对同一个对象而言,sizeof的值是恒定的。

string是C++类型的字符串,他是一个类,所以sizeof(s)表示的并不是字符串的长度,而是类string的大小。strlen(s)根本就是错误的,因为strlen的参数是一个字符指针,如果想用strlen得到s字符串的长度,应该使用sizeof(s.c_str()),因为string的成员函数c_str()返回的是字符串的首地址。实际上,string类提供了自己的成员函数来得到字符串的容量和长度,分别是Capacity()和Length()。string封装了常用了字符串操作,所以在C++开发过程中,最好使用string代替C类型的字符串。

 

下面引用VCbase里的内容再探讨一下。

(1)sizeof是算符,strlen是函数。

(2)sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''\0''结尾的。sizeof还可以用函数做参数

short f(); 
printf("%d\n", sizeof(f()));

(3)数组做sizeof的参数不退化,传递给strlen就退化为指针了。

char var[10]; 
    int test(char var[]) 
    { 
        return sizeof(var); 
    }

var[]等价于*var,已经退化成一个指针,所以大小是4.

(4)大部分编译程序 在编译的时候就把sizeof计算过了 是类型或是变量的长度这就是sizeof(x)可以用来定义数组维数的原因

char str[20]="0123456789"; 
int a=strlen(str); //a=10; 
int b=sizeof(str); //而b=20;

(5)strlen的结果要在运行的时候才能计算出来,时用来计算字符串的长度,不是类型占内存的大小。

(6)sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。这是因为sizeof是个操作符不是个函数。

(7)当适用了于一个结构类型时或变量, sizeof 返回实际的大小, 当适用一静态地空间数组, sizeof 归还全部数组的尺寸。 sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸

(8)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,如:

fun(char [8]) 
fun(char []) 

都等价于 fun(char *) 在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小如果想在函数内知道数组的大小, 需要这样做:进入函数后用memcpy拷贝出来,长度由另一个形参传进去.

fun(unsiged char *p1, int len) 
{ 
  unsigned char* buf = new unsigned char[len+1] 
  memcpy(buf, p1, len); 
} 

//看来这个《程序员面试宝典》也是抄这段文字了。。。

 

和我一起做题目

容易迷糊的几个问题
  • e.g. 1.1
char* ss = "0123456789";

sizeof(ss) 结果 4  ss是指向字符串常量的字符指针 
sizeof(*ss) 结果 1  *ss是第一个字符

  • e.g. 1.2
char ss[] = "0123456789";

sizeof(ss) 结果 11  ss是数组,计算到\0位置,因此是10+1 
sizeof(*ss) 结果 1 *ss是第一个字符

  • e.g. 1.3
char ss[100] = "0123456789";

sizeof(ss) 结果是100, ss表示在内存中的大小 100×1 
strlen(ss) 结果是10,strlen是个函数内部实现是用一个循环计算到\0为止之前

  • e.g. 1.4
int ss[100] = "0123456789";

sizeof(ss) 结果 400 ,ss表示再内存中的大小 100×4 
strlen(ss) 错误 ,strlen的参数只能是char* 且必须是以''\0''结尾的

  • e.g. 1.5
char q[]="abc"; 
char p[]="a\n"; 
sizeof(q),sizeof(p),strlen(q),strlen(p);

结果是 4 3 3 2

这个容易迷糊,好好看清楚。。注意sizeof(p)是“\n”算一位,而strlen(p)在“\n”的时候就已经结束

e.g. 1.6

char szPath[MAX_PATH]

如果在函数内这样定义,那么sizeof(szPath)将会是MAX_PATH,但是将szPath作为虚参声明时(void fun(char szPath[MAX_PATH])),sizeof(szPath)却会是4(指针大小)

还是退化成指针的问题

 

网上找到的几个牛B点的
  • e.g. 2.1
1. sizeof(char) =                         
2. sizeof 'a'   =                            
3. sizeof "a"   =                         
4. strlen("a")) =

答案:1,1,2,1(说明:原来作者给的答案是1,4,2,1。但第二个应该是1而不是4)

  • e.g. 2.2
short (*ptr[100])[200]; 
1. sizeof(ptr)           = 
2. sizeof(ptr[0])        = 
3. sizeof(*ptr[0])       = 
4. sizeof((*ptr[0])[0])) =  

答案:400,4,400,2

这里我们定义了一个100个指针数组,每个指针均指向有200个元素的数组,其内存占用为200*sizeof(short)字节。那么这100个数组指针的大小sizeof(ptr)为100*sizeof(short*)。接着,指针数组的第一个指针ptr[0]指向第一个数组,所以这个指针ptr[0]的大小实际上就是一个普通指针的大小,即sizeof(short*)。*ptr[0]指向第一个数组的起始地址,所以sizeof(*ptr[0])实际上求的是第一个组的内存大小200*sizeof(short)。(*ptr[0])[0])是第一个数组的第一个元素,因为是short型,所以这个元素的大小sizeof((*ptr[0])[0]))等价于sizeof(short)。

e.g. 2.3

#include <stdio.h> 
#pragma pack(push) 
#pragma pack(2) 
typedef struct _fruit 
{ 
  char          apple; 
  int           banana; 
  short         orange;    
  double        watermelon; 
  unsigned int  plum:5; 
  unsigned int  peach:28;  
  char*         tomato; 
  struct fruit* next;      
} fruit; 
#pragma pack(4) 
   
typedef struct _fruit2 
{ 
  char           apple; 
  int            banana;    
  short          orange; 
  double         watermelon; 
  unsigned int   plum:5; 
  unsigned int   peach:28;    
  char*          tomato; 
  struct fruit2* next;      
} fruit2;   
#pragma pack(pop) 
int main(int argc, char *argv[]) 
{ 
  printf("fruit=%d,fruit2=%d\n",sizeof(fruit),sizeof(fruit2)); 
} 

答案:fruit=30,fruit2=36

听听作者怎么说:

如果你回答错误,那么你对数据结构的对齐还没有吃透。这里#pragma pack(2)强制设置编译器对齐属性为2,所以第一个数据结构以2对齐,sizeof(fruit)=(sizeof(apple)+1)+sizeof(banana)+sizeof(orange)+sizeof(watermelon)+((plum:5bit+peach:28bit+15bit)/8bit)+sizeof(tomato)+sizeof(next)(注意式子中1 和 15bit 表示补齐内存,使其以2对齐,),既sizeof(fruit)=(sizeof(char)+1)+sizeof(int)+sizeof(short)+sizeof(double)+sizeof(char*)+sizeof(struct fruit*)。第一个数据结构声明完了之后,又使用#pragma pack(4)强制设置编译器对齐属性为4,所以同理,可以得到sizeof(fruit2)=(sizeof(char)+3)+sizeof(int)+(sizeof(short)+2)+sizeof(double)+((5bit+28bit+31bit)/8bit)+sizeof(char*)+sizeof(struct fruit2*)。

注:#pragma pack(push)保存默认对齐,#pragma pack(pop)恢复默认对齐。

(这个没有细细看,太累了,明天再看这个例子)

 

引用申明

有文章在引用了其它作者的很多文字,所有权都归原作者所有,这里仅供本人总结学习,下面给出参考过或者引用过的链接,谢谢作者。

1.http://edu.codepub.com/2009/1117/17745.php

2.http://dev.yesky.com/143/2563643.shtml

3.http://blog.chinaunix.net/u2/62684/showart_489736.html

4.http://www.vckbase.com/document/viewdoc/?id=1054

5.《程序员面试宝典》第二版 p52-p63

本文出处:
http://pppboy.blog.163.com/blog/static/302037962010825105652390/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章