2.4 类和参数
每个程序员都需要对函数和参数有一个深刻的理解,而且由于类可以作为函数的参数类型或者函数返回值的类型,因此对OOP领域也需要更为深入的理解。本节将阐述一些属于上述类型的函数,其中包括对不同类型参数的回顾。本节中的示例将使用称为point的新类。
2.4.1 编程示例:point类
我们下面将要编写的新类是用于存储和操作平面中一个单独点的位置的数据类型,如图2-9所示。图2-9(a)所示的示例点位于坐标x =–1.0,y = 0.8的位置。point类具有下列成员函数:
● 用于初始化点的构造函数。构造函数的参数使用默认参数,后者将随后讨论。
● 用于移动点的成员函数,能够根据给定值将点沿x轴和y轴移动,如图2-9(b)所示。
● 用于旋转点的成员函数,能够将点绕原点顺时针方向旋转90°,如图2-9(c)所示。
● 两个常量成员函数,用于查询点的当前x坐标和y坐标。

图2-9 平面中的三个点
以上函数都比较简单,但是它们却构成了应用于绘图程序或其他图形应用程序的真实数据类型的基础。包括构造函数在内,所有的成员函数在图2-10所示的头文件中列出,各个成员函数的实现如图2-11所示。在浏览完毕这些图之后,我们将回顾这些函数的实现,并从默认参数(default argument)开始讲解,这是point构造函数一个很有意思的内容。
头文件
// FILE: point.h
// CLASS PROVIDED: point (part of the namespace main_savitch_chapter2A)
//
// CONSTRUCTOR for the point class:
// point(double initial_x = 0.0, double initial_y = 0.0)
// Postcondition: The point has been set to (initial_x, initial_y).
//
// MODIFICATION MEMBER FUNCTIONS for the point class:
图2-10 point类的头文件
// void shift(double x_amount, double y_amount)
// Postcondition: The point has been moved by x_amount along the x axis
// and by y_amount along the y axis.
//
// void rotate90( )
// Postcondition: The point has been rotated clockwise 90 degrees around
// the origin.
//
// CONSTANT MEMBER FUNCTIONS for the point class:
// double get_x( ) const
// Postcondition: The value returned is the x coordinate of the point.
//
// double get_y( ) const
// Postcondition: The value returned is the y coordinate of the point.
//
// VALUE SEMANTICS for the point class:
// Assignments and the copy constructor may be used with point objects.
#ifndef MAIN_SAVITCH_POINT_H
#define MAIN_SAVITCH_POINT_H
namespace main_savitch_2A
{
class point
{
public:
// CONSTRUCTOR
point(double initial_x = 0.0, double initial_y = 0.0);
// MODIFICATION MEMBER FUNCTIONS
void shift(double x_amount, double y_amount);
void rotate90( );
// CONSTANT MEMBER FUNCTIONS
double get_x( ) const { return x; }
double get_y( ) const { return y; }
private:
double x; // x coordinate of this point
double y; // y coordinate of this point
};
}
#endif
图2-10 (续)
实现文件
// FILE: point.cxx
// CLASS IMPLEMENTED: point (See point.h for documentation)
#include "point.h"
namespace main_savitch_2A
{
point::point(double initial_x, double initial_y)
{ // Constructor sets the point to a given position.
x = initial_x;
y = initial_y;
}
void point::shift(double x_amount, double y_amount)
{
x += x_amount;
y += y_amount;
}
void point::rotate90( )
{
double new_x;
double new_y;
new_x = y; // For a 90 degree clockwise rotation, the new x is the
new_y = -x; // original y, and the new y is -1 times the original x.
x = new_x;
y = new_y;
}
}
图2-11 point类的实现文件
2.4.2 默认参数
当程序员在调用函数时,如果并未提供实际的参数,那么编译器将会为参数使用默认的数值,即默认参数(default argument)。默认参数可以在函数的原型中列出。举例来说,对于1.1节自测习题3中的函数原型,一个更改后的版本如下所示:
int date_check(int year, int month = 1, int day = 1);
date_check的实际行为并不重要,重要是这里为参数month和day赋予了默认参数。如该示例中的阴影部分所示,默认参数出现于参数名称之后,两者用等号相连。一旦默认参数有效,那么在调用函数时可以使用给定的参数,也可以不使用。
举例来说,程序可以只用参数year调用函数date_check:
date_check(2000);
由于该函数调用忽略了后面的两个参数,因此将会使用默认参数(month = 1和day = 1)。该函数调用与调用date_check(2000, 1, 1)完全相同。
函数调用也可以使用参数year和month,而忽略day,如下示例所示:
date_check(2000, 7);
在这个示例中,将使用day的默认参数,因此这里的函数调用与调用date_check(2000, 7, 1)相同。
图2-12总结了提供和使用默认参数的一般规则。默认参数特别有利于构造函数,例如图2-10中的point构造函数,该构造函数的原型具有下述两个默认参数:
point(double initial_x = 0.0, double initial_y = 0.0);
该构造函数的两个参数使用double型数字0.0作为默认值,下面三个point对象的声明展示了这两个默认参数的使用方法:
point a(-1, 0.8);
//使用通常的带有两个参数的构造函数。
point b(-1);
//使用-1作为第一个参数,使用默认参数initial_y = 0.0作为第二个参数。
point c;
//同时使用默认参数initial_x = 0.0和initial_y = 0.0。
上述第三种构造函数的使用方法——只是简单的point c;——值得关注,因为这里为两个参数使用了默认值。与之对应,必须有一个不带参数的构造函数。不带参数的构造函数是一个默认构造函数(default constructor)。我们在throttle类中已经提到,总是为类提供默认构造函数十分重要,而提供默认构造函数的一种方法就是使用所有参数均为默认参数的构造函数。
编程提示 ![]()
使用默认参数提供默认构造函数
提供默认构造函数的一种较好方法是将一个构造函数的所有参数都赋予默认参数。使用默认构造函数时通常不需要参数列表,甚至不需要圆括号,因此在使用point的默认构造函数时,只需书写point c;。
默认参数
默认参数是一个数值,当没有为函数提供实参时将把默认参数作为参数来使用。默认参数的使用格式和规则列举如下:
在函数原型参数列表中的语法:
<type name> <variable name> = <default value>
举例:
int date_check(int year, int month = 1, int date = 1);
(1) 默认参数只是在函数原型中指定一次,而并不是在函数实现中指定。
(2) 对于具有多个参数的函数,并不需要为每个参数都指定默认参数。但是,如果只是部分参数具有默认值,那么这些参数必须被置于参数列表的最右端。
(3) 在函数调用中,从实际的参数列表的最右端开始,具有默认值的参数可以被忽略。例如:
date_check(2000);
//使用默认参数month = 1和date = 1
date_check(2000, 7);
//使用默认参数date = 1
date_check(2000, 7, 22);
//并未使用任何默认参数
图2-12 默认参数
2.4.3 参数
像其他数据类型一样,类也可以作为函数参数的类型来使用。下面将回顾三种不同的参数类型,并使用新的point类作为示例。
值参数(value parameter) 值参数是一种最简单的参数。为了便于讲解值参数,下面我们编写一个简单的函数。该函数具有一个值参数,称为点p。函数的返回值是一个整数,表明将点p经90°旋转操作从而最终旋转到右上象限所需的旋转次数,如图2-13所示。

图2-13 旋转的点
下面是这个函数的实现:
int rotations_needed(point p)
// Postcondition: The value returned is the number of 90-degree
// clockwise rotations needed to move p into the upper-right
// quadrant (where x >= 0 and y >= 0).
{
int answer;
answer = 0;
while((p.get_x( ) < 0) || (p.get_y( ) < 0))
{
p.rotate90( );
++answer;
}
return answer;
}
在C++中,值参数的声明方法是在类型名称的后面放置参数名称,因此,在上述参数列表中书写的是point p。值参数的使用效果是:在函数体中对参数所作的更改不会改变调用程序中的实参。考虑如下的一段程序示例:
point sample(6, -4); // Constructor places the point at x = 6, y = -4.
cout << " x coordinate is " << sample.get_x( )
<< " y coordinate is " << sample.get_y( ) << endl;
cout << " Rotations: " << rotations_needed(sample) << endl;
cout << " x coordinate is " << sample.get_x( )
<< " y coordinate is " << sample.get_y( ) << endl;
构造函数之后的代码输出一则有关点坐标的消息。随后,在第二条输出语句中,调用了函数rotations_needed。该函数的参数(在本例中为p)指的是形式参数(formal parameter,下文简称形参),它与函数调用期间传递的值相区别。函数调用期间传递的值(本例中为sample)是实际参数(actual argument或者actual parameter,下文简称实参)。
通过使用值参数,实参为形参提供了初始值。更为准确的说法是,形参实现为函数的一个局部变量,而类的复制构造函数将形参初始化为实参的一个副本。这是实参和形参之间唯一的联系。因此,如果形参p在函数体中发生改变,调用程序中的实参sample将会保持不变。在上述示例中,p将会旋转3次,最终位于x = 4,y = 6的位置。而函数返回旋转次数(3),调用程序中的sample仍将保持原始值。从而,代码的完整输出将如下所示:
x coordinate is 6 y coordinate is -4
Rotations: 3
x coordinate is 6 y coordinate is -4
实参的值sample并没有发生改变。
值参数
通过在类型名称的后面放置参数名称的方法声明值参数。对于值参数,实参为形参提供了初始值。值参数作为函数的局部变量实现,这使得在函数体内对值参数所作的更改并不会对实参造成影响。
举例:
int rotations_needed(point p);
引用参数(Reference parameter) 引用参数是C++中重要的参数类型。我们将采用的引用参数的例子与函数rotations_needed类似,但是这一次,点p将会是引用参数。新的函数没有返回值,它仅仅是将p旋转到右上象限,如下所示:
void rotate_to_upper_right(point& p)
// Postcondition: The value returned is the number of 90-degree
// clockwise rotations needed to move p into the upper-right
// quadrant (where x >= 0 and y >= 0).
{
while((p.get_x( ) < 0) || (p.get_y( ) < 0))
p.rotate90( );
}
在C++中,引用参数的声明方式为:在类型名称的后面紧跟符号&和参数名称,因此在上述参数列表中书写point& p。
引用参数的要点是:在函数体中对引用参数的使用将会访问到调用函数中的实参。让我们来看如下的一段程序示例:
point sample(6, -4); // Constructor places the point at x = 6, y = -4.
cout << " x coordinate is " << sample.get_x( )
<< " y coordinate is " << sample.get_y( ) << endl;
rotate_to_upper_right(sample);
cout << " x coordinate is " << sample.get_x( )
<< " y coordinate is " << sample.get_y( ) << endl;
与前一示例相同,代码首先输出点的坐标,然后调用函数rotate_to_upper_right。形参仍旧称为p,实参仍旧为sample——但此时p是一个引用参数。
由于p是一个引用参数,因此函数体中任何对p的使用都将会实际访问到sample。因而,实参sample将会旋转到右上象限。当函数返回时,sample将具有新的值。代码完整的输出如下所示:
x coordinate is 6 y coordinate is -4
x coordinate is 4 y coordinate is 6
实参sample的值已经被函数更改。
引用参数
引用参数的声明方式为:在类型名称的后面紧跟字符&和参数名称。对于引用参数,在函数体中对参数的使用将会访问到调用函数中的实参。函数体中对形参的更改将会改变实参。
举例:
void rotate_to_upper_right(point& p);
隐患![]()
对引用参数使用错误的实参类型
为了使引用参数能够正常工作,实参的数据类型必须与引用参数的数据类型精确匹配。举例来说,假设我们使用如下的引用参数:
void make_int_42(int& i)
// Postcondition: i has been set to 42.
{
i = 42;
}
假设j是一个整型变量,并作函数调用make_int_42(j)。在函数返回之后,j将具有值42。但如下代码的输出将会令人惊奇:
double d;
d = 0;
make_int_42(d);
//并不改变d
cout << d;
//输出0
尽管这段示例代码可以通过编译,但由于d是错误的数据类型,因此将会创建一份单独的d的integer副本,并作为实参使用,而d的double型变量并不会改变为42。
如果实参的数据类型与形参的数据类型并不精确匹配,那么编译器会试图将实参转变为正确的类型。如果这种转变行得通,那么编译器将会把实参当作值参数来对待,从而传递给函数一份实参的副本。幸运的是,大多数编译器将会提供一则警告消息,例如“Temporary used for parameter 'i' in call to 'make_int_42'”——我们应当确保会留意编译器的警告!
常量引用参数(const reference parameter) 对于大型数据类型来说,值参数要比引用参数的效率低,这是因为值参数必须在函数体内创建实参额外的副本。因此,我们通常更倾向于使用引用参数。但引用参数也并不总是受人青睐,因为我们不希望让程序员过分担心函数是否改变了实参。使用引用参数就一定会改变实参,但是这种改变不会发生在值参数中。
有的时候会有一种解决办法,它既提供了引用参数的高效性,又具有值参数的安全性。这种新的参数类型称为常量引用参数,它可以应用于函数不会试图改变参数的情况。举例来说,假设要编写一个函数用于计算两点之间的距离。该函数使用两个点作为参数,并且不会更改其中任何一个参数,因此我们可以使用常量引用参数,如图2-14所示。图中展示了计算两点间距离的函数,其函数原型如下:
double distance(const point&p1, const point& p2);
函数实现
double distance(const point&
p1, const point&p2)
// Postcondition: The value returned is the
// distance between p1 and p2.
// Library facilities used: cmath
{
double a, b, c_squared;
// Calculate differences in x and y coordinates.
a = p1.get_x( ) - p2.get_x( );
// Difference in x coordinates
b = p1.get_y( ) - p2.get_y( );
// Difference in y coordinates
// Use Pythagorean Theorem to calculate the square of the distance
// between the points.
c_squared = a*a + b*b;
return sqrt(c_squared);
}
图2-14 带有常量引用参数的函数
常量引用参数在参数类型之前使用关键字const,而且在类型名称后面也使用符号&。常量引用参数是高效的(因为它属于引用参数),而且它也向程序员保证实参不会被函数更改。例如,在distance的实现中只使用了get_x和get_y,两者都是常量成员函数,因此不会改变p1和p2。将get_x和get_y实际声明为常量成员函数尤为重要,否则,编译器将不允许使用这两个函数来操作常量引用参数p1和p2。

编程提示 ![]()
一致性地使用const
当我们定义了一个新类,并利用函数和成员函数操作该类时,应当一致性地使用const。特别是:
(1) 任何不改变对象值的成员函数都应当声明为常量成员函数。其做法是:同时在函数原型和函数定义头的参数列表后面放置关键字const。例如,throttle类中的flow函数原型如下所示:
double flow( ) const;
(2) 无论何时,只要使用类作为参数类型而且函数并不更改这个参数,则应当使用常量引用参数。其方法是:在参数列表中的参数类型前面放置关键字const,在类型名称后面放置符号&。例如下面的原型:
double distance(const point& p1, const point& p2);
除非我们在所有满足上述要求的地方都使用const,否则不要使用它。
2.4.4 当函数返回值的类型是类时
函数返回值的类型可以是类,下面是它的一个典型示例,函数返回的点如图2-15所示:
point middle(const point& p1, const point&p2)
// Postcondition: The value returned is the point that
// is halfway between p1 and p2.
{
double x_midpoint, y_midpoint;
// Compute the x and y midpoints.
x_midpoint = (p1.get_x( ) + p2.get_x( )) / 2;
y_midpoint = (p1.get_y( ) + p2.get_y( )) / 2;
// Construct a new point and return it.
point midpoint(x_midpoint, y_midpoint);
return midpoint;
}

图2-15 函数middle返回的点
这个函数计算出了一个新点,并赋给局部变量midpoint,接着返回该点的一个副本。通常,函数的返回值存储在诸如midpoint的局部变量中,但也并非总是这样。下面是另一个示例,其中函数的一个参数就是返回值:
throttle slower(const throttle& t1, const throttle& t2)
// Postcondition: The value returned is a copy of t1 or t2, whichever
// has the slower flow. If the flows are equal, then t1 is returned.
{
if (t1.flow( ) <= t2.flow( ))
return t1;
else
return t2;
}
顺便说一句,在把值返回给调用函数之前,C++返回语句使用复制构造函数将函数的返回值复制到一个临时位置。
2.4.5 自测习题
22. 为2.2.4节中的自测习题12的throttle构造函数添加默认参数。如果完成了这个工作,是否还需要另外两个构造函数?
23. 下面的函数调用中,哪一个调用能够改变点p的值:
cout << rotations_needed(p);
rotate_to_upper_right(p);
24. 形参和实参之间的区别是什么?
25. 假设函数具有一个参数x,函数体将会改变x的值。在什么情况下x应当为值参数?在什么情况下x应当为引用参数?对于这个函数,x可以为常量引用参数吗?
26. 假设一个参数的数据类型是类,在函数内部不能更改这个参数。为了实现这一目的,哪一种参数既高效又安全?







