11.5再谈重载:一个矢量类
下面介绍另一种使用了运算符重载和友元的类设计–一个表示矢量的类。这个类还说明了类设计的其他方面,例如,在同一个对象中包含两种描述同一样东西的不同方式等。即使并不关心矢量,也可以在其他情况下使用这里介绍的很多新技术。矢量(vector),是工程和物理中使用的一个术语,它是一个有大小和方向的量。例如,推东西时,推的效果将取决于推力的大小和推的方向。从某个方向推可能会省力,而从相反的方向推则要费很大的劲。为完整地描述汽车的运动情况,应指出其运动速度(大小)和运动方向;如果逆行,则向高速公路的巡警辩解没有超速、超载是徒劳的(免疫学家和计算机专家使用术语矢量的方式不同,请不要考虑这一点,至少在第16章介绍计算机科学版本–vector 模板类之前应如此)。下面的旁注介绍了更多有关矢量的知识,但对于下面的C+示例来说,并不必完全理解这些知识。
## 矢量
假设工蜂发现了一个非凡的花蜜储藏处,它匆忙返回蜂巢,告知其他蜜蜂,该花蜜储藏处离蜂巢 120码。“这种信息是不完整的”,其他蜜蜂感到很茫然–“还必须告知方向!”,该工蜂答道:“太阳方向偏北30度”。知道了距离(大小)和方向,其他的蜜蜂能很快找到蜜源。蜜蜂得矢量。许多数量都有大小和方向。例如,推的效果取决于力气的大小和方向。在计算机屏幕上移动对象时也涉及到距离和方向。可以使用矢量来描述这类问题。例如,可以用失量来描述如何在屏幕上移动(放置)对象,即用箭头从起始位置画到终止位置,来对它作形象化处理。失量的长度是其大小–描述了移动的距离;箭头的指向描述了方向( 参见图 11.1)。表示这种位置变化的矢量称为位移矢量( displacementvector ).
现在,假设您是[hanappa–伟大的毛象猎手。猎狗报告毛象群位于西北 14.1公里处。但由于当时刮的是东南风,您不想从东南方向接近毛象群,因此先向西走了 10公里,再向北走了 10 公里,最终从南面接近毛象群。您知道这两个位移矢量与指向西北的 14.1公里的矢量的方向相同。伟大的毛象猎手 Lhanappa也知道如何将两个矢量相加。
将两个矢量相加有一种简单的几何解释。首先,画一个矢量,然后从第一个矢量的尾部开始画第二个矢量。最后从第一个失量的开始处向第二个矢量的结尾处画一个矢量。第三个矢量表示前两个矢量的和(参见图 11.2)。注意,两个失量之和的长度可能小于它们的长度之和。
显然,应为矢量重载运算符。首先,无法用一个数来表示矢量,因此应创建一个类来表示矢量。其次,矢量与普通数学运算(如加法、减法)有相似之处。这种相似表明,应重载运算符,使之能用于矢量。出于简化的目的,本节将实现一个二维矢量(如屏幕位移),而不是三维矢量(如表示直升机或体操运动员的运动情况)。描述二维矢量只需两个数,但可以选择到底使用哪两个数:
可以用大小(长度)和方向(角度)描述矢量:
可以用分量x和y表示矢量。
两个分量分别是水平矢量(x分量)和垂直天量(y分量),将其相加可以得到最终的矢量。例如,可以这样描述点的运动:向右移动30个单位,再向上移动40个单位(参见图11.3)。这将把该点沿与水平方向呈 53.1度的方向移动50个单位,因此,水平分量为30个单位、垂直分量为40个单位的矢量,与长度为50个单位、方向为53.1度的矢量相同。位移矢量指的是从何处开始、到何处结束,而不是经过的路线。这种表示基本上和第7章在直角坐标与极坐标之间转换的程序中介绍的相同。
程序清单11.15是一个小程序,它使用了修订后的Vector类。该程序模拟了著名的醉鬼走路问题(Drunkard Walk problem)。实际上,醉鬼被认为是一个有许多健康问题的人,而不是大家娱乐消遣的谈资,因此这个问题通常被称为随机漫步问题。其意思是,将一个人领到街灯柱下。这个人开始走动,但每一步的方向都是随机的(与前一步不同)。这个问题的一种表述是,这个人走到离灯柱50英尺处需要多少步。从矢量的角度看,这相当于不断将方向随机的天量相加,直到长度超过50英尺。程序清单 11.15允许用户选择行走距离和步长。该程序用一个变量来表示位置(一个矢量),并报告到达指定距离处(用两种格式表示)所需的步数。可以看到,行走者前进得相当慢。虽然走了1000步,每步的距离为2英尺,但离起点可能只有50英尺。这个程序将行走者所走的净距离(这里为50英尺)除以步数,来指出这种行走方式的低效性。随机改变方向使得该平均值远远小于步长。为了随机选择方向,该程序使用了标准库函数rand()、srand()和time()(参见程序说明)。请务必将程序清单11.14和程序清单 11.15一起进行编译。
//vect.h -- Vector class with <<,mode state
#ifndef VECTOR_H_
#define VECTOR_H_
#include<iostream>
namespace VECTOR
{
class Vector
{
public:
enum Mode{RECT,POL};
//RECT for rectangular,POL for Polar modes
private:
double x;//horizontal value
double y;//vertical value
double mag; //length of value
double ang;//direction of vector in degrees
Mode mode;//RECT or POL
//private methods for setting values
void set_mag();
void set_ang();
void set_x();
void set_y();
public:
Vector();
Vector(double n1, double n2, Mode form = RECT);
void reset(double n1, double n2, Mode form = RECT);
~Vector();
double xval()const { return x; }//report x val
double yval()const { return y; }//report y val
double magVal()const { return mag; }//report magnitude
double angval()const { return ang; }//report angle
void polar_mode();//set mode to POL
void rect_mode();//set mode to RECT
//operator overloading
Vector operator+(const Vector& b)const;
Vector operator-(const Vector& b)const;
Vector operator-( )const;
Vector operator*(double n)const;
//friends
friend Vector operator*(double n, const Vector& a);
friend std::ostream& operator<<(std::ostream& os, const Vector& v);
};// end Vector
}//end namespace VECTOR
#endif
注意,程序清单11.13中4个报告分量值的函数是在类声明中定义的,因此将自动成为内联函数。这些函数非常短,因此适于声明为内联函数。因为它们都不会修改对象数据,所以声明时使用了const限定符。第10章介绍过,这种句法用于声明那些不会对其显式访问的对象进行修改的函数。程序清单 11.14列出了程序清单11.13 中声明的方法和友元函数的定义,该清单利用了名称空间的开放性,将方法定义添加到 VECTOR名称空间中。请注意,构造函数和reset()函数都设置了矢量的直角坐标和极坐标表示,因此需要这些值时,可直接使用而无需进行计算。另外,正如第4章和第7章指出的,C+的内置数学函数在使用角度时以弧度为单位,所以函数在度和弧度之间进行转换。该 Vector 类实现对用户隐藏了极坐标和直角坐标之间的转换以及弧度和度之间的转换等内容。用户只需知道:类在使用角度时以度为单位,可以使用两种等价的形式来表示矢量。
//函数的行为
//vect.cpp == methods for the Vector class
#include<cmath>
#include"vector.h" //includes<iostream>
using std::sqrt;
using std::sin;
using std::cos;
using std::atan;
using std::atan2;
using std::cout;
using std::endl;
namespace VECTOR
{
//comput degrees in one radian
const double Rad_to_deg = 45.0 / atan(1.0);
//should be about 57.2957795130823
//private methods
//calculates magnitude from x and y
void Vector::set_mag()
{
mag = sqrt(x * x+y * y);
}
void Vector::set_ang()
{
if (x == 0.0 && y == 0.0)
{
ang = 0.0;
}
else
{
ang = atan2(y, x);
}
}
//set x from polar coorinate
void Vector::set_x()
{
x = mag * cos(ang);
}
//set y from polar coorinate
void Vector::set_y()
{
y = mag * sin(ang);
}
//public methods
//default constructor
Vector::Vector()
{
y = x = mag = ang = 0.0;
mode = RECT;
}
//construct vector from rectangular coordinates if form is r
//(the default) or else from polay coordinates if form is p
Vector::Vector(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_mag();
set_ang();
}
else
{
cout << "Incorrect 3rd argument to Vector() -- ";
cout << "vector set to 0\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}
//reset vector from rectangular coorinates if form is
//RECT(the default) or else from polar coorinates if
//form is POL
void Vector::reset(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector() -- ";
cout << "vector set to 0\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}
Vector::~Vector()//destructor
{
}
void Vector::polar_mode()//set to polay mode
{
mode = POL;
}
void Vector::rect_mode() // set to rectangular mode
{
mode = RECT;
}
//operator overloading
//add two Vectors
Vector Vector::operator+(const Vector& b)const
{
return Vector(x + b.x, y + b.y);
}
//sub vector b from a
Vector Vector::operator-(const Vector& b)const
{
return Vector(x - b.x, y - b.y);
}
//reverse sign of Vector
Vector Vector::operator-()const
{
return Vector(-x, -y);
}
//multyply vector by n
Vector Vector::operator*(double n)const
{
return Vector(n*x,n*y);
}
//friend methods
//multiply n by Vector a
Vector operator*(double n, const Vector& a)
{
return a * n;
}
//display rectangular coorinates if mode is RECT
//else display polar coordinates if mode is POL
std::ostream& operator<<(std::ostream& os, const Vector& v)
{
if (v.mode == Vector::RECT)
{
os << "(x,y) = (" << v.x << ", " << v.y << ")";
}
else if (v.mode == Vector::POL)
{
os << "(m,a) = (" << v.mag << ", " << v.ang * Rad_to_deg << ")";
}
else
os << "Vector object mode is invalid";
return os;
}
}//end namespace VECTOR
现在,operator-()有两种不同的定义。这是可行的,因为它们的特征标不同。可以定义-运算符的一元和二元版本,因为 C++提供了该运算符的一元和二元版本。对于只有二元形式的运算符(如除法运算符),只能将其重载为二元运算符。
注意:因为运算符重载是通过函数来实现的,所以只要运算符函数的特征标不同,使用的运算符数量与相应的内置 C++运算符相同,就可以多次重载同一个运算符。
#if 1
#include <iostream>
#include<cstdlib> //rand,srand() prototypes
#include<ctime>//time()prototype
#include"vector.h"
int main()
{
using namespace std;
using VECTOR::Vector;
srand(time(0));//seed random-number generator
double direction;
Vector step;
Vector result(0.0, 0.0);
unsigned long steps = 0;
double target;
double dstep;
cout << "Enter target distance(q to quit):";
while (cin >> target)
{
cout << "Enter step length: ";
if (!(cin >> dstep))
{
break;
}
while (result.magVal() < target)
{
direction = rand() % 360;
step.reset(dstep, direction, Vector::POL);
result = result + step;
steps++;
//cout << "steps++ " << steps << ",magVal() = " << result.magVal() << endl;
}
cout << "After " << steps << " steps,the subject "
"has the following location:\n";
cout << result << endl;
result.polar_mode();
cout << " or\n" << result << endl;
cout << "Average outward distance per step = "
<< result.magVal() / steps << endl;
steps = 0;
result.reset(0.0, 0.0);
cout << "Enter target distance(q to quit): ";
}
cout << "Bye!\n";
cin.clear();
while (cin.get() != '\n')
{
continue;
}
return 0;
}
#endif
该程序使用using声明导入了Vector,因此该程序可使用Vector::POL,而不必使用VECTOR:Vector::POL.
这种处理的随机性使得每次运行结果都不同,即使初始条件相同。然而,平均而言,步长减半,步数将为原来的4倍。概率理论表明,平均而言,步数(N)、步长(s),净距离D之间的关系如下:
N=(D/s)的N次方
这只是平均情况,但每次试验结果可能相差很大。例如,进行1000次试验(走50英尺,步长为2英尺)时,平均步数为636(与理论值625 非常接近),但实际步数位于91~3951。同样,进行1000 次试验(走 50英尺,步长为1英尺)时,平均步数为 2557(与理论值2500 非常接近),但实际步数位于 345~10882因此,如果发现自己在随机漫步时,请保持自信,迈大步走。虽然在蜿蜒前进的过程中仍旧无法控制前进的方向,但至少会走得远一点。
程序说明
首先需要指出的是,在程序清单11.15中使用 VECTOR 名称空间非常方便。下面的 using 声明使 Vector类的名称可用:
using VECTOR::Vector;
因为所有的 Vector 类方法的作用域都为整个类,所以导入类名后,无需提供其他using 声明,就可以使用 Vector 的方法。
总结:后面有本问题的另外一种处理方法:醉汉回家超链接