本文通过将浮点数转换为字符串格式的实例分析,剖析标准库函数_fcvt,并由此出发,阐述了有关低耦合模块编程的一些个人体会。
前段时间用C语言编写一个信息管理系统,因需要在文本文件中存储文本格式的浮点型数据(用于直接人类阅读),在向文件写入数据时需先将浮点数转换为字符串格式。
原本打算自己写一个函数解决,但考虑到进度问题,还是调用库函数的快吧。自己写算法方面没什么问题,只是现在没有那么多时间去做测试,日后还要自己维护,另一方面由于没有什么经验,拿不准浮点数据的截断方案。没想到这一调用却调出了问题。
之前用惯了.net类库,以为C的标准库也提供相同功能的函数。其实并没有。
比如:
将fValue = -123.456f;
转换为:
strValue=”-123.456”;
这在.net类库中很容易实现,但在C标准库中却没有现成的函数直接供调用。这是一个不幸的事实。C程序员要比C#程序员多写几行到几十行不等的代码,进而承担更多的测试工作、运行风险和维护成本。当然,语言的优劣问题没有讨论的必要,看深入一点,其实都是一样的。编程本质是抽象待解决的问题,基于数学逻辑,使用编程语言等工具,向机器表述如何解决问题的工作。无论是C还是C#或者其他语言,我们都可以在不同的层面看到相同的编程思想。
通过实例来分析吧。
上面提到在C环境中将一个float转换为一个string(即char*),标准库中并无现成的函数。VC++ 6.0环境提供了以下函数:
char *_fcvt(
double value,
int count,
int *dec,
int *sign
); //referencing head file:<stdlib.h>
参数解释:
value: Number to be converted.//待转换的浮点数据
count: Number of digits after the decimal point.//精度控制,小数点后需要保留的位数
dec: Pointer to the stored decimal-point position.//存储小数点位置的整形指针
sign: Pointer to the stored sign indicator.//存储标示待转换浮点数正负的整形指针
初次目睹_fcvt的风采,相信很多同志也会和我一样,看不透这个函数要干嘛——形参有点奇怪。当然,你可以MSDN it,你会得到非常详细的描述。然而如若英文一般般,最好还是不要只看文档,做些小的测试最能说明问题,下面将说明这一点。
这个函数返回了一个令我惊讶的数据,以下是我在VC++ 6.0中用Win32 Console Application所做测试的部分结果,我们将据此对各个参数进行分析。
变量定义:
int dec;
int sign;
float value;
char* string;
用不同的数据调用_fcvt及相应结果:
1.
调用:
value=123.456f;
string=_fcvt(value,4,&dec,&sign);
结果:
string: ”1234560”
dec: 3
sign: 0
2.
调用:
value=-23.456f;
string=_fcvt(value,2,&dec,&sign);
结果:
string: ”2345”
dec: 2
sign: 1
如果测试只是做到这里,你看到的大概有以下几点:
a. 最简单的sign返回浮点数的正负:0代表正,1代表负
b. string也不算复杂,它不存储小数点和正负号,并根据参数count截取小数点后的位数
c. dec 则是存储了小数点在string中的位置索引
当然,这看到几点说明了你还有不算很差的观察力,目前的测试结果确实说明了这几点。显然,string并不是我们一开始所期待的”123.456”或者”-23.45”。但现在我们至少可以据此设计我们的目标函数,进而得到所期待的”123.456”或者”-23.45”。
这也是挺简单的问题了,基本思路是根据sign、dec、string进行字符串构造。可以期待得到”123.456”或者”-23.45”,按照这个思路和测试1、2去实施,事实也是如此。
但我们很难发现测试1、2会遗漏什么,事实上对这两个测试我们并没有什么理由指出其不全面之处。但接下来的一切会告诉我们这两个测试遗漏的什么。冥思是没有结果的。当我们使用自定义的转换函数去工作,处理一些诸如0.023等绝对值小于1的数据时,这个函数(按照上述的实现方式)就会down掉,或者返回一些莫名其妙的结果。原因在哪里呢?还是看看下面_fcvt函数的测试结果:
3.
调用:
value=0.0123f;
string=_fcvt(value,2,&dec,&sign);
结果:
string: ”1”
dec: -1
sign: 0
4.
调用:
value=0.00123f;
string=_fcvt(value,2,&dec,&sign);
结果:
string: ””
dec: -2
sign: 0
God!居然出现了空字符串和负值索引!?!好的,现在需要稍微动动脑筋来分析这样的测试结果。怎么入手?Check MSND?就算你English Very Good,恐怕也搞不定。针对这个问题,看MSDN上的例子也没用,因为MS提供的例子属于我们的测试1、2的类别范畴。
解决这个问题,首先要转变我们思考问题的基础。我们分析测试1、2,总是一厢情愿地认为参数dec存储小数点在string的位置,string存储截断的结果,MSDN的说法也有导致我们这种先入为主的思维的嫌疑,的确,这种思维方式十分形象具体。测试3、4的结果却指出这种形象具体的思维定势是错误的。
既然如此,最直接的方法是看看_fcvt函数的实现,看看它是怎样被设计的。VC++ 6.0的目录好像没有提供它的实现,或者根本就像printf,往下应该是汇编了。怎么办?
现在我们能做的只剩下尝试自己分析_fcvt函数的实现。勇敢地尝试,如果是你,你会怎么设计。怎样的设计会得到你期望的结果?怎样的设计会得到与测试1~4的结果相吻合的函数?这都是要思考的问题。
其实也很简单,几行稿子就能很快分析好。我的看法是:
拿测试3分析
对于传入的参数value=0.0123进行下面的运算
for(int i=0;i<count;i++)
{
value=value*10;//为截取指定精度的数据做准备
}
对于string=_fcvt(value,2,&dec,&sign);
count=2
最终得到value=1.23
再对value=1.23进行四舍五入的取整处理:
int nTemp=(int)(1.23+0.5);//这是四舍五入的处理方式
很显然,我们需要对nTemp进行从整形到字符类型的转换,
string=itoa(nTemp);//itoa在C标准库中
于是string=”1”
sign自然不必说了
最后,dec=strlen(string)-count; //这一步比较关键,凭直觉猜测的
dec=1-2=-1
到目前为止上述分析可以很好解释测试1~4的结果,并且如你所见这在算法上也有很简单的实现。虽然并未得到_fcvt设计者的证实。但是根据上述分析以及下面要阐述的更重要的依据,我有百分之九十九的把握相信这种设计,剩下的百分之一留给我不了解的汇编,它可能又更直接的实现方式。但这一点也不影响我们要讨论的主要问题。
下面我们讨论上述提到的所谓的更重要的依据:
首先,C标准库为什么不直接提供我们期望的类似char* _ftoa(double value,int count)的函数直接实现转换,而是提供了“半残”的_fcvt?_fcvt又是这副尊荣,设计者最初的想法是什么呢,目的何在,又有什么核心理念的指导?
在这里,我们要提及代码的可复用性和模块之间的耦合度问题。这两者有很重要的联系,较低的模块耦合度等价于较高的代码可复用性。当然,我对这个问题还没有足够的认识,很多看法来自书本。在软件工程中,管理问题最重要。举个例子来说明我对这个问题的一点认识。
假设有以下几个功能模块:A、B、C、D、E
其中模块组合A+B、A+C、A+D、A+E分别等价于模块b、c、d、e
现在我们需要以上四个模块组合来完成某些工作。有两个方案:
1. 分别设计A B C D E五个模块,然后组合为A+B、A+C、A+D、A+E。成本为 A B C D E。
2. 直接进行b c d e模块的设计。成本为 A B A C A D A E。
其中成本包括了代码编写、测试、维护、运行风险等等
显然软件工程正是致力于设计第一种方案的设计和优化,两种方案的优劣对比很明显。
回到我们的问题,_fcvt函数正是上述例子中的模块A,它作为一个解决基础抽象问题的模块,具有基础性,只要稍加利用,便可以解决很多问题,而不只是将一个浮点数据转换为字符串类型。
比如我们要将浮点数转换为字符串,但是并不想在负数前面加上一个‘-’号;或者我们只对浮点数的有效数字的字符串形式感兴趣……等等不同需求。这个世界的需求是无穷无尽的,标准库设计者不可能实现每一个对应的功能,不是因为技术问题,而是成本问题。比如设计者不可能了解到各种可能的需求,没有了需求还有后文吗?【.net类库似乎实现了我们想要的很多功能,但成本不大吗?再者,.net类库实际上只是在一定的限度上细化各类问题,有个极限!】
但是我们模模糊糊地知道我们应该具备怎样的设计思路。编程不是为了解决某一个特定的问题,而是为了解决拥有成千上万数量的问题群或叫做某一类问题。标准[类]库的设计更深入一步,在更高层面上考虑问题,致力于解决很多类问题。
所以立足点应该是致力于解决多类问题。这需要一种远见,这是实现代码可复用、可扩展的基础。【当然,这种远见的实现需要很大的成本,但所带来的价值却是成千上百倍的】我们怎么设计一个可以同时有助于解决很多类问题的方法呢?
我的基本观点是:
有两个原则,
第一、 提供尽可能多的细节。如同_fcvt提供了一开始出乎我们意料的dec,这是输出细节,用于提供处理结果信息。提供了count,这是输入细节,用于细化用户需求。
第二、 解决尽可能少的问题。为什么这么说?这涉及到比较复杂的问题,关系到问题的分层系统。举个简单的例子,我们在一块木板上加上几个轮子,为了做一个滑板,这样的话,我们便拥有了一个滑板。这里我们要赋予滑板和木板如同代码一样可以无成本无限复制的特性,因为我们拥有一份电子数据和一千份相同的电子数据是没有什么区别的,只有赋予这种特性,才会产生在编程世界中会产生的效用,由于这是为了说明编程问题,所以赋予赋予滑板和木板如同代码一样可以无成本无限复制的特性并无不合理之处。好了,现在我们把手头的一个木板做成一个滑板,我们可以无成本复制上一千份,它们的功能是给一千个人提供娱乐。我们也可把一个木板做成一个凳子,再复制上一千份,可供一千个人同时休息。显然,我们可以做很多东西,提供很多服务。这一切的基础是什么?那一块木板!而不是一个滑板或一个凳子,一个滑板和一个凳子基本不可能实现彼此的功能,将一个滑板改造成一个凳子或者反过来,成本是一个大问题吧。所以最具有创造力的是那块木板,它可以魔术般地变出千姿百态的世界。原因在哪里?木板实现了很少的功能,甚至没有什么功能,但是它又最具有创造力,因为只要稍加改造,赋予创意,便会化腐朽为神奇!!!这就是问题的分层系统——经济社会的一门大学问!
这也是编程的思想灵魂,致力于低成本高效率地解决现实问题,思路是优化功能模块组合,提供最佳的整体解决方案。不在语言,在于思想,在于问题的抽象。
现在回到我们最初的问题,我们要基于_fcvt设计一个将浮点数转换为字符串格式的函数。在彻底地认识了_fcvt之后,剩下的问题解决起来就易如反掌了。
最直接的思路:【以下是本人的个人自定义C函数库中的代码片段,供各位参考。该函数在Visual C++6.0环境中做过比较全面的测试,并额外补充了注释。有误之处,欢迎各位慷慨批评、指正】
#include <stdlib.h>
#include <malloc.h>
//convert float point data to string as tpye-appearance
//将一个浮点数据转化成为打印形式的字符串,即:若传入的浮点数是负数,
//得到的字符串前将拥有一个负号
//本库函数是基于<stdlib.h>
// char *_fcvt(
// double value,
// int count,
// int *dec,
// int *sign
// );的操作
//传入fValue待转换的浮点数,nCount小数点后要保留的位数;
//返回目标字符串
char* LION_DigitsPro_fToS(float fValue,unsigned nCount)
{
char* back;//用于存储要返回的打印形式的字符串,如 -123.456
char* temp;//用于存储由fcvt函数返回的字符串,如123456
int decimalPointPosition;//标记小数点前的数字格式
int i;//计数变量
int size;//标记浮点数的正负
int tempLen;//标记字符串temp的长度
//利用fcvt函数求取相关量temp等
temp=_fcvt(fValue,nCount,&decimalPointPosition,&size);
tempLen=strlen(temp);
//根据上述求取的相关量构造返回字符串back
if (fValue==0 || tempLen==0)
{
//待转换的浮点数为0或按精度截断后为0,按如下方式处理
back=(char*)malloc(sizeof(char)*2);
back[0]='0';
back[1]='/0';
return back;
}
if (size==0)
{
//待转换的浮点数为正数
if (decimalPointPosition>0)
{
//待转换的浮点数绝对值不小于1
//从_fcvt得到的temp字符串前段没有有效'0'
//按如下方法处理
//申请内存
back=(char*)malloc(sizeof(char)*(tempLen+2));
//目标字符数组填充小数点前的字符
for(i=0;i<decimalPointPosition;i++)
{
back[i]=temp[i];
}
//插入小数点
back[i]='.';
//填充小数点后的字符
for(;i<decimalPointPosition+nCount;i++)
{
back[i+1]=temp[i];
}
// 标示字符串结束符
back[i+1]='/0';
//如果要求保存的精度位数为0,我们将截断个位之后的字符
if (nCount==0)
{
back[i]='/0';
}
}
else
{
//待转换的浮点数绝对值不小于1
//从_fcvt得到的temp字符串前段有有效'0',可能发生截断
//按如下方法处理
//申请内存
back=(char*)malloc(sizeof(char)*(tempLen+3-decimalPointPosition));
//在目标字符数组前段填充"0.0......"
back[0]='0';
back[1]='.';
for (i=0;i<abs(decimalPointPosition);i++)
{
back[i+2]='0';
}
//在目标字符数组后端填充由_fcvt得到的字符串
for (i=0;i<tempLen;i++)
{
back[2-decimalPointPosition+i]=temp[i];
}
//标示字符串结束符
back[2-decimalPointPosition+i]='/0';
}
}
else
{
//待转换的浮点数为负数
if(decimalPointPosition>0)
{
//待转换的浮点数绝对值不小于1
//从_fcvt得到的temp字符串前段没有有效'0'
//按如下方法处理
//申请内存
back=(char*)malloc(sizeof(char)*(tempLen+3));
//在目标数组首位插入'-'
back[0]='-';
//向目标字符数组填充小数点前的字符
for(i=0;i<decimalPointPosition ;i++)
{
back[i+1]=temp[i];
}
//插入小数点
back[i+1]='.';
//向目标字符数组填充小数点后的字符
for (;i<decimalPointPosition+nCount;i++)
{
back[i+2]=temp[i];
}
//标示字符串结束符
back[i+2]='/0';
//如果要求保存的精度位数为0,我们将截断个位之后的字符
if (nCount==0)
{
back[i+1]='/0';
}
}
else
{
//待转换的浮点数绝对值不小于1
//从_fcvt得到的temp字符串前段有有效'0',可能发生截断
//按如下方法处理
//申请内存
back=(char*)malloc(sizeof(char)*(tempLen-decimalPointPosition +4));
//用"-0.0......"填充目标字符数组的前段
back[0]='-';
back[1]='0';
back[2]='.';
for (i=0;i<abs(decimalPointPosition);i++)
{
back[3+i]='0';
}
//在目标字符数组后端填充由_fcvt得到的字符串
for (i=0;i<tempLen;i++)
{
back[2-decimalPointPosition+i]=temp[i];
}
//标示字符串结束符
back[2-decimalPointPosition+i]='/0';
}
}
//返回字符串
return back;
}
//附注:本函数在Visual C++ 6.0环境中测试通过
//需要注意的一个问题是,由于本库函数调用了标准库中的fcvt函数,fcvt函数在转换浮点数时存在一个数值的精度问题
//上述代码并未对此进行修正。主要原因在于在较低精度的要求下,该函数已经可以满足需求
//在较高精度的要求下,可能会出现浮点数的精度问题,如截断。
/************************************************************************/
/* */
/************************************************************************/
LION
2010-3-2
揭阳