C语言的类型转换

C语言的类型转换

1.1大端和小端

        我们知道存储器是以字节为单位来存放数据的,每个字节有8位,因此1个字节最多可以表示256种不同的状态。256种状态对于常用的整数和浮点类型来说是远远不够的,因此这些类型的数据需要用多个字节来表示,如在32位环境下,int一般是4个字节,double一般是8个字节。对于跨越多字节的程序对象,我们必须建立一个规则:在存储器中如果排列这个字节。

       排列表示一个对象的字节有两个通用的规则。某些机器选择在存储器中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则称为小端法,如X86机器都是采用的小端法;后一种规则称为大端法,如某些IBM的机器和SPARC机器就是大端法;一些较新的机器如ARM则可以配置为大端或小端运行。考虑一个int类型的变量x,它的16进制表示为0x12345678,它的地址是在0x100处,则小端法表示0x100,0x101,0x102,0x103这四个字节为:0x78,0x56,0x34,0x12,而大端法则相反,为:0x12,0x34,0x56,0x78。

       对于大多数应用场合,程序员完全不需要考虑他们机器所使用的字节顺序,编译器自动完成了相关的操作,因此无论为哪种类型机器编译的程序都会得到同样的运行结果。不过有两种场合下,字节顺序会成为问题:

1.  进行网络传输时

              不同表示法的机器间进行网络传输时,字节顺序会出现反序的情况。为了避免这类问题,编写网络应用程序时必须要显式的规定字节序,如Socket通信中规定字节序为大端表示,这样网络传输的双方就可以预估接收到的数据是不是需要进行字节序的调整了。

2.  进行强制类型转换时

              在C语言中我们经常对指针使用强制类型转换,这种做法允许我们绕开正常的类型系统去实现一些复杂的功能,如将int类型的变量x的最低字节设置为0,可以这么做:

char *p=(char*)&x;
*p=0;
       这里将x的地址重解释为一个char类型对象的地址,于是对它的设置就只会影响到1个char的大小。如果目标机器是大端表示的,上面的例子就会有问题,因为此时p指向的是x的最高字节,对它的设置会产生相反的效果。

1.2整数类型间的转换

       C语言支持多种整数类型,它们有着不同的字节长度:char为1个字节,short为2个字节,int为2/4个字节(16位/32或64位环境),long为4/8个字节(16或32/64位环境),long long为8个字节(C99支持)。每种类型又分signed和unsigned,它们有着不同的数值范围:signed类型的整数的范围是[-2w-1,2w-1-1],unsigned的范围是[0,2w-1],其中w是每种类型的位宽。

       以上各种类型的长度和范围可能在不同的机器和编译环境下有所不同,这是因为C语言标准中只规定了每种类型必须能支持的最小范围,比如int的长度要求是sizeof(short)<=sizeof(int)<=sizeof(long),因此如果你发现某个环境下的int是2个字节时(这在嵌入式环境下是很常见的)不要太惊讶。不过32位编译环境下可以默认int的长度为32位。

       这些整数类型在运算时经常需要进行转换,这些转换一般遵循以下原则:

1.  signed和unsigned间的转换不会改变整数的位值(二进制表示),只是改变了解释这些位的方式。如:

int v=-12345;
unsigned int uv=v;
printf(“v=%d,uv=%u\n”,v,uv);

在常见的编译环境下,上面代码的运行结果为:v=-12345,uv=4294954951。

2.  较小的类型转换为较大的类型时会发生扩展,这种扩展按有无符号分为两种方式:signed类型会进行符号扩展,即多出的位都用原数的符号位填满,如char c=0xfe,int i=c,则i的位值为0xfffffffe;unsigned类型会进行零扩展,即多出的位都用0填满,如unsigned char c=0xfe,unsigned int i=c,则i的位值为0x000000fe。

3.  同时发生转换1和转换2时,优先进行转换2,即先改变数的大小,再改变数的符号,如:

char c=-1;
unsigned int i=c;
printf(“c=%d,i=%u\n”,c,i);

在常见的编译环境下,上面代码的运行结果为:c=-1,i=4294967295。

4.  较大的类型转换为较小的类型时会发生截断,即简单的取较大类型的低字节。如

int i=56789;
short s=i;
printf(“%d\n”,s);

在常见的编译环境下,上面代码的运行结果为:-8747。      

        注意,上面的转换原则在通常的编译环境下是有效的,但不排除你正在使用的编译器使用了其它的实现方法,因此可能上面的代码在你的机器上有着不同的行为表现。

1.2.1关于有符号数与无符号数的建议

      
       就像我们看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为,而这些非直观特点经常导致程序错误,并且这种错误很难被发现。

下面的例子说明这种错误的隐蔽性与危险性。

例子:

        2002年,FreeBSD的程序员发现他们的getpeername函数的实现存在安全漏洞。代码的简化版本如下:

/*...*/
void *memcpy(void *dest, void *src, size_tn);
/*...*/
#define KSIZE 1024
char kbuf[KSIZE];
/*...*/
int copy_from_kernal(void *user_dest, intmaxlen)
{
    int len=KSIZE<maxlen?KSIZE:maxlen;
    memcpy(user_dest,kbuf,len);
    return len;
}
        如果有恶意的程序员在调用copy_from_kernal的代码中对maxlen使用了负数,那么这个负数会被传给memcpy的参数n。不过请注意参数n的类型是size_t,这个类型通常被定义为无符号整型,那么memcpy会把它当作一个非常大的正数,并且试图复制这么多字节的数据到用户的缓冲区,结果程序成功的讲到了没有被授权的内核区域。

       由此可见这种错误是多么的难以发现。解决办法很简单,将maxlen和len都声明为size_t类型,这样就避免了无意间产生的有无符号数的转换。但如果调用copy_from_kernal的程序员不小心传进去一个负数实参,他可能会复制比自己预期要多的数据,这个错误够他找上一阵子了。

       避免这类错误的一种方法就是不要用无符号类型!像Java等语言中根本就没有无符号类型。如果我们只是把整数变量看作是位的集合,并没有任何值的意义的话,这时候无符号类型就派上用场了,如果我们这里用有符号数来表示一个位组,可能要花上大量的时间来避免符号扩展对结果的影响。这也是无符号类型唯一非常有用的地方。

1.2.2确定大小的整数类型

       对于某些程序来说,用确定大小的类型来储存数据非常重要。例如当有可移植性的需求时,或需要进行网络通信时,让数据类型与不同平台的程序采用的类型兼容是非常重要的。

       ISO C99标准在stdint.h中引入了一套确定大小的整数类型,它们的声明形如intN_t和uintN_t,指定N位有符号和无符号整型。N的常见值为8、16、32和64,如表示一个位组的类型可以是uint8_t,表示32位有符号整数的类型是int32_t。

       这些数据类型对应着一组宏,定义了每个N的值对应的最小值和最大值。这些宏名字形如INTN_MIN、INTN_MAX和UINTN_MAX。

1.3整数与浮点数间的转换

       C语言中的浮点类型有float、double和longdouble三种,其中float为4字节,double为8字节,很多机器上的long double实际和double是相同的,但GCC在如X86机器上将longdouble实现为10字节。

       当在int、float和double格式间进行强制类型转换时,程序改变数值和位模式的原则如下(假设int是4字节的):

1.  从int转换成float,数字不会溢出,但可能被舍入(有效数字不足)。

2.  从int或float转换成double,因为double有更大的范围,也有更高的精度,所以能够保留精确数值,不会发生溢出和舍入。

3.  从double转换成float,可能发生溢出,也可能舍入。

4.  从float或double转换成int,值会向0舍入,例如1.999会转换成1,而-1.999会转换成-1;值可能会溢出,C语言标准没有对这种情况指定固定的结果。

1.4类型提升的规则

       在一个包含不同类型数据的表达式中,较小的类型会按以下原则进行提升:

1.  如果某运算符的两个操作数为不同的整数类型,则较小的整数类型会提升为较大的类型,提升时先改变数的大小,后改变数的符号。

2.  如果两个操作数为不同的浮点类型,则较小的类型会提升为较大的类型,这种转换很安全。

3.  如果两个操作数分别为整数和浮点数,则整数会被提升为int,再提升为double,浮点数会被提升为double。

1.5指针类型的转换

       C语言中允许不同类型的指针进行强制转换,这种转换不会改变指针本身的值。例如库函数中的qsort的原型为:

       void qsort (void* base, size_t num, size_tsize, int (*cmp)(const void*,const void*));

       其中输入数据要的是指针,而且为了能适应不同的数据类型,包括自定义的结构体类型,第一个参数需要是void*指针,而对应到具体类型的操作则在cmp指向的函数中进行。这个函数需要的也是void*指针,所以我们在使用前先要对它进行强制转换,如对一个int型数组按递增顺序排序的话,cmp函数可以是:

int cmp(const void *a,const void *b)
{
    int*ia=(int*)a;
    int*ib=(int*)b;
    return*a-*b;
}

       任何指针都可以与void*隐式转换,但不能与其它类型的指针进行隐式转换,必须进行显式的强制转换。我们知道指针本身的值是它指向的对象的地址,但这个地址上的对象实际是否是指针对应的类型,甚至是否存在,我们并不清楚。因此,如果我们已知这个地址实际对应着哪个类型的对象,就可以放心的将指向这个地址的指针进行强制转换。

       如socket编程中,很多函数都需要struct sockaddr类型的指针,但实际这个类型只是一个接口,我们在实际使用时需要将真正的类型转换为这种指针:

       bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));

        其中servaddr的实际类型为struct sockaddr_in。

       这种转换灵活而强大,但同时也会带来很大的安全风险。这种转换绕开了C语言本身的类型系统,编译器也不会对转换中产生的错误报警,而一旦目标地址对应的对象类型与我们预期的不符,可能程序就会因为不正确的内存访问而崩溃。另外,有恶意的程序员可能会对其它模块传过来的指针进行粗暴的转换,这就破坏了正常的数据流动,而这种行为是我们之前无法预估的。因此,建议是,尽量不要用,除非你调用的函数要求你这么做。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值