关键字和运算符sizeof详解

版权声明:转载请说明来源 https://blog.csdn.net/weixin_39640298/article/details/84330819

概述

在学习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位机器上就有差别了。

扫描二维码关注公众号,回复: 4354487 查看本文章

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。

通过上面的定义其实就能看出区别了:

  1. sizeof是运算符,strlen是函数
  2. sizeof是计算所占空间大小,strlen是返回字符串长度
  3. sizeof是编译期间就算好了,strlen是运行期间计算
  4. sizeof可对多种类型计算,strlen的参数必须是以“\0”为结尾的字符型指针(char*)
  5. 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

猜你喜欢

转载自blog.csdn.net/weixin_39640298/article/details/84330819
今日推荐