在这一章中,我们将编写计算器程序运运算的核心部分。我先为核心编制一个ANSI C版本,然后将详细探讨如何使这些代码在Palm OS中运行。 设计计算器核心 在开始写代码之前,我们必须知道到底该做些什么。一个解决这类问题的好方法是建立一个包括程序各个状态并通过箭头相连表示它们之间的关系的状态表。对本程序来说,这也是个很好的办法,因为这个程序比一般的程序包含了更多状态和状态关联。 你也可以将状态表制成一个大纲的形式,这样会使对程序看的更清楚。通常,当我想知道程序该完成哪些工作时,我就先从纸上画出程序的状态表大纲。不管效果如何,你可以先试一下。我建议你在看我的状态表之前,自己先做一个。表14-1是我所做的状态表。 状态: 操作算子――->改变符号―――>完成操作、显示结果、保存―――>回到预备状态 等于号(equal)―――>改变符号―――>完成操作、显示结果―――>回到预备状态 清除号(clear) ―――>清除数字和操作算子―――>回到预备状态 完成号(Done)―――>拷贝当前显示结果到粘贴板―――>退出程序预备状态:显示当前数字 0―――>显示0―――>预备状态 其它数字―――>显示数字―――>插入状态 改变符号―――>显示-0―――>预备状态 小数点―――>显示0. ―――>小数状态 指数―――>出错声―――>预备状态 整数状态:显示一个整数 数字―――>如果是允许输入的最大数字,则发出声音―――>整数状态 改变符号―――>给当前整数取反―――>整数状态 小数点―――>在当前整数后加小数点显示―――>小数状态 指数―――>在当前整数后加e+0显示―――>指数状态 小数状态:显示一个数字和一个小数点 数字―――>如果是允许输入的最大数字就发出报警音―――>把新输入的数字显示在最后―――>小数状态 改变符号―――>给当前显示的数字取反显示―――>小数状态 小数点―――>出错音―――>小数状态 指数―――>在当前显示的数字后加e+0显示―――>指数状态 指数预备状态:带e+0的显示一个数字 0―――>指数预备状态 其它数字―――>显示数字的指数―――>指数输入状态 改变符号―――>改变当前的指数符号为e-0―――>指数预备状态 小数点―――>出错音―――>指数预备状态 指数号―――>出错音―――>指数预备状态 指数输入状态:显示带指数的数字 数字―――>如果是允许输入的最大数字就发出警报音―――>给输入的数字取指数―――>指数输入状态 改变符号―――>改变指数的符号―――>指数输入状态 小数点―――>出错音―――>指数输入状态 指数号―――>出错音―――>指数输入装态 在我开始的状态表中,还有一些其它的状态,例如Clear状态和Enter Second Number状态,但当我画完时,发现这些状态可以很容易的融入到其它的状态中去。在开始编程之前,状态表将是一个很好的参考材料,所以我建议都来使用状态表,况且其它的大多数界面都要比本例子简单和直接。 剩余代码 为了好解释有关将ANSI标准C代码导入Palm OS的知识,我用以下的方法编写了程序。在程序中有一些可移植性的错误,看看你自己在看导入代码那部分之前能不能将它找出来。 新的calc.h 这个calc.h将代替上一章最后创建的“虚”文件,以便我们做进一步的调试。 #ifndef CALC_H #define CALC_H // // calc.h // Definitions for the generic calculation routines. // Copyright (c) 1999, Robert Mykland. All rights reserved. // /// // Global Prototypes // /// void calcAdd( void ); // Queue an add operation void calcAppend( int ); // Append a digit void calcChangeSign( void ); // Change the sign of the entry void calcClear( void ); // Clear/reset the calculator void calcDivide( void ); // Queue a divide operation void calcEquals( void ); // Finish the current operation void calcExponent( void ); // Start gathering the exponent void calcMultiply( void ); // Queue a multiply operation void calcPoint( void ); // Start gathering the fraction void calcSubtract( void ); // Queue a subtraction operation // // Global Constants // // #define MAX_NUMBER_SIZE 40 #endif // CALC_H 现在头文件已经转换为了原来传统类型的文件。 Calc.c的普通代码 在文件calc.c中实现了calc.h中定义的函数。我们将对这些代码进行简单的描述,因为它们和Palm OS关系不大,只是ANSI中实现计算器功能的普通代码。 // // calc.c // Implements a generic calculator. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include "app.h" // The definitions for this application #include "calc.h" // The definitions for this module /// // Global Prototypes // /// void calcAdd( void ); // Queue an add operation void calcAppend( int ); // Append a digit void calcChangeSign( void ); // Change the sign of the entry void calcClear( void ); // Clear/reset the calculator void calcDivide( void ); // Queue a divide operation void calcEquals( void ); // Finish the current operation void calcExponent( void ); // Start gathering the exponent void calcMultiply( void ); // Queue a multiply operation void calcPoint( void ); // Start gathering the fraction void calcSubtract( void ); // Queue a subtraction operation // // Local Prototypes // // static double ca2n( char* ); // Converts an ascii string to a double static void n2ca( double, char* ); // Changes a double to a string / // Local Constants // / #define MAX_DIGITS 8 #define MAX_EXP_DIGITS 2 enum { OPERATION_NONE = 0, OPERATION_ADD, OPERATION_DIVIDE, OPERATION_MULTIPLY, OPERATION_SUBTRACT }; / // Local Variables // / static char caNumber[MAX_NUMBER_SIZE] = "+0"; static int iDigitCount; static int iOperator; static double nOperand; static int oExponent; static int oFraction; 以上是下面讲到函数所用的头文件、函数原型以及变量的声明。 // // Global Functions // // //---------------------------------------------------------------------------- void calcAdd( //---------------------------------------------------------------------------- // Queues an add operation. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // Resolve any pending operations calcEquals(); // Queue the operation iOperator = OPERATION_ADD; // We're done return; } 这个函数解决了“加”的操作。函数calcEquals()处理了所有没有完成的计算,所以我们不需检查“加”到底是“加”还是“被加”的状态。 //---------------------------------------------------------------------------- void calcAppend( //---------------------------------------------------------------------------- // Appends a digit to the entry. //---------------------------------------------------------------------------- int iDigit ) // The digit to append //---------------------------------------------------------------------------- { char caDigit[2]; // If we are entering the exponent part if( oExponent ) { // If the exponent digit count is at maximum, then signal an error if( iDigitCount >= MAX_EXP_DIGITS ) { calcSignalError(); return; } } else // If we are entering the number part { // If the digit count is at maximum, then signal an error if( iDigitCount >= MAX_DIGITS ) { calcSignalError(); return; } } // Destroy leading zeroes if( (oFraction == false) && (iDigitCount < 2) && (caNumber[1] == '0') ) caNumber[1] = '/0'; // Append the digit caDigit[0] = iDigit + 0x30; caDigit[1] = '/0'; strcat( caNumber, caDigit ); // Increase the digit count if it wasn't a leading zero if( (oFraction == true) || (iDigitCount == 0) || (caNumber[1] != '0') ) iDigitCount++; // Display the new number calcDisplay( caNumber ); return; } 函数calcAppend()函数是程序中最复杂的函数之一。它将检查我们输入的数字、指数符和操作符是否规范。 //---------------------------------------------------------------------------- void calcChangeSign( //---------------------------------------------------------------------------- // Changes the sign of the number or exponent. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { int iPlace; // Find the last sign in the number for( iPlace = strlen( caNumber ) - 1; iPlace >= 0; iPlace-- ) { // If it's a plus, change to minus if( caNumber[iPlace] == '+' ) { caNumber[iPlace] = '-'; break; } // If it's a minus, change to plus if( caNumber[iPlace] == '-' ) { caNumber[iPlace] = '+'; break; } } // Display the new number calcDisplay( caNumber ); return; } 函数calcChangeSign()是另一个可以判定我们是在一般数字还是指数状态的函数。这里很简单,只需要将最近的数据符号返回就行了。这也是为什么要将符号保存的原因。 //---------------------------------------------------------------------------- void calcClear( //---------------------------------------------------------------------------- // Clears/resets the calculator. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // Set our local variables in a default state strcpy( caNumber, "+0" ); iDigitCount = 0; iOperator = OPERATION_NONE; nOperand = 0.0; oExponent = false; oFraction = false; // Display the new number calcDisplay( caNumber ); return; } 这个函数将程序恢复到最初状态,我们可以首先调试这个函数。 //---------------------------------------------------------------------------- void calcDivide( //---------------------------------------------------------------------------- // Queues a divide operation. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // Resolve any pending operations calcEquals(); // Queue the operation iOperator = OPERATION_DIVIDE; // We're done return; } 这个函数和calcAdd()十分相似,实际上所有的计算操作符的函数都是相似的。 //---------------------------------------------------------------------------- void calcEquals( //---------------------------------------------------------------------------- // Resolves a math operation. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { double nOperand2; // If there is an entry if( iDigitCount > 0 ) // Convert the entry to floating point nOperand2 = ca2n( caNumber ); else // If there is no entry // The entry is the last operand nOperand2 = nOperand; // Perform the operation switch( iOperator ) { case OPERATION_ADD: nOperand = nOperand + nOperand2; break; case OPERATION_DIVIDE: nOperand = nOperand / nOperand2; break; case OPERATION_MULTIPLY: nOperand = nOperand * nOperand2; break; case OPERATION_SUBTRACT: nOperand = nOperand - nOperand2; break; default: nOperand = nOperand2; break; } // Clear the operator iOperator = OPERATION_NONE; // Convert the result from floating point for display n2ca( nOperand, caNumber ); // Display the new number calcDisplay( caNumber ); // Reset the entry iDigitCount = 0; strcpy( caNumber, "+0" ); oExponent = false; oFraction = false; // We're done return; } 这个函数实现了真正的运算操作。 //---------------------------------------------------------------------------- void calcExponent( //---------------------------------------------------------------------------- // Starts gathering the exponent. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // If we're not already doing the exponent if( (oExponent == false) && // and if the number is nonzero (ca2n( caNumber ) != 0.0) ) { // Set up the exponent part oExponent = true; iDigitCount = 0; strcat( caNumber, "e+0" ); // Display the new number calcDisplay( caNumber ); } else // This was done in error calcSignalError(); // We're done return; } 这个函数实现了“指数”键的操作,这里定义的变量将影响到calcAppend()函数的操作。 //---------------------------------------------------------------------------- void calcMultiply( //---------------------------------------------------------------------------- // Queues a multiply operation. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // Resolve any pending operations calcEquals(); // Queue the operation iOperator = OPERATION_MULTIPLY; // We're done return; } 此函数“加”和“除”操作是类似的。 //---------------------------------------------------------------------------- void calcPoint( //---------------------------------------------------------------------------- // Appends a decimal point to the entry. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // If we are not collecting the fractional part already if( (oFraction == false) && // If we are not doing the exponent (oExponent == false) && // If we are not maxed out on digits (iDigitCount != MAX_DIGITS) ) { // If no digit has been entered, enter a zero if( iDigitCount == 0 ) calcAppend( 0 ); // Now we will have a fractional part oFraction = true; // Append the decimal point strcat( caNumber, "." ); } else // This was done in error calcSignalError(); // Display the new number calcDisplay( caNumber ); return; } 这个函数和calcAppend()一样,比较复杂,因为它要解决各种不同情况下,输入数字的状态问题。 //---------------------------------------------------------------------------- void calcSubtract( //---------------------------------------------------------------------------- // Queues a subtract operation. //---------------------------------------------------------------------------- void ) //---------------------------------------------------------------------------- { // Resolve any pending operations calcEquals(); // Queue the operation iOperator = OPERATION_SUBTRACT; // We're done return; } 此函数和“加”、“除“、和“乘”函数类似。 / // Local Functions // / //---------------------------------------------------------------------------- static double ca2n( //---------------------------------------------------------------------------- // Converts a decimal ascii string to a double. //---------------------------------------------------------------------------- char* cpNumber ) // The string to convert //---------------------------------------------------------------------------- { double nSign; int oDecimal; int iDivisor; int iCount; char caInt[MAX_DIGITS + 1]; int iNumber; double nNumber; // Get any leading sign nSign = 1.0; if( *cpNumber == '+' ) cpNumber++; if( *cpNumber == '-' ) { nSign = -1.0; cpNumber++; } // Convert to an integer string oDecimal = false; iDivisor = 0; for( iCount = 0; (iCount <= MAX_DIGITS) && *cpNumber && (*cpNumber != 'e'); iCount++ ) { // Do the decimal point thing if( *cpNumber == '.' ) { oDecimal = true; iCount--; cpNumber++; continue; } // If we are gathering the fraction if( oDecimal ) iDivisor++; // Otherwise, copy the digit caInt[iCount] = *cpNumber++; } // Zero delimit the string caInt[iCount] = '/0'; // Use atoi iNumber = atoi( caInt ); // Convert to a double nNumber = nSign * (double)iNumber * pow( 10.0, -(double)iDivisor ); // If there is an exponent if( *cpNumber == 'e' ) { cpNumber++; // Get any leading sign nSign = 1.0; if( *cpNumber == '+' ) cpNumber++; if( *cpNumber == '-' ) { nSign = -1.0; cpNumber++; } // Convert to an integer string for( iCount = 0; (iCount <= MAX_EXP_DIGITS) && *cpNumber; iCount++ ) caInt[iCount] = *cpNumber++; // Zero delimit the string caInt[iCount] = '/0'; // Use atoi iNumber = atoi( caInt ); // Multiply the number nNumber *= pow( 10.0, (double)iNumber ); } // Return the number return( nNumber ); } 上面的函数将ASCII字符串转换为双精度浮点数。这绝不是要纯数学性的方法才能完成,相对来说还是比较容易弄懂的。不过,完全搞清这些函数的操作也是不必要的,因为这些对我们以后的课程并不重要。 //---------------------------------------------------------------------------- static void n2ca( //---------------------------------------------------------------------------- // Converts a double to an ascii string. //---------------------------------------------------------------------------- double nNumber, // The number to convert char* cpNumber ) // Storage for the converted number //---------------------------------------------------------------------------- { double nExp; int iExp; int iNumber; char caInt[9]; int iZeroes; // Handle zero if( nNumber == 0.0 ) { strcpy( cpNumber, "+0" ); return; } // Grab the sign *cpNumber = '+'; if( nNumber < 0.0 ) { nNumber = -nNumber; *cpNumber = '-'; } cpNumber++; // Normalize nExp = log10( nNumber ); iExp = (int)nExp; if( nExp < 0 ) iExp--; iExp -= MAX_DIGITS - 1; nNumber /= pow( 10.0, (double)iExp ); // Convert to an integer iNumber = (int)(nNumber + 0.5); // Convert to an integer string itoa( caInt, iNumber ); // Count trailing zeroes for( iZeroes = 0; caInt[strlen( caInt ) - 1 - iZeroes] == '0'; iZeroes++ ) ; // Handle decimal notation if( (iExp <= 0) && (iExp > -MAX_DIGITS) ) { // Integer part strncpy( cpNumber, caInt, MAX_DIGITS + iExp ); cpNumber[MAX_DIGITS + iExp] = '/0'; // Decimal point strcat( cpNumber, "." ); // Mantissa part strcat( cpNumber, caInt + MAX_DIGITS + iExp ); // Eliminate trailing zeroes while( cpNumber[strlen( cpNumber ) - 1] == '0' ) cpNumber[strlen( cpNumber ) - 1] = '/0'; } // Handle decimal notation with leading zeroes else if( (iExp <= -MAX_DIGITS) && (iExp > -(MAX_DIGITS + iZeroes)) ) { // Integer part and decimal point strcpy( cpNumber, "0." ); iExp += MAX_DIGITS; // Other zeroes for( ; iExp; iExp++ ) strcat( cpNumber, "0" ); // Eliminate trailing zeroes while( caInt[strlen( caInt ) - 1] == '0' ) caInt[strlen( caInt ) - 1] = '/0'; // The rest of the number strcat( cpNumber, caInt ); } else // Handle exponential notation { // Build the number part *cpNumber++ = *caInt; *cpNumber++ = '.'; strcpy( cpNumber, caInt + 1 ); // Convert the exponent to ascii iExp += MAX_DIGITS - 1; itoa( caInt, iExp ); // Build the exponent part strcat( cpNumber, "e" ); if( iExp > 0 ) strcat( cpNumber, "+" ); strcat( cpNumber, caInt ); // Eliminate trailing zeroes while( cpNumber[strlen( cpNumber ) - 1] == '0' ) cpNumber[strlen( cpNumber ) - 1] = '/0'; } // We're done return; } 上面的函数将一个浮点数转换为ASCII字符串形式。另外说一句,我所用的运算法则要求清晰甚于效率。 可移植性问题 站在可移植性的角度上,上面的代码有两个问题。第一个,数据类型int在代码中的使用,这我已经在前边提到过不要随便使用,因为代码将工作在两个不同的平台上。那么我们就不得不深入函数内部修改数据类型。然而我们不希望修改一些传统的函数。即使我们的确那样做了,但在调用ANSI标准函数如atoi()时,也会用到int。 那数据类型int在代码中真是一个问题吗?如果你观察仔细或单步运行代码的话,就会发现这的确是个问题。是的,一些ANSI函数现在还没有定义。但是如果按我的思路,或者学完本章后再回过头来看一下,你会发现有很多地方,特别是在一些传统函数中,int明显的为32位。问题就出在CodeWarrior编译器是16位的。 工程的修正 幸运的是,我们可以通过改变工程的设置将类型int改为32位。知道这一点对我们很有好处。现在,你可能不敢在程序中随便使用int了吧,但是如果你的确使用了,如16位的int,你可以通过修改设置来修正bug(另一个方法是使用类型保护(type-safe)变量) 运行CodeWarrior IDE并打开“计算器”工程,我假定你已保存了一个比较完整的工程副本,否则你会为调试所有的改变而感到痛苦不堪。下面,选择Edit | Caculator Setting进入Code Generation,68K Processor。选中标有“4 byte ints.”的复选框。这样就在编译器中产生了32位的int类型。 App.h内容的添加 当写完这些代码,第二个可移植性问题出现了,就是Palm OS不支持所有的ANSI 标准C的函数调用!所幸的是,我们已有很多办法来解决这个问题。我们通过引入MathLib取代了那些“坏”函数。我们自己还写了一系列的函数使双精度数和ASCII码可相互转换。下面就是我们将要添加的内容: // Definitions for calc.c // #define atoi StrAToI #define itoa StrIToA #define strcat StrCat #define strcpy StrCopy #define strlen StrLen #define strncpy StrNCopy 许多ANSI的函数字符串在Palm OS的字符串处理库中被重新表示。为将它们应用到ANSI标准C中,我们只需像上面那样在开头部分定义就行了。 调试 我建议你将calc.c从CD上拷下来就行了,因为输入这些一般的C代码对学习并没有什么好处。然而,在这里,你将有机会学到如浮点数转换的令人激动的代码。 调试程序,我建议先单步运行calcAppend()和calcExponent()使程序入口能顺利工作。然后调试calEquals(),接着再单步调试传统例程n2ca()和ca2n()。 |