三只小猪

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/longshanks/article/details/2948562
三只小猪
莫华枫

    小时候听说过三只小猪的故事,隐约记得故事是讲三只小猪用不同方法造房子,对抗老狼。这些天做软件,遇到一个无比简单的问题,但在三种不同的语言中,却有着截然不同的解法。

    最近,冷不丁地接到公司下派的一个紧急任务,做手持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++中,
这类任务的实现方法,完全看程序员的想象力了。:)

展开阅读全文

没有更多推荐了,返回首页