11-更多数组函数示例
7.3.3 更多数组函数示例
选择使用数组来表示数据时,实际上是在进行一次设计方面的决策。但设计决策不仅仅是确定数据的存储方式,还涉及如何使用数据。程序员常会发现,编写特定的函数来处理特定的数据操作是有好处的(这里讲的好处指的是程序的可靠性更高、修改和调试更为方便)。另外,构思程序时将存储属性与操作结合起来,便是朝OOP思想迈进了重要的一步;以后将证明这是很有好处的。
来看一个简单的案例。假设要使用一个数组来记录房地产的价值(假设拥有房地产)。在这种情况下,程序员必须确定要使用哪种类型。当然,double的取值范围比int和long大,并且提供了足够多的有效位数来精确地表示这些值。接下来必须决定数组元素的数目(对于使用new创建的动态数组来说,可以稍后再决定,但我们希望使事情简单一点)。如果房地产数目不超过5个,则可以使用一个包含5个元素的double数组。
现在,考虑要对房地产数组执行的操作。两个基本的操作分别是,将值读入到数组中和显示数组内容。我们再添加另一个操作:重新评估每种房地产的值。为简单起见,假设所有房地产都以相同的比率增加或者减少。(别忘了,这是一本关于C++的书,而不是关于房地产管理的书。)接下来,为每项操作编写一个函数,然后编写相应的代码。下面首先介绍这些步骤,然后将其用于一个完整的示例中。
1.填充数组
由于接受数组名参数的函数访问的是原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。该函数的一个参数是要填充的数组的名称。通常,程序可以管理多个人的投资,因此需要多个数组,因此不能在函数中设置数组长度,而要将数组长度作为第二个参数传递,就像前一个示例那样。另外,用户也可能希望在数组被填满之前停止读取数据,因此需要在函数中建立这种特性。由于用户输入的元素数目可能少于数组的长度,因此函数应返回实际输入的元素数目。因此,该函数的原型如下:
int fill_array(double ar[], int limit);
该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。例如,如果使用该函数来处理一个包含5个元素的数组,则将5作为第二个参数。如果只输入3个值,则该函数将返回3。
可以使用循环连续地将值读入到数组中,但如何提早结束循环呢?一种方法是,使用一个特殊值来指出输入结束。由于所有的属性都不为负,因此可以使用负数来指出输入结束。另外,该函数应对错误输入作出反应,如停止输入等。这样,该函数的代码如下所示:
int fill_array(double ar[], int limit)
{
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value #" << (i + 1) << ": ";
cin >> temp;
if (!cin) // bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0) // signal to terminate
break;
ar[i] = temp;
}
return i;
}
注意,代码中包含了对用户的提示。如果用户输入的是非负值,则这个值将被赋给数组,否则循环结束。如果用户输入的都是有效值,则循环将在读取最大数目的值后结束。循环完成的最后一项工作是将i加1,因此循环结束后,i将比最后一个数组索引大1,即等于填充的元素数目。然后,函数返回这个值。
2.显示数组及用const保护数组
创建显示数组内容的函数很简单。只需将数组名和填充的元素数目传递给函数,然后该函数使用循环来显示每个元素。然而,还有另一个问题——确保显示函数不修改原始数组。除非函数的目的就是修改传递给它的数据,否则应避免发生这种情况。使用普通参数时,这种保护将自动实现,这是由于C++按值传递数据,而且函数使用数据的副本。然而,接受数组名的函数将使用原始数据,这正是fill_array()函数能够完成其工作的原因。为防止函数无意中修改数组的内容,可在声明形参时使用关键字const(参见第3章):
void show_array(const double ar[], int n);
该声明表明,指针ar指向的是常量数据。这意味着不能使用ar修改该数据,也就是说,可以使用像ar[0]这样的值,但不能修改。注意,这并不是意味着原始数组必须是常量,而只是意味着不能在show_array()函数中使用ar来修改这些数据。因此,show_array()将数组视为只读数据。假设无意间在show_array()函数中执行了下面的操作,从而违反了这种限制:
ar[0] += 10;
编译器将禁止这样做。例如,Borland C++将给出一条错误消息,如下所示(稍作了编辑):
Cannot modify a const object in function
show_array(const double *,int)
其他编译器可能用其他措词表示其不满。
这条消息提醒用户,C++将声明const double ar [ ]解释为const double *ar。因此,该声明实际上是说,ar指向的是一个常量值。结束这个例子后,我们将详细讨论这个问题。下面是show_array()函数的代码:
void show_array(const double ar[], int n)
{
using namespace std;
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << ar[i] << endl;
}
}
3.修改数组
在这个例子中,对数组进行的第三项操作是将每个元素与同一个重新评估因子相乘。需要给函数传递3个参数:因子、数组和元素数目。该函数不需要返回值,因此其代码如下:
void revalue(double r, double ar[], int n)
{
for (int i = 0; i < n; i++)
ar[i] *= r;
}
由于这个函数将修改数组的值,因此在声明ar时,不能使用const。
4.将上述代码组合起来
至此,您根据数据的存储方式(数组)和使用方式(3个函数)定义了数据的类型,因此可以将它们组合成一个程序。由于已经建立了所有的数组处理工具,因此main()的编程工作非常简单。该程序检查用户输入的是否是数字,如果不是,则要求用户这样做。余下的大部分编程工作只是让main()调用前面开发的函数。程序清单7.7列出了最终的代码,它将编译指令using放在那些需要iostream工具的函数中。
程序清单7.7 arrfun3.cpp
// arrfun3.cpp -- array functions and const
#include <iostream>
const int Max = 5;
// function prototypes
int fill_array(double ar[], int limit);
void show_array(const double ar[], int n); // don't change data
void revalue(double r, double ar[], int n);
int main()
{
using namespace std;
double properties[Max];
int size = fill_array(properties, Max);
show_array(properties, size);
if (size > 0)
{
cout << "Enter revaluation factor: ";
double factor;
while (!(cin >> factor)) // bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; Please enter a number: ";
}
revalue(factor, properties, size);
show_array(properties, size);
}
cout << "Done.\n";
cin.get();
cin.get();
return 0;
}
int fill_array(double ar[], int limit)
{
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value #" << (i + 1) << ": ";
cin >> temp;
if (!cin) // bad input
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0) // signal to terminate
break;
ar[i] = temp;
}
return i;
}
// the following function can use, but not alter,
// the array whose address is ar
void show_array(const double ar[], int n)
{
using namespace std;
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << ar[i] << endl;
}
}
// multiplies each element of ar[] by r
void revalue(double r, double ar[], int n)
{
for (int i = 0; i < n; i++)
ar[i] *= r;
}
下面两次运行该程序时的输出:
Enter value #1: 100000
Enter value #2: 80000
Enter value #3: 222000
Enter value #4: 240000
Enter value #5: 118000
Property #1: $100000
Property #2: $80000
Property #3: $222000
Property #4: $240000
Property #5: $118000
Enter revaluation factor: 0.8
Property #1: $80000
Property #2: $64000
Property #3: $177600
Property #4: $192000
Property #5: $94400
Done.
Enter value #1: 200000
Enter value #2: 84000
Enter value #3: 160000
Enter value #4: -2
Property #1: $200000
Property #2: $84000
Property #3: $160000
Enter reevaluation factor: 1.20
Property #1: $240000
Property #2: $100800
Property #3: $192000
Done.
函数fill_array()指出,当用户输入5项房地产值或负值后,将结束输入。第一次运行演示了输入5项房地产值的情况,第二次运行演示了输入负值的情况。
5.程序说明
前面已经讨论了与该示例相关的重要编程细节,因此这里回顾一下整个过程。我们首先考虑的是通过数据类型和设计适当的函数来处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计(bottom-up programming),因为设计过程从组件到整体进行。这种方法非常适合于OOP——它首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计(top-down programming),首先指定模块化设计方案,然后再研究细节。这两种方法都很有用,最终的产品都是模块化程序。
6.数组处理函数的常用编写方式
假设要编写一个处理double数组的函数。如果该函数要修改数组,其原型可能类似于下面这样:
void f_modify(double ar[], int n);
如果函数不修改数组,其原型可能类似于下面这样:
void _f_no_change(const double ar[], int n);
当然,在函数原型中可以省略变量名,返回类型也可以是void之外的其他类型。这里的要点是,ar实际上是一个指针,指向传入的数组的第一个元素;另外,由于通过参数传递了元素数,这两个函数都可使用任何长度的数组,只要数组的类型为double:
double rewards[1000];
double faults[50];
...
f_modify(rewards, 1000);
f_modify(faults, 50);
这种做法是通过传递两个数字(数组地址和元素数)实现的。正如你看到的,函数缺少一些有关原始数组的知识;例如,它不能使用sizeof来获悉原始数组的长度,而必须依赖于程序员传入正确的元素数。