三只小猪
莫华枫
小时候听说过三只小猪的故事,隐约记得故事是讲三只小猪用不同方法造房子,对抗老狼。这些天做软件,遇到一个无比简单的问题,但在三种不同的语言中,却有着截然不同的解法。
最近,冷不丁地接到公司下派的一个紧急任务,做手持POS和PC程序之间交换数据的程序。各种各样的麻烦中,有一个小得不起眼的问题,POS机中数据的字
节序和PC相反。这可不算是什么啊。没错,是在太简单了。尽管如此,还是引发了一场争论。做POS程序的,希望PC程序做转换。做PC程序的,希望POS
程序做转换。(谁都想少做点,对吧;))。最终,作为和事佬的我,为了维护和谐的氛围,揽下了这件事。当然,到底在那里做,还是要决定的。最终选择在PC
上,毕竟PC上调试起来容易。(天煞的,这个POS机没有debug,也没有模拟器,显示屏还没我手机的大,做起来着实费事)。
其实,我的本意是想在POS上做这个转换。因为POS用的是C(一个不知什么年代的gcc),可以直接操作字节。基本的代码看起来差不多应该是这样:
unsigned long InvData(unsigned long val, int n) {
unsigned long t=val, res=0;
for(; n >0; n--)
{
res = res << 8;
res |= (unsigned char)t;
t = t >> 8;
}
return res;
}
n是数据类型的字节长度。这里使用了最长的无符号整数类型。这是核心转换函数,各种类型的转换函数都可以从中派生:
long InvDataLong(long val) {
return (long)InvData((unsigned long)val, sizeof(val));
}
short InvDataShort(short val) {
return (short)InvData((unsigned short)val, sizeof(val));
}
...
最后,有一个比较特殊的地方,float。float的编码不同于整型,如果直接用(unsigned
long)强制类型转换,只会把float数值的整数部分赋予参数,得不到正确的结果。正确的做法,应当是把float占用的四个字节直接映射成一个
unsigned long:
float InvDataFloat(float val) {
float val=InvData(*(unsigned long*)(&val), sizeof(val));
return *(float*)(&val);
}
通过将float变量的地址强制转换成unsigned long*类型,然后再dereference成unsigned
long类型。当然还有其他办法,比如memcpy,这里就不多说了。至于double类型,为了简化问题,这里将其忽略。如果有64位的整型,那么
double可以采用类似的解法。否则,就必须写专门的处理函数。
当然,最终我还是使用C#写这个转换。相比之下,C#的转换代码显得更具现代风味。基本算法还是一样:
public static ulong DataInv(ulong val, int n)
{
ulong v1_ = val, v2_ = 0;
for (; n > 0; n--)
{
v2_ <<= 8;
v2_ |= (byte)v1_;
v1_ >>= 8;
}
return v2_;
}
对于习惯于C/C++的同学们注意了,long/ulong在C#中不是4字节,而是8字节。也就是C/C++中的longlong。以这个函数为基础,其它整数类型的字节序转换也就有了:
public static ulong DataInv(ulong val)
{
return DataInv(val, sizeof(ulong));
}
public static uint DataInv(uint val)
{
return (uint)DataInv((ulong)val, sizeof(uint));
}
public static int DataInv(int val)
{
return (int)DataInv((uint)val);
}
...
然而,面对float,出现了麻烦。在C#中,没有指针,无法象C那样将float展开成ulong。(unsafe代码可以执行这类操作,但这不是C#嫡亲的特性,并且是违背C#设计理念的。这里不做考虑)。C#提供了另一种风格的操作:
public static float DataInv(float val)
{
float res_ = 0;
byte[] buf_ = BitConverter.GetBytes(val);
byte t = 0;
t = buf_[0];
buf_[0] = buf_[3];
buf_[3] = t;
t = buf_[1];
buf_[1] = buf_[2];
buf_[2] = t;
res_ = BitConverter.ToSingle(buf_, 0);
return res_;
}
这个做法尽管有些累赘,但道理上很简单:把float变量转换成一个字节流,然后把相应的位置对调,就获得了字节反序的float。相比C的float转
换,C#明显不够简练。原因很简单,C#根本不是用来干这个的。C是一种非常底层的语言,它的内存模型是完全直观的,与硬件系统相对应的。因而,对于这种
与机器相关的操作,当然也显得游刃有余。而C#定位于高层开发的高级语言,底层的内存模型是被屏蔽的,程序员无需知道和关心。
不过,C#的代码却拥有它的优势。只需看一眼这些函数的使用代码,便不言自明了:
//C代码
int x=234;
float y=789.89;
short z=332;
x=InvDataInt(x);
y=InvDataFloat(y);
z=InvDataShort(z);
//C#代码
int x=234;
float y=789.89;
short z=332;
x=DataInv(x);
y=DataInv(y);
z=DataInv(z);
在C代码中,对于不同的类型,需要使用不同命名的函数。而在C#代码中,则只需使用DataInv这样一个函数名。至于届时选用那个版本的函数,编译器会
根据实际的类型自动匹配。C#运用函数重载这个特性,使得调用代码可以采用统一的形式。即便是数据的类型有所变化,也无需对调用代码做任何修改。(这在我
的开发过程中的得到了验证,传输数据的成员类型曾经发生变化,我也只是修改了数据结构的定义,便将问题搞定)。这一点,在C中是无法做到的。
归结起来,C由于侧重于底层,在数据转换方便的灵活性,使得转换代码的构建更加容易。而C#则得益于函数重载,在转换代码使用方面,有独到的优势。
迄今为止,三只小猪中,还只有两只出现。下面就要第三只出场了。
作为C++的粉丝,我会自然而然地想到使用C++来实现这个转换功能。于是便有了如下的代码:
unsigned long InvData(unsigned long val, int n) {
unsigned long t=val, res=0;
for(; n >0; n--)
{
res = res << 8;
res |= (unsigned char)t;
t = t >> 8;
}
}
long InvData(long val) {
return (long)InvData((unsigned long)val, sizeof(val));
}
short InvData(short val) {
return (short)InvData((unsigned short)val, sizeof(val));
}
...
float InvData(float val) {
float val=InvData(*(unsigned long*)(&val), sizeof(val));
return *(float*)(&val);
}
这些代码就好象是C和C#代码的杂交后代。既有C的底层操作,也有C#的函数重载,兼有两者的优点。
不过,还能做得更好:
template<typename T>
T InvData(T val) {
T t=val, res=0;
int n=sizeof(T);
for(; n >0; n--)
{
res = res << 8;
res |= (unsigned char)t;
t = t >> 8;
}
return (T)res;
}
这样,就把所有的整型都一网打尽了,仅用一个函数模板,便完成了原先诸多函数所做的工作。而float版本的函数则保持不变,作为InvData()的一个重载。按照C++的函数模板-重载规则,float版的函数重载将被优先使用。
好了,三只小猪的故事讲完了。前两只小猪各有优点,也各有缺点。而第三只小猪则杂合和前两者的优点,并且具有更大的进步。尽管第三只小猪存在各种各样的缺陷,但毕竟它的众多特性为我们带来了很多效率和方便,这些还是应当值得肯定的。
附:第三只小猪的其他手段:
1、强制转换成字符串数组
template<typename T>
T InvData1(T v) {
unsigned char* pVal1=(unsigned char*)(&v)
, *pVal2=pVal1+sizeof(T)-1, t;
while(pVal2-pVal1>1)
{
t=*pVal2;
*pVal2=*pVal1;
*pVal1=t;
pVal1++;
pVal2--;
}
return v;
}
2、使用标准库,blog上有人留言建议的
template<typename T>
T InvData(T v) {
unsigned char* pVal=(unsigned char*)(&v);
size_t n=sizeof(T);
std::reverse(pVal, pVal+n, pVal);
}
3、使用traits
template<size_t n> struct SameSizeInt;
template<> struct SameSizeInt<1> { typedef unsigned char Type; };
template<> struct SameSizeInt<2> { typedef unsigned short Type; };
template<> struct SameSizeInt<4> { typedef unsigned long Type; };
template<> struct SameSizeInt<8> { typedef unsigned longlong Type; };
template<typename T>
T InvData(T v) {
size_t n=sizeof(T);
typedef SameSizeInt<sizeof(T)>::Type NewT;
NewT v1=*(NewT*)(&v), v2=0;
for(; n >0; n--)
{
v2= v2<< 8;
v2|= (unsigned char)v1;
v1 = v1 >> 8;
}
return *(T*)(&v2);
}
甚至可以使用tmp去掉其中的循环。在C++中,这类任务的实现方法,完全看程序员的想象力了。:)