C/C++类型转换和sizeof

一 进制表示

  binary:二进制;octal:八进制;hexadecimal:十六进制;decimal:十进制。
  C语言使用默认是10进制的;C,C++规定,一个数如果要指明是二进制的,开头加0b;一个数如果要指明它采用八进制,必须在它前面加上一个0,如:123是十进制,但0123则表示采用八进制;16进制数必须以 0x开头。
  数据是由二进制补码存储的,第一位为符号位(1负,0正):当原码为正数的时候,正数的原码反码补码都相同,即00011的反码也为00011,补码也为00011;当原码为负数的时候,反码即按位取反,比如10011为原码,10011可表示-3,那么符号位不变,其余位按位取反即反码11100.那么10011的补码便是11101.

二 基本变量

1 基本类型变量

char%c、%d、%u)
short%hd)
int%d)
long%ld)
long long%lld)
//对应无符号类型
unsigned char%c、%d、%u)
unsigned short%hu、%ho、%hx)
unsigned int%u,%o、%x)
unsigned long%lu、%lo、%lx)
unsigned long long%llu、%llo、%llx)
//无符号类型打印出的值就是内存中存数的实际二进制值
float%f)
double%lf)
long double(%Lf)

2 符号扩展

  需要扩展的变量量为有符号数,扩展存储位数的方法,即在新的高位字节使用当前最高有效位即符号位的值进行填充。
  例1:

char a=0xff; //有符号值为-1,二进制为11111111,其中最高位为符号位
short b=a; //b的有符号值为-1,在内存中存储的值为1111111111111111

  例2:

char a=1; //有符号值为1,二进制为00000001,其中最高位为符号位
short b=a; //b的有符号值为1,在内存中存储的值为0000000000000001

3 零扩展

  对于要扩展量无符号数,扩展存储位数的方法。在新的高位直接填0。

unsigned char a=0xff;//二进制为11111111,所有值都是有效值
unsigned short b=a;//b经过零扩展后,内存中存储的值为0000000011111111

4 其他

  其实这里注重的是要扩展的量是有符号量还是无符号量。若要扩展量为有符号量,不管扩展成有符号还是无符号,都遵循符号扩展;若要扩展量为无符号量,不管扩展成有符号还是无符号,都遵循零扩展。
  例1:

char a =0xff;//a为-1,其为有符号量,二进制为11111111
unsigned shortb=a;//此处a要进行符号扩展,b的二进制为11111111 11111111

  例2:

unsigned char a=0xff;//a为无符号量,二进制为11111111
short b=a;//此处a要进行零扩展,b的二进制为00000000 11111111

三 类型转换

  C语言的类型转换,可以分为两种:自动类型转换(隐式类型转换,由编译器帮你去完成)强制类型转换(显示类型转换,你知道自己想要什么,所以才转换)

1 类型转换和扩展:

  一、有符号数的转换规则

方法
charshort符号位扩展
charlong符号位扩展
charunsigned char最高位失去符号位意义,变为数据位
charunsigned short符号位扩展到short;然后从short转到 unsigned short
charunsigned long符号位扩展到long; 然后从long 转到unsigned long
charfloat符号位扩展到long; 然后从long 转到float
chardouble符号位扩展到long; 然后从long 转到double
charlong double符号位扩展到long; 然后从long 转到long double
shortchar保留低位字节
shortlong符号位扩展
shortunsigned char保留低位字节
shortunsigned short最高位失去符号位意义,变为数据位
shortunsigned long符号位扩展到long; 然后从long转到unsigned double
shortfloat符号位扩展到long; 然后从long 转到float
shortdouble符号位扩展到long; 然后从long 转到double
shortlong double符号位扩展到long; 然后从long 转到double
longchar保留低位字节
longshort保留低位字节
longunsigned char保留低位字节
longunsigned short保留低位字节
longunsigned long最高位失去符号位意义,变为数据位
longFloat使用单精度浮点数表示。可能丢失精度。
longdouble使用双精度浮点数表示。可能丢失精度。
longlong double使用双精度浮点数表示。可能丢失精度。

  二、无符号数的转换规则

方法
unsigned charchar最高位作为符号位
unsigned charshort0扩展
unsigned charlong0扩展
unsigned charunsigned short0扩展
unsigned charunsigned long0扩展
unsigned charfloat转换到long;再从 long 转换到float
unsigned chardouble转换到long;再从 long 转换到double
unsigned charlong double转换到long;再从 long 转换到double
unsigned shortchar保留低位字节
unsigned shortshort最高位作为符号位
unsigned shortlong0扩展
unsigned shortunsigned char保留低位字节
unsigned shortunsigned long0扩展
unsigned shortfloat转换到long;再从 long 转换到float
unsigned shortdouble转换到long;再从 long 转换到double
unsigned shortlong double转换到long;再从 long 转换到double
unsigned longchar保留低位字节
unsigned longshort保留低位字节
unsigned longlong最高位作为符号位
unsigned longunsigned char保留低位字节
unsigned longunsigned short保留低位字节
unsigned longfloat转换到long;再从 long 转换到float
unsigned longdouble直接转换成double
unsigned longlong double转换到long;再从 long 转换到double

  总结:
  一、短数据类型扩展为长数据类型

  1. 要扩展的短数据类型为有符号数
    进行符号扩展,即短数据类型的符号位填充到长数据类型的高字节位(即比短数据类型多出的那一部分),保证扩展后的数值大小不变。
char x=0b10001001;
short y=x;
//则y的值应为11111111 10001001b;
char x=0b00001001;
short y=x;
//则y的值应为00000000 00001001b;
  1. 要扩展的短数据类型为无符号数
    进行零扩展,即用零来填充长数据类型的高字节位。
unsigned char x=0b10001001;
short y=x;
//则y的值应为00000000 10001001b;
unsigned char x=0b00001001;
short y=x;
//则y的值应为00000000 00001001b;

  二、长数据类型缩减为短数据类型
  如果长数据类型的高字节全为1或全为0,则会直接截取低字节赋给短数据类型;如果长数据类型的高字节不全为1或不全为0,则转会就会发生错误。
  三、同一长度的数据类型中有符号数与无符号数的相互转化
  直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。另短类型扩展为长类型时,但短类型与长类型分属有符号数与无符号数时,则先按规则一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。

2 隐式转换原则

  变换操作数采取就高不就低的原则,即级别低的操作数先被转换成和级别高的操作数具有同一类型,然后再进行运算,结果的数据类型和级别高的操作数相同。且有符号的转换成无符号的。
高   double ←← float
↑    ↑
↑    long
↑    ↑
↑    unsigned
↑    ↑
低    int ←← char,short

3 结构、联合变量及其成员变量

  同类型的结构可以相互赋值:

typedef struct test{
	char*name;
    unsigned short age;
} Struct_test;
Struct_test a,b={“zhangsan”,20};
a = b;//a和b中的数据是一样的,但在不同的存储空间

  通过共用体来确定机器是大端序还是小端序的例子:
  大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
  小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

#include <stdio.h>
union test {
	char a[4];
	int b;
};

int main(int argc, char **argv) {
	union test tst;
	tst.a[0] = 0x04;
	tst.a[1] = 0x03;
	tst.a[2] = 0x02;
	tst.a[3] = 0x01;
	printf("0x%x\n", tst.b);
	return 0;
}

  若打印出0x4030201则为大端序,若为0x1020304则为小端序

4 void **的转换

  C语言中没有通用指针类型,void *之所以可以用作通用指针,是因为当它和其他类型相互赋值的时候,如果需要,它可以自动转换成其他类型。但是,void **就不会自动转换了,原因是,当你使用void **指针的时候,例如用*操作符访问void **所指向的void *值得时候,编译器无法知道void *值是否从其他类型的指针转换而来,从而,编译器只能认为它仅仅是个void *指针,所以程序就无法正确访问到想要的结果。
  换言之,你使用的任何void **值必须是某个位置的void *值得地址,(void **)&dp这样的类型转化虽然能编译通过,但执行结果可能不是我们想要的。如果void **指针指向的不是void *类型,并且这个类型的大小和内存表示和void *也不同,则程序就无法正确访问到此类型。

四 C++隐式类型转换

1 原因

  C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。
  某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。
  C++是一门强类型语言,类型的检查是非常严格的。如果没有类型的隐式转换,这将给程序开发者带来很多的不便。
  当然,凡事都有两面性,在你享受方便快捷的一面时,你不得不面对太过智能以至完全超出了你的控制。风险就在不知不觉间出现。

2 隐式调用构造函数

  当构造函数只有一个参数,或者含有默认参数的时候,且参数类型为内建类型时,可以不用显示调用构造函数。

五 C++显示类型转换

  有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:

int i, j;
double slope = i/j;

  就要使用某种方法将 i 和/或 j 显式地转换成 double,这种方法称作强制类型转换( cast )。
  虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
  一个命名的强制类型转换具有如下形式:cast-name<type>(expression);;其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值 。cast_name是static_castdynamic_castconst_castreinterpret_cast中的一种。

1 static_cast

  static_cast,命名上理解是静态类型转换,如int转换成char, 只要不包含底层const,都可以使用 static_cast。例如, 通过将一个运算对象强制转换成 double 类型就能使表达式执行浮点数除法:

/ / 进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;

  当需要把一个较大的算术类型赋值给较小的类型时, static_cast 非常有用。 此时,强制类型转换告诉程序的读者和编译器: 我们知道并且不在乎潜在的精度损火。 一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型, 就会给出警告信息; 但是当我们执行了显式的类型转换后, 警告信息就会被关闭了。
  1、基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。(基类和子类之间的动态类型转换建议用dynamic_cast)。
  2、基本数据类型转换。enum, struct, int, char, float等。static_cast不能进行无关类型(如非基类和子类)指针之间的转换。
  3、static_cast 对于编译器无法自动执行的类型转换也非常有用。 例如, 我们可以使用 static_cast 找回存在于 void*指针中的值:

void* p = &d; // 正确: 任何非常量对象的地址都能存入 void*
// 正确 : 将 void*转换回初始的指针类型
double *dp = static_cast<double*>(p);

  当我们把指针存放在 void*中, 并且使用 static_cast 将其强制转换回原来的类型时,应该确保指针的值保持不变。 也就是说, 强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。 类型一旦不符, 将产生未定义的后果。
  4、把任何类型的表达式转换成void类型。
  5、static_cast不能去掉类型的const、volitale属性(用const_cast)。

int n =6;
double d = static_cast<double>(n); // 基本类型转换
int*pn =&n;
double*d = static_cast<double*>(&n) //无关类型指针转换,编译错误
void*p = static_cast<void*>(pn); //任意类型转换成void类型

2 dynamic_cast

  命名上理解是动态类型转换。如子类和父类之间的多态类型转换。有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL):
  1、安全的基类和子类之间转换。
  2、必须要有虚函数。
  3、相同基类不同子类之间的交叉转换。但结果是NULL。
  4、dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。

class BaseClass {
public:
	int m_iNum;
	virtualvoid foo(){}; //基类必须有虚函数。保持多台特性才能使用dynamic_cast
};

class DerivedClass: public BaseClass {
public:
	char*m_szName[100];
	void bar(){};
};

BaseClass* pb =new DerivedClass();
DerivedClass *pd1 = static_cast<DerivedClass *>(pb); //子类->父类,静态类型转换,正确但不推荐
DerivedClass *pd2 = dynamic_cast<DerivedClass *>(pb); //子类->父类,动态类型转换,正确

BaseClass* pb2 =new BaseClass();
DerivedClass *pd21 = static_cast<DerivedClass *>(pb2); //父类->子类,静态类型转换,危险!访问子类m_szName成员越界
DerivedClass *pd22 = dynamic_cast<DerivedClass *>(pb2); //父类->子类,动态类型转换,安全的。结果是NULL

3 const_cast

  去掉类型的const或volatile属性。
  1、常量指针被转化成非常量指针,转换后指针指向原来的变量(即转换后的指针地址不变)。

class A{
public:
	A()
	{
		m_iNum = 0;
	}
public:
	int m_iNum;
};
void foo()
{
	//1. 指针指向类
	const A *pca1 = new A;
	A *pa2 = const_cast<A*>(pca1); //常量对象转换为非常量对象
	pa2->m_iNum = 200; //fine
	//转换后指针指向原来的对象
	cout<< pca1->m_iNum <<pa2->m_iNum<<endl; //200 200
	//2. 指针指向基本类型
	const int ica = 100;
	int * ia = const_cast<int *>(&ica);
	*ia = 200;
	cout<< *ia <<ica<<endl; //200 100
}

  2、常量引用转为非常量引用。

class A{
public:
	A()
	{
		m_iNum = 1;
	}
public:
	int m_iNum;
};
void foo()
{
	A a0;
	const A &a1 = a0;
  A a2 = const_cast<A&>(a1); //常量引用转为非常量引用
  a2.m_iNum = 200; //fine
  cout<< a0.m_iNum << a1.m_iNum << a2.m_iNum << endl; // 1 200
}

  3、常量对象(或基本类型)不可以被转换成非常量对象(或基本类型)。

void foo() 
{ 
	//常量对象被转换成非常量对象时出错 
	const A ca; 
	A a = const_cast<A>(ca); //不允许 
	const int i = 100; 
	int j = const_cast<int>(i); //不允许 
} 

  4、添加const属性。

int main(int argc, char ** argv_)
{
	int i = 100;
	int *j = &i;
	const int *k = const_cast<const int*>(j);
	//const int *m = j; 感觉和这样写差不多
	//指的地址都一样
	cout << i << "," << &i << endl; //100, 0012FF78
	cout << *j << "," << j << endl; //100, 0012FF78
	cout << *k << "," << k << endl; //100, 0012FF78
	*j = 200;
	//*k = 200; //error
	return 0;
}

  总结:
  1、使用const_cast去掉const属性,其实并不是真的改变原类类型(或基本类型)的const属性,它只是又提供了一个接口(指针或引用),使你可以通过这个接口来改变类型的值。也许这也是const_case只能转换指针或引用的一个原因吧。
  2、使用const_case添加const属性,也是提供了一个接口,来不让修改其值,不过这个添加const的操作没有什么实际的用途(也许是我认识太浅了)。

4 reinterpret_cast

  仅仅重新解释类型,但没有进行二进制的转换:
  1、转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。
  2、在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。但不能将非32bit的实例转成指针。
  3、最普通的用途就是在函数指针类型之间进行转换。
  4、很难保证移植性。
  5、这种类型转换实际上是强制编译器接受static_cast通常不允许的类型转换,它并没有改变指针值的二进制表示,只是改变了编译器对源对象的解释方式。应尽量避免使用reinterpret_cast
  reinterpret_cast虽然看似强大,作用却没有那么广。IBM的C++指南、C++之父Bjarne Stroustrup的FAQ网页和MSDN的Visual C++也都指出:错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。这样说起来,reinterpret_cast转换成其它类型的目的只是临时的隐藏自己的什么(做个卧底?),要真想使用那个值,还是需要让其露出真面目才行。那到底它在C++中有其何存在的价值呢?

int doSomething(){return0;};
typedef void(*FuncPtr)(); //FuncPtr is 一个指向函数的指针,该函数没有参数,返回值类型为 void
FuncPtr funcPtrArray[10]; //10个FuncPtrs指针的数组 让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:
funcPtrArray[0] =&doSomething;// 编译错误!类型不匹配,reinterpret_cast可以让编译器以你的方法去看待它们:funcPtrArray
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //不同函数指针类型之间进行转换

5 总结

  去const属性用const_cast;基本类型转换用static_cast;多态类之间的类型转换用daynamic_cast;不同类型的指针类型转换用reinterpret_cast。

六 类型转换函数

1 atof(将字符串转换成浮点型数)

  相关函数 atoi,atol,strtod,strtol,strtoul
  表头文件 #include <stdlib.h>
  定义函数 double atof(const char nptr);
  函数说明 atof()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(’\0’)才结束转 换,并将结果返回。参数nptr字符串可包含正负号、小数点或E(e)来表示指数部分,如123.456或123e-2。返回值 返回转换后的浮点型数。
  附加说明 atof()与使用strtod(nptr,(char*)NULL)结果相同。

2 atol(将字符串转换成长整型数)

  相关函数 atofatoistrtodstrtolstrtoul
  表头文件 #include<stdlib.h>
  定义函数 long atol(const char \*nptr);
  函数说明 atol()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(’\0’)才结束转换,并将结果返回。
  返回值 返回转换后的长整型数。
  附加说明 atol()与使用strtol(nptr,(char **)NULL,10);结果相同。

3 gcvt(将浮点型数转换为字符串,取四舍五入)

  相关函数 ecvt,fcvt,sprintf
  表头文件 #include<stdlib.h>
  定义函数 char *gcvt(double number,size_t ndigits,char *buf);
  函数说明 gcvt()用来将参数number转换成ASCII码字符串,参数ndigits表示显示的位数。gcvt()与ecvt()和fcvt()不同的地方 在于,gcvt()所转换后的字符串包含小数点或正负符号。若转换成功,转换后的字符串会放在参数buf指针所指的空间。
  返回值 返回一字符串指针,此地址即为buf指针。

4 strtod(将字符串转换成浮点数)

  相关函数 atoiatolstrtodstrtolstrtoul
  表头文件 #include<stdlib.h>
  定义函数 double strtod(const char *nptr,char **endptr);
  函数说明 strtod()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,到出现非数字或字符串结束时(’\0’)才结束转 换,并将结果返回。若endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr传回。参数nptr字符串可包含正负号、 小数点或E(e)来表示指数部分。如123.456或123e-2。
  返回值 返回转换后的浮点型数。

5 strtol(将字符串转换成长整型数)

  相关函数 atofatoiatolstrtodstrtoul
  表头文件 #include<stdlib.h>
  定义函数 long int strtol(const char *nptr,char **endptr,int base);
  函数说明 strtol()会将参数nptr字符串根据参数base来转换成长整型数。参数base范围从2至36,或0。参数base代表采用的进制方式,如 base值为10则采用10进制,若base值为16则采用16进制等。当base值为0时则是采用10进制做转换,但遇到如’0x’前置字符则会使用 16进制做转换。一开始strtol()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束 时(’\0’)结束转换,并将结果返回。若参数endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr返回。
  返回值 返回转换后的长整型数,否则返回ERANGE并将错误代码存入errno中。
  附加说明 ERANGE指定的转换字符串超出合法范围。

6 strtoul(将字符串转换成无符号长整型数)

  相关函数 atofatoiatolstrtodstrtol
  表头文件 #include<stdlib.h>
  定义函数 unsigned long int strtoul(const char *nptr,char **endptr,int base);
  函数说明 strtoul()会将参数nptr字符串根据参数base来转换成无符号的长整型数。参数base范围从2至36,或0。参数base代表采用的进制方 式,如base值为10则采用10进制,若base值为16则采用16进制数等。当base值为0时则是采用10进制做转换,但遇到如’0x’前置字符则 会使用16进制做转换。一开始strtoul()会扫描参数nptr字符串,跳过前面的空格字符串,直到遇上数字或正负符号才开始做转换,再遇到非数字或 字符串结束时(’\0’)结束转换,并将结果返回。若参数endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr返回。
  返回值 返回转换后的长整型数,否则返回ERANGE并将错误代码存入errno中。
  附加说明 ERANGE指定的转换字符串超出合法范围。

7 toascii(将整型数转换成合法的ASCII 码字符)

  相关函数isasciitouppertolower
  表头文件 #include<ctype.h>
  函数定义 int toascii(int c)
  函数说明 toascii()会将参数c转换成7位的unsigned char值,第八位则会被清除,此字符即会被转成ASCII码字符。
  返回值 将转换成功的ASCII码字符值返回。

8 atoi函数(将字符串转换成整型数)

  相关函数atofatolatrtodstrtolstrtoul
  头文件:#include<stdlib.h>
  定义函数:int atoi(const char *nptr)
  函数说明:atoi()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(’\0’)才结束转换,并将结果返回。
  返回值:返回转换后的整型数。
  附加说明:atoi()与使用strtol(nptr,(char**)NULL,10);结果相同。

char arr[] = "8,7";
int a = atoi(arr);
cout << a << endl; //8

文章链接

【csapp】【微软面试题】有符号数到无符号数隐式转换:https://blog.csdn.net/u012162613/article/details/40887457


一 什么是sizeof

  首先看一下sizeof在msdn上的定义:The sizeof keyword gives the amount of storage, in bytes, associated with a variable or a type (including aggregate types). This keyword returns a value of type size_t.
  sizeof是一个关键字。

二 用法

  sizeof运算符返回一条表达式或一个类型名字所占的字节数。其所得的值是一个size_t类型的常量表达式。运算符的运算对象有两种形式:

sizeof(type)
sizeof expr

  在第二种形式中, sizeof返回的是表达式结果类型的大小。对类型使用sizeof,注意这种情况下写成sizeof typename是非法的。与众不同的一点是, sizeof并不实际计算其运算对象的值:

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大小的方式

  C++11新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无须我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。
  string对象或vector对象执行sizeof运算只返回该类型固定部分的人小,不会计算对象中的元素占用了多少空间。

三 sizeof的常量性

sizeof的计算发生在编译时刻,所以它可以被当作常量表达式使用,如:

char ary[ sizeof( int ) * 10 ]; // ok

最新的C99标准规定sizeof也可以在运行时刻进行计算,如下面的程序在Dev-C++中可以正确执行:

int n;
n = 10; // n动态赋值
char ary[n]; // C99也支持数组的动态定义
printf("%d/n", sizeof(ary)); // ok. 输出10

但在没有完全实现C99标准的编译器中就行不通了,上面的代码在VC6中就通不过编译。所以我们最好还是认为sizeof是在编译期执行的,这样不会带来错误,让程序的可移植性强些。

四 数据类型的sizeof

1 C++固有数据类型

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

2 自定义数据类型

  typedef可以用来定义C++自定义类型。自定义类型的sizeof取值等同于它的类型原形。

3 函数类型

  考虑下面的问题:

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
cout<<sizeof*f2<<endl; //*f2,和f2()等价,因为可以看作object,所以括号不是必要的。被认为是double

  结论:对函数使用sizeof,在编译阶段会被函数返回值的类型取代。
  C99标准规定,函数、不能确定类型的表达式以及位域(bit-field)成员不能被计算sizeof值,即下面这些写法都是错误的:

sizeof(foo);//error
void foo2(){}
sizeof(foo2());//error
struct S{
    unsigned int f1 : 1;
    unsigned int f2 : 5;
    unsigned int f3 : 12;
};
sizeof(S.f1);//error

五 指针

  考虑下面问题:

cout<<sizeof(string*)<<endl; // 4
cout<<sizeof(int*)<<endl; // 4
cout<<sizof(char****)<<endl; // 4

  可以看到,不管是什么类型的指针,大小都是4的,因为指针就是32位的物理地址。
  结论:只要是指针,大小就是4。(64位机上测试是8)。
  顺便唧唧歪歪几句,C++中的指针表示实际内存的地址。和C不一样的是,C++中取消了模式之分,也就是不再有small,middle,big,取而代之的是统一的flat。flat模式采用32位实地址寻址,而不再是c中的segment:offset模式。举个例子,假如有一个指向地址f000:8888的指针,如果是C类型则是8888(16位,只存储位移,省略段),far类型的C指针是f0008888(32位,高位保留段地址,地位保留位移),C++类型的指针是f8888(32位,相当于段地址*16+位移,但寻址范围要更大)。

六 数组

  考虑下面问题:

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

  数组a的大小在定义时未指定,编译时给它分配的空间是按照初始化的值确定的,也就是7。c是多维数组,占用的空间大小是各维数的乘积,也就是6。可以看出,数组的大小就是他在编译时被分配的空间,也就是各维数的乘积*数组元素的大小。
  结论:数组的大小是各维数的乘积*数组元素的大小。
  这里有一个陷阱:

int *d = new int[10];
cout<<sizeof(d)<<endl; // 4

  d是我们常说的动态数组,但是他实质上还是一个指针,所以sizeof(d)的值是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)=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

七 向函数传递数组的问题

  考虑下面的问题:

#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和strlen

  考虑下面的问题:

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类型的字符串。
  注:关于sizeof(string),好像不同的实现返回的结果不一样。

九 从union的sizeof问题看cpu的对界

  考虑下面问题:(默认对齐方式)

union u
{
   double a;
   int b;
};
union u2
{
   char a[13];
   int b;
};
union u3
{
   char a[13];
   char b;
};
cout<<sizeof(u)<<endl; // 8
cout<<sizeof(u2)<<endl; // 16
cout<<sizeof(u3)<<endl; // 13

  都知道union的大小取决于它所有的成员中,占用空间最大的一个成员的大小。所以对于u来说,大小就是最大的double类型成员a了,所以sizeof(u)=sizeof(double)=8。但是对于u2和u3,最大的空间都是char[13]类型的数组,为什么u3的大小是13,而u2是16呢?关键在于u2中的成员int b。由于int类型成员的存在,使u2的对齐方式变成4,也就是说,u2的大小必须在4的对界上,所以占用的空间变成了16(最接近13的对界)。
  结论:复合数据类型,如unionstructclass的对齐方式为成员中对齐方式最大的成员的对齐方式。
  顺便提一下CPU对界问题,32的C++采用8位对界来提高运行速度,所以编译器会尽量把数据放在它的对界上以提高内存命中率。对界是可以更改的,使用#pragma pack(x)宏可以改变编译器的对界方式,默认是8。C++固有类型的对界取编译器对界方式与自身大小中较小的一个。例如,指定编译器按2对界,int类型的大小是4,则int的对界为2和4中较小的2。在默认的对界方式下,因为几乎所有的数据类型都不大于默认的对界方式8(除了long double),所以所有的固有类型的对界方式可以认为就是类型自身的大小。更改一下上面的程序:

#pragma pack(2)
union u2
{
 char a[13];
 int b;
};

union u3
{
 char a[13];
 char b;
};
#pragma pack(8)

cout<<sizeof(u2)<<endl; // 14
cout<<sizeof(u3)<<endl; // 13

  由于手动更改对界方式为2,所以int的对界也变成了2,u2的对界取成员中最大的对界,也是2了,所以此时sizeof(u2)=14
  结论:C++固有类型的对界取编译器对界方式与自身大小中较小的一个。

十 struct的sizeof问题

  因为对齐问题使结构体的sizeof变得比较复杂,看下面的例子:(默认对齐方式下)

struct s1{
   char a;
   double b;
   int c;
   char d;
};
struct s2{
   char a;
   char b;
   int c;
   double d;
};
cout<<sizeof(s1)<<endl; // 24
cout<<sizeof(s2)<<endl; // 16

  同样是两个char类型,一个int类型,一个double类型,但是因为对界问题,导致他们的大小不同。计算结构体大小可以采用元素摆放法,我举例子说明一下:首先,CPU判断结构体的对界,根据上一节的结论,s1和s2的对界都取最大的元素类型,也就是double类型的对界8。然后开始摆放每个元素。
  对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。
  对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。
  这里有个陷阱,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:

struct s1{
   char a[8];
};
struct s2{
   double d;
};
struct s3{
   s1 s;
   char a;
};
struct s4{
   s2 s;
   char a;
};
cout<<sizeof(s1)<<endl; // 8
cout<<sizeof(s2)<<endl; // 8
cout<<sizeof(s3)<<endl; // 9
cout<<sizeof(s4)<<endl; // 16;

  s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。
  字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
  1、结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
  2、结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
  3、结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
  对于上面的准则,有几点需要说明:
  1、前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。想想为什么。
  结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:#define offsetof(s,m) (size_t)&(((s *)0)->m)
  例如,想要获得S2中c的偏移量,方法为:

size_t pos = offsetof(S2, c);// pos等于4

  2、基本类型是指前面提到的像charshortintfloatdouble这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。
  这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以VC6为例,以后不再说明):

struct S3{
	char c1;
	S1 s;
	char c2;
};

S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。
c1的偏移量为0,s的偏移量呢这时s是一个整体,它作为结构体变量也满足前面三个准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补上3个填充字节。最后得到sizeof(S3)的值为16。
通过上面的叙述,我们可以得到一个公式:
结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:

sizeof(struct) = offsetof(last item) + sizeof(last item) + sizeof(trailing padding)

到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma pack实现,也可以直接修改/Zp编译开关。#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。
  还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如下:

struct S5 { };
sizeof(S5); // 结果为1

十一 含位域结构体的sizeof

前面已经说过,位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。
C99规定intunsigned intbool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。使用位域的主要目的是压缩存储,其大致规则为:
1、如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2、如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3、如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
4、如果位域字段之间穿插着非位域字段,则不进行压缩;
5、整个结构体的总大小为最宽基本类型成员大小的整数倍。
还是让我们来看看例子。
示例1:

struct BF1{
	char f1 : 3;
	char f2 : 4;
	char f3 : 5;
};

其内存布局为:
|f1_|f2||f3||
|
|
|||||||||||||||
0 3 7 8 1316
位域类型为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。
示例3:

struct BF3{
	char f1 : 3;
	char f2;
	char f3 : 5;
};

非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。

十二 不要让double干扰你的位域

  在结构体和类中,可以使用位域来规定某个成员所能占用的空间,所以使用位域能在一定程度上节省结构体占用的空间。不过考虑下面的代码:

struct s1{
   int i: 8;
   int j: 4;
   double b;
   int a:3;
};
struct s2{
   int i;
   int j;
   double b;
   int a;
};
struct s3{
   int i;
   int j;
   int a;
   double b;
};
struct s4{
   int i: 8;
   int j: 4;
   int a:3;
   double b;
};
cout<<sizeof(s1)<<endl; // 24
cout<<sizeof(s2)<<endl; // 24
cout<<sizeof(s3)<<endl; // 24
cout<<sizeof(s4)<<endl; // 16

  可以看到,有double存在会干涉到位域(sizeof的算法参考上一节),所以使用位域的的时候,最好把float类型和double类型放在程序的开始或者最后。

十三 无法使用sizeof的情况

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

十四 特殊情况

  参数为结构或类
  sizeof应用在类和结构的处理情况是相同的。但有两点需要注意:
  第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。
  第二、没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。
  参数为其他

int func(char s[5]);
{
	return 1;
}

  sizeof(func(“1234”))=4//因为func的返回类型为int,所以相当于求sizeof(int)。
  sizeof操作符的作用是返回一个对象或类型名的长度,长度的单位是字节。
  返回值的类型是标准库命名为size_t的类型,size_t类型定义在cstddef头文件中,该头文件是C标准库的头文件stddef.h的C++版本。他是一个和机器相关的unsigned类型,其大小足以保证内存中对象的大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值