概述
在学习C语言时,老师讲解sizeof时还以为就是一个普通的运算符,只是用来计算所占字节的,现在看来要么我当时在睡觉,要么我灵魂在出窍。在找工作笔试的过程中,总是会碰到关于sizeof用法的各种题目,碰到比较难度比较大的题目时,曾经一度的怀疑人生。现在就把这个运算符的使用归纳一下,以免以后还怀疑人生。
概念及语法
定义:sizeof运算符返回一条表达式或一个类型名字所占的字节数。返回值是一个size_t类型的常量表达式。
根据其定义就能看出来,语法分为两种形式:
sizeof(type) 返回类型的大小
sizeof expr 返回表达式结果类型的大小
例如:
Sales_data data, *p;
sizeof(Sales_data); //存储Sales_data类型的对象所占的空间大小
sizeof data; //data的类型的大小,即sizeof(Sales_data)
sizeof p; //指针所占空间的大小
sizeof *p; //p所指类型的空间大小,即sizeof(Sales_data)
sizeof data.revenue; //Sales_data的revenue成员对应的类型大小
sizeof Sales_data::revenue; //另一种获取revenue大小的方式
通过最后一个例子可以看出,sizeof允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员但是sizeof运算符无须我们提供一个具体的对象。
注意:sizeof 并不实际计算其运算对象的值,例如:
int a = 0;
cout << sizeof(a = 3) << endl; //4
cout << a << endl; //0 sizeof并不实际计算
输出的结果为4 和 0。原因是C/C++是静态强类型语言,所有数据类型的占用空间大小都是确定的。同时sizeof的计算发生在编译器时刻,编译器知道类型的大小,所以sizeof计算的类型大小在编译器的时刻就知道了。回应了前面sizeof的定义,返回值是一个常量表达式。
C99标准规定,函数类型、不能确定类型的表达式以及位域成员不能被计算sizeof值。估计都看晕了,举例说明吧:
int foo()
{
retrun 1;
}
sizeof(foo); //错误,编译通不过
void foo2(){}
sizeof(foo2() ); //错误
struct S
{
unsigned int f1 : 1;
unsigned int f2 : 5;
unsigned int f3 : 12;
}
sizeof(S.f1); //错误
下面让我们从各个角度对sizeof进行解析,说明一下有时在不同位数的系统下,sizeof的计算会有差别,如果没有特别声明的话默认为32位系统。
1、基本数据类型的sizeof
基本数据类型是指char、short、int、float、double、long double这样的内置数据类型。
由于他们有的内存大小和系统有关,所以在不同的系统下取值可能不一样。
在32位和64位机器上对应的大小都是:1、2、4、4、8、8
但在16位机器上就有差别了。
bool值,在C++中属于内置类型,数值为true和false,sizeof大小为1。在C中不属于内置类型,数值为1和0,sizeof应该为4(我没有验证这个)。
至于整形的可用修饰词unsigned,它只影响最高位bit的意义,所占长度不会改变。
2、自定义数据类型
用typedef或using定义的自定义类型,例如
typedef short WORD;
using DWORD = long;
cout<<(sizeof(short) == sizeof(WORD))<<endl; // 相等,输出 1
cout<<(sizeof(long) == sizeof(DWORD))<<endl; // 相等,输出 1
所以自定义类型的sizeof取值等同于它的类型原形。
3、指针变量的sizeof
指针是存储单元的地址,sizeof指针变量就等于计算机内部地址总线的宽度。所以在32位计算机中,一个指针变量的返回值固定是4。同理在64位系统中,返回值必定为8。
例如,在32位系统下:
char* pstr = "Hello Word!";
int* pi = null;
string* ps = null;
char** ppc = &pc;
void (*pf)(); //函数指针
sizeof( pc ); //4
sizeof( pi ); //4
sizeof( ps ); //4
sizeof( ppc ); //4
sizeof( pf ); //4
4、函数的sizeof
sizeof对一个函数求值,其结果是函数返回值类型的大小,函数不会被调用。符合sizeof编译器定值,不参与运算的注意事项。
注意:
1)不可以对返回值类型为空的函数求值。对应不确定类型
2)不可以对函数名求值。对应函数类型
3)对有参数的函数,在用sizeof时,须写上实参表。
double FuncA(int a, double b)
{
return a+b;
}
int FuncB()
{
return 3;
}
void FuncC(){}
count << sizeof( FuncA( 2, 3.5f ) ) << endl; //8 实际函数并不会被调用
count << sizeof( FuncB() ) << endl; //4 实际函数并不会被调用
count << sizeof( FuncC() ) << endl; //编译通不过
5、数组的sizeof
对数组执行sizeof运算得到整个数组所占空间的大小,例如:
char arr[ ] = "abcdef";
int b[20] = {3,4};
char c[2][3] = {"aa,"bb"}
cout << sizeof(a) << endl; //7
cout << sizeof(b) << endl; //20
cout << sizeof(c) << endl; //6
所以有时可以得到数组元素的个数,写法也有好几种,但是意思是一样的:
int num = sizeof(b)/sizeof(*b);
int num = sizeof(b)/sizeof(b[0]);
int num = sizeof(b)/sizeof(int);
这里有几个经典的面试题:
void foo(char a[ 3 ] )
{
cout << sizeof(a) << endl; //4
}
void foo2(char a[ ] )
{
cout << sizeof(a) << endl; //4
}
这里函数参数a已不再是数组类型,隐式转换成了指针,相当于char* a。因为我们调用foo时,程序不会再栈上分配一个大小为3的数组,使用“传址”的方式,所以结果为4。
int* parr = new int [10];
cout << sizeof(parr) << endl; //4
parr是我们常说的动态数组,但它实际上还是个指针,所以返回值为4。
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)=36sizeof(double)=72 。
同样的, *a 表示一个 double[6] 类型的数组,所以 sizeof(**a)=6sizeof(double)=24 。
**a 就表示其中的一个元素,也就是 double 了,所以 sizeof(***a)=4 。
至于 ****a ,就是一个 double 了,所以 sizeof(****a)=sizeof(double)=8
6、 结构体(struct)的sizeof
struct的sizeof主要涉及到了对齐和补齐的概念所以有点复杂,首先让我们理解一下什么是对齐?
计算机为了加快计算机的取数速度,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),对一些变量的起始地址做了“对齐”处理。这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
字节对齐的细节和编译器的实现相关,但一般而言,满足三个准则:
1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
2. 结构体的每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节(internal adding)。
3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员后加上填充字节(trailing padding)。
先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。
光看概念也理解不了,让我们牢记概念然后看几个例子进行理解:
struct stu1
{
int ia; //类型大小为4
char cb; //类型大小为1
int ic; //类型大小为4
double dd; //类型大小为8
char ce; //类型大小为1
};
cout << sizeof(stu1) << endl; //32
通过观察我们可以看见最宽的基本类型(double)占8个字节。前面说了第1个成员的地址就是结构体的首地址,所以偏移量为0,整除8(准则一)。往后看第2个成员是偏移是第1个成员大小,这里为4符合准则二。然后看第3个成员的偏移是5(4+1),不符合准则二,则进行补位到8(4+1+补位3)。然后看第4个成员的偏移为12(4+1+补位3+4),不能整除自身8,则进行补位到16。然后看最后一个成员,偏移24(16+8)符合规则二。好了这样整体占25,但是不符合规则三,则进行补位到32。这样说有点抽象我画了下面的图,便于理解:
其中空着的就是需要进行补位的字节。
在看下面的例子:
struct stu2
{
char cm;
stu1 s;
int in;
};
cout << sizeof(stu2) << endl;
大家算一下正确答案是多少?这里牵扯到另一个知识点:
由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。
意思就是最宽类型成员从所有子成员中找为double,但是偏移地址时要把符合类型看成一个整体。所以结果应该是48。
到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同。#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐数,其取值为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof( item ) = min( n, sizeof( item ) )
再看示例:
#pragma pack(push) // 将当前pack设置压栈保存
#pragma pack(2) // 必须在结构体定义之前使用
struct S1
{
char c;
int i;
};
struct S3
{
char c1;
S1 s;
char c2;
};
#pragma pack(pop) // 恢复先前的pack设置
计算sizeof(S1)时,min(2, sizeof(i))的值为2,所以i的偏移量为2,加上sizeof(i)等于6,能够被2整除,所以整个S1的大小为6。同样,对于sizeof(S3),s的偏移量为2,c2的偏移量为8,加上sizeof(c2)等于9,不能被2整除,添加一个填充字节,所以sizeof(S3)等于10。
可查看之前总结的#pragma的详细介绍。
7、类(class)的sizeof
之前我整理过class与struct的区别,知道两者最主要的是防控属性的区别,其他都一样了,上面介绍的struct规则同样适用于class,而这里介绍类特性的也同样适应于C++中的struct。
类的sizeof有点复杂,让我边举例子边进行讲解吧:
1、空类的大小为1。空类型实例中不包含任何信息,应该大小为0. 但是当我们声明该类型的实例的时候,它必须在内存中占有一定独一无二的内存,否则无法使用这些实例。至于占用多少内存,由编译器决定。g++中每个空类型的实例占1字节空间。注意空struct即空类,这就是为什么c++的空struct占一个字节的原因。
2、构造函数、析构函数、成员函数调用时只需知道函数地址即可,而这些函数的地址与类型相关,而与具体的实例无关,因此不会在实例中额外添加任何信息。
3、 静态数据成员放在全局数据成员中,它不占类实例大小,多个类实例只有一个实体。可以看作是一种特殊的全局变量。
4、 类的非静态数据成员和c语言中的struct类似,也需要对齐,可能需要字节填充。
通过以上的四条我们看几个例子:
class A
{
public:
static int a; //静态成员
static char c; //静态成员
A(){};
~A(){};
void foo(){}; //成员函数
};
class B : public A //从A中继承
{
public:
......
char m_ca;
int m_ib;
char m_cc;
}
cout << sizeof(A) << endl; //只有静态成员和函数,相当于空类,求值为1
cout << sizeof(B) << endl; //符合上面讲的对齐原则,求值为12
5、 如果一个类中有虚函数,则该类型会生成一个虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,因此类大小必须加上一个指针所占的空间。如果是普通继承,子类和基类共享这个指针。
class C :public B
{
public:
.....
virtual void Func(); //只要有虚函数,就会生成一个虚函数表,添加一个指针指向这个表
};
cout << sizeof(C) << endl; //32位系统为16,64位系统为24
6、虚继承时,派生类会生成一个指向虚基类表的指针,占一个指针大小空间。
class D : virtual public C //这里会在创建一个指向虚函数基类的指针
{
public:
......
virtual void Func(); //这里不会增加指针了,原因是和基类共用一个
}
cout << sizeof(D) << endl; //32位系统为20,64位系统为32
8、含位域结构体的sizeof
前面已经说过,位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。
C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。
使用位域的主要目的是压缩存储,其大致规则为:
(1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
(2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
(3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
(4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
(5) 整个结构体的总大小为最宽基本类型成员大小的整数倍
例如:
struct BF1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
位域类型为char,第1个字节仅能容纳下f1和f2,所以f2被压缩到第1个字节中,而f3只能从下一个字节开始。因此sizeof(BF1)的结果为2。
示例2:
struct BF2
{
char f1 : 3;
short f2 : 4;
char f3 : 5;
};
由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2。
9、联合(union)的sizeof
结构体在内存组织上市顺序式的,联合体则是重叠式,各成员共享一段内存;所以整个联合体的sizeof也就是每个成员sizeof的最大值,比如:
union u
{
int a;
float b;
double c;
char d;
};
sizeof(u); //最大值为8
10、与strlen的比较
首先来看一下两者的概念:
sizeof是运算符,用来计算所占空间大小,其值在编译期间就计算好了,参数可以是数组、指针、类型、对象、函数等。
strlen(…)是函数,用来返回字符串的长度,其值在运行期计算,参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。字符串可以是自己定义的,也可能是内存中随机的,以“\0”结束就行
char arr[10] = "Hello";
int iLen = strlen(arr);
int iLen2 = sizeof(arr);
cout << iLen << " and " << iLen2 << endl;
arr数组求sizeof时,返回编译器为其分配的数组空间大小,不关心里面存了多少数据。而strlen关心存储的数据内容,不关心空间的大小和类型。输出为 5 and 10。
通过上面的定义其实就能看出区别了:
- sizeof是运算符,strlen是函数
- sizeof是计算所占空间大小,strlen是返回字符串长度
- sizeof是编译期间就算好了,strlen是运行期间计算
- sizeof可对多种类型计算,strlen的参数必须是以“\0”为结尾的字符型指针(char*)
- sizeof计算数组时不会退化成指针,strlen数组名作为参数会退化成指针
11、特别注意
对string或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中元素的个数。例如:
std::vector<int> vec1; //定义一个vector,然后插入10条数据
vec1.push_back(1);
vec1.push_back(2);
vec1.push_back(3);
vec1.push_back(4);
vec1.push_back(5);
vec1.push_back(6);
vec1.push_back(7);
vec1.push_back(8);
vec1.push_back(9);
vec1.push_back(10);
std::cout << "size: " << vec1.size() << " sizeof: " << sizeof(vec1) << std::endl;
std::string str;
str = "0123456789";
std::cout << "size: " << str.size() << " sizeof: " << sizeof(str) << std::endl;
实测在32位机器上返回size: 10 sizeof: 16 和 size: 10 sizeof: 28
在64位机器上返回size: 10 sizeof: 32 和 size: 10 sizeof: 40
感谢大家,我是假装很努力的YoungYangD(小羊)。
参考资料:
《C++ Primer 第六版》
http://www.cppblog.com/bloodsuck/articles/7575.html
https://www.cnblogs.com/carekee/articles/1630789.html
https://www.cnblogs.com/huolong-blog/p/7587711.html
https://blog.csdn.net/starstar1992/article/details/61619422
http://www.cnblogs.com/xuyuantao/archive/2010/08/15/1800266.html