C语言里的左移和右移分析

今天在主页上看到一句话“程序员之所以犯错误,不是因为他们不懂,而是因为他们自以为什么都懂。”挺打动我的,很多错误是我们自以为的认为程序怎样怎样的跑,然后就跑飞了。
先来看个问题:

小明1岁就会说话了,喊了声”妈妈“,小明他妈就死了,后来,喊爷爷时,爷爷死了。但是他喊爸爸时,隔壁老王死了。—网络

这里程序猿A说,有bug啊,喊爸爸时候,隔壁老王死了,那之前喊爷爷时候,怎么可能是小明爷爷死呢,应该是老王他爹死才对。这时,只见程序猿B摇摇头的叹气:冤冤相报何时了。0_0!

一:左移
左移: 将a的二进制数左移2位,右补0,位左移后溢出,舍弃。
百度上看到这样一句话,这句话很多书页有,“左移一位相当于该数乘以2,但此结论只适用于该数左移时被溢出舍弃的高位中不包含1的情况。”这句话不能说它全错,但又不恰当。根据这句话意思,如一个char a=128(10000000);如左移1位,a=0。

实际上,如果“<<”的左操作数为字符类型、short 或unsigned short 类型时,左操作数存在隐式的类型转换,即整数提升(Integer promotion),此时左操作数被转换为int 类型参与运算(其中有一个例外,就是如果short 类型与int 类型相同,unsigned short会被转换为unsigned int)。这样从本质上来说,参与运算的并非是a 本身,而是(int)a或(unsigned int)a 的值,运算所求得的值也并非是a 的类型,而是int 类型或unsigned int类型。”-----C程序中的谬误

char a = 128;
printf("a<<1:%d\n",a<<1);

这个结果输出的就是256了。当然,如果如下这样写:

char a = 128;
a = a << 1;
printf("a=%d\n", a);

这时涉及到的就是数据截断了,相当于如下代码:

char a = 128;
int b = 0;
b = a << 1;
a = (char)b;
printf("a=%d\n", a);

截取的时候是截取低位(),
二:右移

右移:按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1。----百度

简单理解就是原来符号位是啥,右移时就补啥。很多地方能看到这句话“右移一位相当于除以2,右移n 位相当于除以2n。 ”对于正数还比较好理解,二进制每高一位都是原来的2倍,而右移一位高位补0,即每一位都除于2。当负数,右移高位补的是1,为啥右移还是除于2呢。这涉及到数值在计算机的存储方式,以及这种存储方式对数值运算的作用。下次再讲_

三:大小端

大端(网络序):最高字节存放在内存的最低位字节地址上,而字数据的低字节则存放在高地址中;小端则相反;
假设一个4字节的字:0x01020304;存储地址从00-03

 内存地址:0x00000000  0x00000001  0x00000002  0x00000003
大端存储        0x01        0x02        0x03        0x04
小端存储        0x04        0x03        0x02        0x01

-----回到左、右移话题_
假设一个int a =128;(0x00 0x00 0x00 0x80)那么它在不同大小端系统中存储如下

 内存地址:0x00000000  0x00000001  0x00000002  0x00000003
大端存储   00000000    00000000    000000000   10000000
小端存储   10000000    00000000    000000000   00000000

这样一个值左移一位,即a<<1。根据上图得到如下结果:

 内存地址:0x00000000  0x00000001  0x00000002  0x00000003
大端存储   00000000    00000000    000000001   00000000
小端存储   00000000    00000000    000000000   00000000

显然大端得到的结果就是0x00 00 01 00,即十进制的256。而小端设备得到的确实0。奇怪了吧,难道左右移的算法(乘2、除2)还要根据大小端不成?Duang~~关键点来了:

(1) 数据在寄存器中都是以大端模式次序存放的。
(2) 对于内存中以小端模式存放的数据。CPU存取数成时,小端和大端之间的转换是通过硬件实现的,没有数据加载/存储的开销。—忘记哪里看到的了

大小端是针对数值在内存中的存储方式,如我们启动一个进程,数据都是在内存中的;而当数据要参与运算时,即执行到该机器指令时,数据被cpu调到寄存器中运算,而寄存器都是以大端模式存放,再参与运算,然后将结果根据大小端规则重新放回内存中。所以,不管大小端设备,128<<1都是256。

四:hton还是ntoh
前面的左右移和大小端都是为本节的铺垫
到底是要用hton,或是ntoh,第一次看到这两个函数,觉得蛮简单的,无非是收到将一个数据转为主机序或网络序(大端),看完百度百科后发现,偶滴神啊,还真是这样~~!值到后面碰到一个bug。。。
一次验证中发现,原来这两个函数就是一个篮球、一个足球—壳不一样,里面全是空气。。。也就是两个函数的实现一模一样

#define ___swab32(x) \
({ \
	__u32 __x = (x); \
	((__u32)( \
		(((__u32)(__x) & (__u32)0x000000ffUL) << 24) | \
		(((__u32)(__x) & (__u32)0x0000ff00UL) <<  8) | \
		(((__u32)(__x) & (__u32)0x00ff0000UL) >>  8) | \
		(((__u32)(__x) & (__u32)0xff000000UL) >> 24) )); \
})

uint32_t htonl(uint32_t hostlong)
{
return (checkCPUendian() ? (hostlong) : ___swab32(hostlong))
}

uint32_t ntohl(uint32_t hostlong)
{
return (checkCPUendian() ? (hostlong) : ___swab32(hostlong))
}

那么,问题来了。为啥一样的代码能实现不同功能。其实,这里我们理解的”功能“只是从hotn、ntoh的表面意义去理解,实际上这两个函数的操作无非时将数值转换一遍进行存储,而怎么转换取决于主机序。如果,主机序等于网络序–即都是大端,那不管是hton还是ntoh都是啥也不用干,直接赋值即可;相反,主机序不等于网络序,不管是hton还是ntoh都是把数值反转一遍,因为反转一遍即为网络序,再反转一遍又变回主机序。
所以,如果一个设备的主机序是小端的,那么不管调用hton还是调用ntoh都能把一个值转为网络序。其实,现在行业已经规定设备间通信都用网络序通信,因此,你在发送数据前需要转为网络序,即调用hton,但你使用ntoh也完全没问题;同样,从网络收到一个数据,需要转为主机序,你调用hton也一样能取到正确的数值(只要发送端转过一次)。
网络通信无非就是把一堆数值从一个设备送到另一个设备,不管另一个设备在何方、是何物。只要能128送过去还是128,而不会变成821就可以。现在分析一个数值的发送过程:A(小端)发送0x01020304===》16909060到B(大小端未知);
int b = 16909060;/* a是一个整型 */
b的存储方式如下:

内存地址:0x00000000 0x00000001 0x00000002 0x00000003
小端存储 04 03 02 01

如果不进行转网络序,msg.a = b;
则A申请一个内存msg放这个值,msg存储同上;那么B收到数据,也是这样的顺序。
如果B是大端设备,那得到的值就是0x04030201 =》67305985,显然错了;如果是小端,b = ntoh(msg.a);msg.a = 0x01020304=>hton后也变成了0x04030201,同样错了。

因此A中发送之前必须转为网络序msg.a = hton(b),此时a的值就已经不等于b了。
五:值通信
项目中经常看到消息发送前这样封装的

#define ENCODE_INT32(BUF, INDEX, DATA32) do { \
    *((u_int8_t *)(BUF) + (INDEX)) = (u_int8_t)((DATA32) >> 24); \
    (INDEX)++;                          \
    *((u_int8_t *)(BUF) + (INDEX)) = (u_int8_t)((DATA32) >> 16);    \
    (INDEX)++;                          \
    *((u_int8_t *)(BUF) + (INDEX)) = (u_int8_t)((DATA32) >> 8); \
    (INDEX)++;                          \
    *((u_int8_t *)(BUF) + (INDEX)) = (u_int8_t)(DATA32);    \
    (INDEX)++;                    \
} while (0)

使用时,对于要发送的4字节的数值通过这个方法封装,其实就是值通信的概念;即DATA32值是多少,就发送多少,不关心大小端。有点像hton的方法,我们知道hton是针对字节数大于1的数值,对于1个字节没必要转,而上述这种封装方法即是把4个字节按低位到高位顺序拆开成4个字节存储。所以接收方收到时也必须按低位到高位顺序方式再组合成一个4字节数值,即能达到hton、ntoh效果;DECODE如下:

#define DECODE_INT32(BUF, INDEX, RESULT32)  do {\
    (RESULT32) = *((u_int8_t *)(BUF) + (INDEX)) << 24; \
    (INDEX)++;                \
    (RESULT32) |= *((u_int8_t *)(BUF) + (INDEX)) << 16;  \
    (INDEX)++;               \
    (RESULT32) |= *((u_int8_t *)(BUF) + (INDEX)) << 8; \
    (INDEX)++;               \
    (RESULT32) |= *((u_int8_t *)(BUF) + (INDEX));  \
    (INDEX)++; \
} while (0)
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zcqiang_zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值