第11章-cpp使用类

本章内容包括:
• 运算符重载。
• 友元函数。
• 重载<<运算符,以便用于输出。
• 状态成员。
• 使用rand()生成随机值。
• 类的自动转换和强制类型转换。
• 类转换函数。

运算符重载:

运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多种含义。实际上,很多C++(也包括C语言)运算符已经被重载。例如,将*运算符用于地址,将得到存储在这个地址中的值;但将它用于两个数字时,得到的将是它们的乘积。C++根据操作数的数目和类型来决定采用哪种操作。

C++允许将运算符重载扩展到用户定义的类型,例如,允许使用+将两个对象相加。编译器将根据操作数的数目和类型决定使用哪种加法定义。重载运算符可使代码看起来更自然。例如,将两个数组相加是一种常见的运算。通常,需要使用下面这样的for循环来实现:

for (int i = 0; i < 20, i++) {
    evening[i] = sam[i] + janet[i];
}

但在C++中,可以定义一个表示数组的类,并重载+运算符。于是便可以有这样的语句:

evening = sam + janet; // 两个数组相加

这种简单的加法表示法隐藏了内部机理,并强调了实质,这是OOP的另一个目标。

要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:

operatorop(argument-list)

例如,operator+( )重载+运算符,operator*( )重载*运算符。op必须是有效的C++运算符,不能虚构一个新的符号。例如,不能有operator@( )这样的函数,因为C++中没有@运算符。然而,operator 函数将重载[ ]运算符,因为[ ]是数组索引运算符。例如,假设有一个Salesperson类,并为它定义了一个operator+( )成员函数,以重载+运算符,以便能够将两个Saleperson对象的销售额相加,则如果district2、sid和sara都是Salesperson类对象,便可以编写这样的等式:

district2 = sid + sara;

编译器发现,操作数是Salesperson类对象,因此使用相应的运算符函数替换上述运算符:

district2 = sid.operator+(sara);

然后该函数将隐式地使用sid(因为它调用了方法),而显式地使用sara对象(因为它被作为参数传递),来计算总和,并返回这个值。当然最重要的是,可以使用简便的+运算符表示法,而不必使用笨拙的函数表示法。

虽然C++对运算符重载做了一些限制,但了解重载的工作方式后,这些限制就很容易理解了。因此,下面首先通过一些示例对运算符重载进行阐述,然后再讨论这些限制。

计算时间:一个运算符重载示例

如果今天早上在Priggs的账户上花费了2小时35分钟,下午又花费了2小时40分钟,则总共花了多少时间呢?这个示例与加法概念很吻合,但要相加的单位(小时与分钟的混合)与内置类型不匹配。第7章通过定义一个travel_time结构和将这种结构相加的sum( )函数来处理类似的情况。现在将其推广,采用一个使用方法来处理加法的Time类。

// mytime0.h -- Time class before operator overloading
#ifndef MYTIME0_H_
#define MYTIME0_H_
class Time {
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void AddMin(int m);
	void AddHr(int h);
	void Reset(int h = 0, int m = 0);
	const Time Sum(const Time& t) const;
	void Show() const;
};
#endif
// mytime0.cpp  -- implementing Time methods
#include <iostream>
#include "mytime0.h"
Time::Time() {
	hours = minutes = 0;
}
Time::Time(int h, int m) {
	hours = h;
	minutes = m;
}
void Time::AddMin(int m) {
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}
void Time::AddHr(int h) {
	hours += h;
}
void Time::Reset(int h, int m) {
	hours = h;
	minutes = m;
}
const Time Time::Sum(const Time& t) const {
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes / 60;
	sum.minutes %= 60;
	return sum;
}
void Time::Show() const {
	std::cout << hours << " hours, " << minutes << " minutes";
}
// usetime0.cpp -- using the first draft of the Time class
// compile usetime0.cpp and mytime0.cpp together
#include <iostream>
#include "mytime0.h"
int main() {
	using std::cout;
	using std::endl;
	Time planning;
	Time coding(2, 40);
	Time fixing(5, 55);
	Time total;

	cout << "planning time = ";
	planning.Show();
	cout << endl;

	cout << "coding time = ";
	coding.Show();
	cout << endl;

	cout << "fixing time = ";
	fixing.Show();
	cout << endl;

	total = coding.Sum(fixing);
	cout << "coding.Sum(fixing) = ";
	total.Show();
	cout << endl;
	return 0;
}
planning time = 0 hours, 0 minutes
coding time = 2 hours, 40 minutes
fixing time = 5 hours, 55 minutes
coding.Sum(fixing) = 8 hours, 35 minutes

添加加法运算符

将Time类转换为重载的加法运算符很容易,只要将Sum( )的名称改为operator +( )即可。这样做是对的,只要把运算符(这里为+)放到operator的后面,并将结果用作方法名即可。在这里,可以在标识符中使用字母、数字或下划线之外的其他字符。

// mytime1.h -- Time class before operator overloading
#ifndef MYTIME1_H_
#define MYTIME1_H_
class Time {
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void AddMin(int m);
	void AddHr(int h);
	void Reset(int h = 0, int m = 0);
	Time operator+(const Time& t) const;
	void Show() const;
};
#endif
// mytime1.cpp  -- implementing Time methods
#include <iostream>
#include "mytime1.h"
Time::Time() {
	hours = minutes = 0;
}
Time::Time(int h, int m) {
	hours = h;
	minutes = m;
}
void Time::AddMin(int m) {
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}
void Time::AddHr(int h) {
	hours += h;
}
void Time::Reset(int h, int m) {
	hours = h;
	minutes = m;
}
Time Time::operator+(const Time& t) const {
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes / 60;
	sum.minutes %= 60;
	return sum;
}
void Time::Show() const {
	std::cout << hours << " hours, " << minutes << " minutes";
}
// usetime1.cpp -- using the second draft of the Time class
// compile usetime1.cpp and mytime1.cpp together
#include <iostream>
#include "mytime1.h"
int main() {
	using std::cout;
	using std::endl;
	Time planning;
	Time coding(2, 40);
	Time fixing(5, 55);
	Time total;

	cout << "planning time = ";
	planning.Show();
	cout << endl;

	cout << "coding time = ";
	coding.Show();
	cout << endl;

	cout << "fixing time = ";
	fixing.Show();
	cout << endl;

	total = coding + fixing;
	// operator notation
	cout << "coding + fixing = ";
	total.Show();
	cout << endl;

	Time morefixing(3, 28);
	cout << "more fixing time = ";
	morefixing.Show();
	cout << endl;
	total = morefixing.operator+(total);
	// function notation
	cout << "morefixing.operator+(total) = ";
	total.Show();
	cout << endl;
	return 0;
}
planning time = 0 hours, 0 minutes
coding time = 2 hours, 40 minutes
fixing time = 5 hours, 55 minutes
coding + fixing = 8 hours, 35 minutes
more fixing time = 3 hours, 28 minutes
morefixing.operator+(total) = 12 hours, 3 minutes

总之,operator+()函数的名称使得可以使用函数表示法或运算符表示法来调用它。编译器将根据操作数的类型来确定如何做:

int a, b, c;
Time A, B, C;
c = a + b;
C = A + B;

可以将两个以上的对象相加吗?例如,如果t1、t2、t3和t4都是Time对象,可以这样做吗?

t4 = t1 + t2 + t3;

以上可以转换为:

t4 = t1.operator+(t2+t3);
再转换:
t4 = t1.operator+(t2.operator+(t3));

所以前面的连加运算是合法的。

可以在代码中直接写如下代码:

std::string answer = "hello";
if (operator==("hello", answer)) {
    ...
}

重载的限制

                      可重载的运算符

多数C++运算符(参见上表)都可以用这样的方式重载。重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。

1.重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将减法运算符(−)重载为计算两个double值的和,而不是它们的差。虽然这种限制将对创造性有所影响,但可以确保程序正常运行。

2.使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符(%)重载成使用一个操作数:

int x;
Time shiva;
% x; // 无效的取模操作符
% shiva; // 无效的重载操作符

其他重载运算符

还有一些其他的操作对Time类来说是有意义的。例如,可能要将两个时间相减或将时间乘以一个因子,这需要重载减法和乘法运算符。这和重载加法运算符采用的技术相同,即创建operator–()和operator*()方法。也就是说,将下面的原型添加到类声明中:

Time operator-(const Time & t) const;
Time operator*(double n) const;
// mytime2.h -- Time class after operator overloading
#ifndef MYTIME2_H_
#define MYTIME2_H_
class Time {
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0); // 可以直接调用Time(int h)默认了int m = 0的参数
	void AddMin(int m);
	void AddHr(int h);
	void Reset(int h = 0, int m = 0); // 可以直接调用Reset(),参数自动默认了。
	Time operator+(const Time& t) const;
	Time operator-(const Time& t) const;
	Time operator*(double n) const;
	void Show() const;
};
#endif
// mytime2.cpp  -- implementing Time methods
#include <iostream>
#include "mytime2.h"
Time::Time() {
	hours = minutes = 0;
}
Time::Time(int h, int m) {
	hours = h;
	minutes = m;
}
void Time::AddMin(int m) {
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}
void Time::AddHr(int h) {
	hours += h;
}
void Time::Reset(int h, int m) {
	hours = h;
	minutes = m;
}
Time Time::operator+(const Time& t) const {
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes / 60;
	sum.minutes %= 60;
	return sum;
}
Time Time::operator-(const Time& t) const {
	Time diff;
	int tot1, tot2;
	tot1 = t.minutes + 60 * t.hours;
	tot2 = minutes + 60 * hours;
	diff.minutes = (tot2 - tot1) % 60;
	diff.hours = (tot2 - tot1) / 60;
	return diff;
}
Time Time::operator*(double mult) const {
	Time result;
	long totalminutes = hours * mult * 60 + minutes * mult;
	result.hours = totalminutes / 60;
	result.minutes = totalminutes % 60;
	return result;
}
void Time::Show() const {
	std::cout << hours << " hours, " << minutes << " minutes";
}
// usetime2.cpp -- using the third draft of the Time class
// compile usetime2.cpp and mytime2.cpp together
#include <iostream>
#include "mytime2.h"
int main() {
	using std::cout;
	using std::endl;
	Time weeding(4, 35);
	Time waxing(2, 47);
	Time total;
	Time diff;
	Time adjusted;

	cout << "weeding time = ";
	weeding.Show();
	cout << endl;

	cout << "waxing time = ";
	waxing.Show();
	cout << endl;

	cout << "total work time = ";
	total = weeding + waxing;   // use operator+()
	total.Show();
	cout << endl;

	diff = weeding - waxing;    // use operator-()
	cout << "weeding time - waxing time = ";
	diff.Show();
	cout << endl;

	adjusted = total * 1.5;      // use operator*()
	cout << "adjusted work time = ";
	adjusted.Show();
	cout << endl;
	return 0;
}
weeding time = 4 hours, 35 minutes
waxing time = 2 hours, 47 minutes
total work time = 7 hours, 22 minutes
weeding time - waxing time = 1 hours, 48 minutes
adjusted work time = 11 hours, 3 minutes

友元

C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以致于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元。友元有3种:

  • 友元函数
  • 友元类
  • 友元成员函数

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

介绍如何成为友元前,先介绍为何需要友元。在为类重载二元运算符时(带两个参数的运算符)常常需要友元。将Time对象乘以实数就属于这种情况,下面来看看。

在前面的Time类示例中,重载的乘法运算符与其他两种重载运算符的差别在于,它使用了两种不同的类型。也就是说,加法和减法运算符都结合两个Time值,而乘法运算符将一个Time值与一个double值结合在一起。这限制了该运算符的使用方式。记住,左侧的操作数是调用对象。也就是说,下面的语句:

A = B * 2.75;
将被转换为下面的成员函数调用:
A = B.operator*(2.75);

但下面的语句又如何呢?

A = 2.75 * B; // 不能对应一个成员方法

能否定义一个成员函数如下,来匹配上面的表达式呢?

Time operator*(double n, Time& t)

可以看到报错:“此运算符函数的参数太多”。也就是说对于operatorop对于成员函数只能有一个参数。上图中下面的一行解注释后没有任何问题,也就是说加上关键字成为友元函数就可以了。

对于下面的函数原型:

Time operator*(double n) const;
friend Time operator*(double n, Time& t);

它们的实现如下:

可以看到:友元函数不属于类作用域,所以没有Time::前缀,不然报错;也不能加friend关键字,不然报错:“类声明的外部说明符无效”。友元函数可以有两个参数,最多也只能有两个,不然也会报错:“此运算符函数的参数太多”。无论是成员函数还是非成员函数(友元函数),都是通过Time对象调用的。

注:赋值运算符是只能由成员函数重载的运算符之一,第十章有提到。

从概念上说,2.75 * B应与B *2.75相同,但第一个表达式不对应于成员函数,因为2.75不是Time类型的对象。记住,左侧的操作数应是调用对象,但2.75不是对象。因此,编译器不能使用成员函数调用来替换该表达式。

解决这个难题的一种方式是,告知每个人(包括程序员自己),只能按B * 2.75这种格式编写,不能写成2.75 * B。这是一种对服务器友好-客户警惕的(server-friendly, client-beware)解决方案,与OOP无关。

然而,还有另一种解决方式——非成员函数(记住,大多数运算符都可以通过成员或非成员函数来重载)。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。这样,编译器能够将下面的表达式:

A = 2.75 * B;
与下面的非成员函数调用匹配:
A = operator*(2.75, B);
该函数原型如下:
Time operator*(double m, const Time & t);

对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,也就是说,double值乘以Time值。

使用非成员函数可以按所需的顺序获得操作数(先是double,然后是Time),但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。

❶创建友元

创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend

friend Time operator*(double m, const Time & t);

该原型意味着下面两点:

  • 虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
  • 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同

第二步是编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend,定义应该如下:

Time operator*(double m, const Time & t) {
    Time result;
    long totalminutes = t.hours * mult * 60 + t.minutes * mult;
    result.hours = totalminutes / 60;
    result.minutes = totalminutes % 60;
    return result;
}

有了上述声明和定义后:

A = 2.75 * B;
将转换为如下语句,从而调用刚才定义的非成员友元函数:
A = operator*(2.75, B);

总之,类的友元函数是非成员函数,其访问权限与成员函数相同。

乍一看,您可能会认为友元违反了OOP数据隐藏的原则,因为友元机制允许非成员函数访问私有数据。然而,这个观点太片面了。相反,应将友元函数看作类的扩展接口的组成部分。例如,从概念上看,double乘以Time和Time乘以double是完全相同的。也就是说,前一个要求有友元函数,后一个使用成员函数,这是C++句法的结果,而不是概念上的差别。通过使用友元函数和类方法,可以用同一个用户接口表达这两种操作。另外请记住,只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据。总之,类方法和友元只是表达类接口的两种不同机制。

实际上,按下面的方式对定义进行修改(交换乘法操作数的顺序),可以将这个友元函数编写为非友元函数:

Time operator*(double m, const Time & t) {
    return t * m; // 使用t.operator*(m)
}

原来的版本显式地访问t.minutes和t.hours,所以它必须是友元。这个版本将Time对象t作为一个整体使用,让成员函数来处理私有值,因此不必是友元。然而,将该版本作为友元也是一个好主意。最重要的是,它将该作为正式类接口的组成部分。其次,如果以后发现需要函数直接访问私有数据,则只要修改函数定义即可,而不必修改类原型。

如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。

常用的友元:重载<<运算符

一个很有用的类特性是,可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。与前面介绍的示例相比,这种重载要复杂些,因此我们分两步(而不是一步)来完成。

假设trip是一个Time对象。为显示Time的值,前面使用的是show()。然而,如果可以像下面这样操作将更好:

cout << trip; // 使cout识别Time类???

之所以可以这样做,是因为<<是可被重载的C++运算符之一。实际上,它已经被重载很多次了。最初,<<运算符是C和C++的位运算符,将值中的位左移。ostream类对该运算符进行了重载,将其转换为一个输出工具。前面讲过,cout是一个ostream对象,它是智能的,能够识别所有的C++基本类型。这是因为对于每种基本类型,ostream类声明中都包含了相应的重载的operator<<( )定义。也就是说,一个定义使用int参数,一个定义使用double参数,等等。因此,要使cout能够识别Time对象,一种方法是将一个新的函数运算符定义添加到ostream类声明中。但修改iostream文件是个危险的主意,这样做会在标准接口上浪费时间。相反,通过Time类声明来让Time类知道如何使用cout。

①<<的第一种重载版本

要使Time类知道使用cout,必须使用友元函数。这是什么原因呢?因为下面这样的语句使用两个对象,其中第一个是ostream类对象(cout):

cout << trip;

如果使用一个Time成员函数来重载<<,Time对象将是第一个操作数,就像使用成员函数重载*运算符那样。这意味着必须这样使用<<:

trip << cout; // 如果operator<<()是一个Time类成员函数

这样会令人迷惑。但通过使用友元函数,可以像下面这样重载运算符:

void operator<<(ostream & os, const Time & t) {
    os << t.hours << " hours, " << t.minutes << " minutes";
}

然后可以使用下面语句:

cout << trip;
按下面这样的格式打印数据:
4 hours, 23 minutes

新的Time类声明使operatro<<()函数成为Time类的一个友元函数。但该函数不是ostream类的友元(尽管对ostream类并无害处)。operator<<()函数接受一个ostream参数和一个Time参数,因此表面看来它必须同时是这两个类的友元。然而,看看函数代码就会发现,尽管该函数访问了Time对象的各个成员,但从始至终都将ostream对象作为一个整体使用。因为operator<<()直接访问Time对象的私有成员,所以它必须是Time类的友元。但由于它并不直接访问ostream对象的私有成员,所以并不一定必须是ostream类的友元。这很好,因为这就意味着不必修订ostream的定义。

注意,新的operator<<()定义使用ostream引用os作为它的第一个参数。通常情况下,os引用cout对象,如表达式cout << trip所示。但也可以将这个运算符用于其他ostream对象,在这种情况下,os将引用相应的对象。

另一个ostream对象是cerr,它将输出发送到标准输出流——默认为显示器,但在UNIX、Linux和Windows命令行环境中,可将标准错误流重定向到文件。另外,第6章介绍的ofstream对象可用于将输出写入到文件中。通过继承(参见第13章),ofstream对象可以使用ostream的方法。这样,便可以用operator<<()定义来将Time的数据写入到文件和屏幕上,为此只需传递一个经过适当初始化的ofstream对象(而不是cout对象)。

调用cout << trip应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式cout << trip将导致os成为cout的一个别名;而表达式cerr << trip将导致os成为cerr的一个别名。Time对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少。

②<<的第二种重载版本

前面介绍的实现存在一个问题。像下面这样的语句可以正常工作:

cout << trip;

但这种实现不允许像通常那样将重新定义的<<运算符与cout一起使用:

cout << "Trip time: " << trip << " (Tuesday)\n"; // 不能这样做

要理解这样做不可行的原因以及必须如何做才能使其可行,首先需要了解关于cout操作的一点知识。请看下面的语句:

int x = 5;
int y = 8;
cout << x << y;

C++从左至右读取输出语句,意味着它等同于:
(cout << x) << y;

正如iosream中定义的那样,<<运算符要求左边是一个ostream对象。显然,因为cout是ostream对象,所以表达式cout << x满足这种要求。然而,因为表达式cout << x位于<< y的左侧,所以输出语句也要求该表达式是一个ostream类型的对象。因此,ostream类将operator<<()函数实现为返回一个指向ostream对象的引用。具体地说,它返回一个指向调用对象(这里是cout)的引用。因此,表达式(cout << x)本身就是ostream对象cout,从而可以位于<<运算符的左侧。

可以对友元函数采用相同的方法。只要修改operator<<()函数,让它返回ostream对象的引用即可:

ostream & operator<<(ostream & os, const Time & t) {
    os << t.hours << " hours, " << t.minutes << " minutes";
    ★→→ 若不是友元函数,t.hours/t.minutes是不能访问到的,因为是private的!
    return os;
}

注意,返回类型是ostream &。这意味着该函数返回ostream对象的引用。因为函数开始执行时,程序传递了一个对象引用给它,这样做的最终结果是,函数的返回值就是传递给它的对象。也就是说,下面的语句:

cout << trip;
将被转换为下面的调用:
operator<<(cout, trip);
而该调用返回cout对象。因此,下面的语句可以正常工作:
cout << "Trip time:" << trip << " (Tuesday)\n";

这个operator<<()版本还可用于将输出写入到文件中:

#include<fstream>
...
ofstream fout;
fout.open("savetime.txt");
Time trip(12, 40);
fout << trip;
最后一条语句转换为:
operator<<(fout, trip);
类继承让ostream引用能够指向ostream对象和ofstream对象。

提示:一般来说,要重载<<运算符来显示c_name的对象,可使用一个友元函数,其定义如下:

ostream & operator<<(ostream & os, const c_name & obj) {
    os << ...; // 显示对象内容
    return os;
}

注:只有在类声明中的原型中才能使用friend关键字。除非函数定义也是原型(默认是内联函数),否则不能在函数定义中使用该关键字。

下面程序清单列出了修改后的类定义,其中包括operator*()和operator<<()这两个友元函数。它将第一个友元函数作为内联函数,因为其代码很短。

// mytime3.h -- Time class with friends
#ifndef MYTIME3_H_
#define MYTIME3_H_
#include <iostream>
class Time {
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void AddMin(int m);
	void AddHr(int h);
	void Reset(int h = 0, int m = 0);
	Time operator+(const Time& t) const;
	Time operator-(const Time& t) const;
	Time operator*(double n) const;
	friend Time operator*(double m, const Time& t) { // 默认类中定义的函数都是内联函数
		return t * m;
	} // 只要是在类声明中直接定义的函数,都自动成为内联函数,无需加inline关键字
	friend std::ostream& operator<<(std::ostream& os, const Time& t);
};
#endif
// mytime3.cpp  -- implementing Time methods
#include "mytime3.h"
Time::Time() {
	hours = minutes = 0;
}
Time::Time(int h, int m) {
	hours = h;
	minutes = m;
}
void Time::AddMin(int m) {
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}
void Time::AddHr(int h) {
	hours += h;
}
void Time::Reset(int h, int m) {
	hours = h;
	minutes = m;
}
Time Time::operator+(const Time& t) const {
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes / 60;
	sum.minutes %= 60;
	return sum;
}
Time Time::operator-(const Time& t) const {
	Time diff;
	int tot1, tot2;
	tot1 = t.minutes + 60 * t.hours;
	tot2 = minutes + 60 * hours;
	diff.minutes = (tot2 - tot1) % 60;
	diff.hours = (tot2 - tot1) / 60;
	return diff;
}
Time Time::operator*(double mult) const {
	Time result;
	long totalminutes = hours * mult * 60 + minutes * mult;
	result.hours = totalminutes / 60;
	result.minutes = totalminutes % 60;
	return result;
}
std::ostream& operator<<(std::ostream& os, const Time& t) {
     // t.hours、t.minusts中hours、minutes就是私有成员。但不能像成员函数一样直接通过this隐式调用。
    ★→→ 若不是友元函数,t.hours/t.minutes是不能访问到的,因为是private的!
	os << t.hours << " hours, " << t.minutes << " minutes";
	return os;
}
// usetime3.cpp -- using the fourth draft of the Time class
// compile usetime3.cpp and mytime3.cpp together
#include <iostream>
#include "mytime3.h"
int main() {
	using std::cout;
	using std::endl;
	Time aida(3, 35);
	Time tosca(2, 48);
	Time temp;

	cout << "Aida and Tosca:\n";
	cout << aida << "; " << tosca << endl;
	temp = aida + tosca;     // operator+()
	cout << "Aida + Tosca: " << temp << endl;
	temp = aida * 1.17;  // member operator*()
	cout << "Aida * 1.17: " << temp << endl;
	cout << "10.0 * Tosca: " << 10.0 * tosca << endl;
	return 0;
}
Aida and Tosca:
3 hours, 35 minutes; 2 hours, 48 minutes
Aida + Tosca: 6 hours, 23 minutes
Aida * 1.17: 4 hours, 11 minutes
10.0 * Tosca: 28 hours, 0 minutes

重载运算符:作为成员函数还是非成员函数

对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。例如,Time类的加法运算符在Time类声明中的原型如下:

Time operator+(const Time & t) const; // 成员函数
这个类也可以使用下面的原型:
friend Time operator+(const Time & t1, const Time & t2); // 非成员函数

加法运算符需要两个操作数。对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;对于友元版本来说,两个操作数都作为参数来传递。

非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。

这两个原型都与表达式T2 + T3匹配,其中T2和T3都是Time类型对象。也就是说,编译器将下面的语句:

T1 = T2 + T3;
转换为下面两个的任何一个:
T1 = T2.operator+(T3); // 成员函数
T1 = operator+(T2, T3); // 非成员函数

记住,在定义运算符时,必须选择其中的一种格式,而不能同时选择这两种格式。因为这两种格式都与同一个表达式匹配,同时定义这两种格式将被视为二义性错误,导致编译错误。

那么哪种格式最好呢?对于某些运算符来说(如前所述),成员函数是唯一合法的选择。在其他情况下,这两种格式没有太大的区别。有时,根据类设计,使用非成员函数版本可能更好(尤其是为类定义类型转换时)。本章后面的“转换和友元”一节将更深入地讨论这种情形。

再谈重载:一个失量类

下面介绍另一种使用了运算符重载和友元的类设计——一个表示矢量的类。这个类还说明了类设计的其他方面,例如,在同一个对象中包含两种描述同一样东西的不同方式等。即使并不关心矢量,也可以在其他情况下使用这里介绍的很多新技术。矢量(vector),是工程和物理中使用的一个术语,它是一个有大小和方向的量。例如,推东西时,推的效果将取决于推力的大小和推的方向。

显然,应为矢量重载运算符。首先,无法用一个数来表示矢量,因此应创建一个类来表示矢量。其次,矢量与普通数学运算(如加法、减法)有相似之处。这种相似表明,应重载运算符,使之能用于矢量。

       使用失量表示位移
       将两个失量相加

出于简化的目的,本节将实现一个二维矢量(如屏幕位移),而不是三维矢量(如表示直升机或体操运动员的运动情况)。描述二维矢量只需两个数,但可以选择到底使用哪两个数:

  • 可以用大小(长度)和方向(角度)描述矢量;
  • 可以用分量x和y表示矢量。
          失量的x和y分量

有时一种表示形式更方便,而有时另一种更方便,因此类描述中将包含这两种表示形式。另外,设计这个类时,将使得用户修改了矢量的一种表示后,对象将自动更新另一种表示。使对象有这种智能,是C++类的另一个优点。下面程序清单列出了这个类的声明。为复习名称空间,该清单将类声明放在VECTOR名称空间中。另外,该程序使用枚举创建了两个常量(RECT和POL),用于标识两种表示法。

// 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 vector
		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 { // report x value
			return x;
		}
		double yval() const { // report y value
			return y;
		}
		double magval() const { // report magnitude
			return mag;
		}
		double angval() const { // report angle
			return ang;
		}
		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 namespace VECTOR
#endif
// vect.cpp -- methods for the Vector class
#include <cmath>
#include "vect.h"   // includes <iostream>
using std::sqrt;
using std::sin;
using std::cos;
using std::atan;
using std::atan2;
using std::cout;
namespace VECTOR { // 这里也要用VECTOR名称空间,否则下面的Vector报错为“未定义的标识符”
	// compute 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 coordinate
	void Vector::set_x() {
		x = mag * cos(ang);
	}
	// set y from polar coordinate
	void Vector::set_y() {
		y = mag * sin(ang);
	}
	// public methods
	Vector::Vector() {          // default constructor
		x = y = mag = ang = 0.0;
		mode = RECT;
	}
	// construct vector from rectangular coordinates if form is r
	// (the default) or else from polar 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_x();
			set_y();
		} 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 coordinates if form is
	// RECT (the default) or else from polar coordinates 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 polar 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);
	}
	// subtract 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);
	}
	// multiply 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 coordinates 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) // 友元函数,不在类作用域,所以必须Vector::RECT
                // 但这个友元函数在名称空间VECTOR中,因此无需使用全限定名VECTOR::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
// randwalk.cpp -- using the Vector class
// compile with the vect.cpp file
#include <iostream>
#include <cstdlib>      // rand(), srand() prototypes
#include <ctime>        // time() prototype
#include "vect.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::Mode::POL);
			result = result + step;
			steps++;
		}
		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";
	return 0;
}
Enter target distance (q to quit): |50
Enter step length: |2
After 87 steps, the subject has the following location:
(x,y) = (-11.9372, -48.5572)
 or
(m,a) = (50.003, -103.812)
Average outward distance per step = 0.574747
Enter target distance (q to quit): |50
Enter step length: |2
After 185 steps, the subject has the following location:
(x,y) = (34.6234, -36.1695)
 or
(m,a) = (50.07, -46.2511)
Average outward distance per step = 0.270649
Enter target distance (q to quit): |50
Enter step length: |1
After 2457 steps, the subject has the following location:
(x,y) = (38.5995, 32.2178)
 or
(m,a) = (50.2783, 39.8507)
Average outward distance per step = 0.0204633
Enter target distance (q to quit): |50
Enter step length: |1
After 3754 steps, the subject has the following location:
(x,y) = (33.4929, -37.4119)
 or
(m,a) = (50.2138, -48.1636)
Average outward distance per step = 0.0133761
Enter target distance (q to quit): |q
Bye!

来看一元负号运算符,它只使用一个操作数。将这个运算符用于数字(如−x)时,将改变它的符号。因此,将这个运算符用于矢量时,将反转矢量的每个分量的符号。更准确地说,函数应返回一个与原来的矢量相反的矢量(对于极坐标,长度不变,但方向相反)。下面是重载负号的原型和定义:

Vector operator-() const;
Vector Vector::operator-() const {
    return Vector(-x, -y);
}

现在,operator-()有两种不同的定义。这是可行的,因为它们的特征标不同。可以定义−运算符的一元和二元版本,因为C++提供了该运算符的一元和二元版本。对于只有二元形式的运算符(如除法运算符),只能将其重载为二元运算符。

概率理论表明,平均而言,步数(N)、步长(s),净距离D之间的关系如下:

N = (D/s)^{2},这只是平均情况,但每次试验结果可能相差很大。例如,进行1000次试验(走50英尺,步长为2英尺)时,平均步数为636(与理论值625非常接近),但实际步数位于91~3951。同样,进行1000次试验(走50英尺,步长为1英尺)时,平均步数为2557(与理论值2500非常接近),但实际步数位于345~10882。因此,如果发现自己在随机漫步时,请保持自信,迈大步走。虽然在蜿蜒前进的过程中仍旧无法控制前进的方向,但至少会走得远一点。

程序中使用VECTOR名称空间非常方便,下面的using声明使Vector类的名称可用:

using VECTOR::Vector;

因为所有的Vector类方法的作用域都为整个类,所以导入类名后,无需提供其他using声明,就可以使用Vector的方法。

事实上,通过注释掉名称空间(去掉VECTOR名称空间),其他程序代码不用任何改动,是一样运行的。此例中并未表现出VECTOR名称空间的方便。

谈谈随机数。标准ANSI C库(C++也有)中有一个rand()函数,它返回一个从0到某个值(取决于实现)之间的随机整数。该程序使用求模操作数来获得一个0~359的角度值。rand()函数将一种算法用于一个初始种子值来获得随机数,该随机值将用作下一次函数调用的种子)依此类推。这些数实际上是伪随机数,因为10次连续的调用通常将生成10个同样的随机数(具体值取决于实现)。然而,srand()函数允许覆盖默认的种子值,重新启动另一个随机数序列。该程序使用time(0)的返回值来设置种子。time(0)函数返回当前时间,通常为从某一个日期开始的秒数(更广义地,time()接受time_t变量的地址,将时间放到该变量中,并返回它。将0用作地址参数,可以省略time_t变量声明)。因此,下面的语句在每次运行程序时,都将设置不同的种子,使随机输出看上去更为随机:

srand(time(0));

顺便说一句,在将一系列位置存储到文件中很容易。首先包含头文件<fstream>,声明一个ofstream对象,将其同一个文件关联起来:

#include<fstream>
...
ofstream fout;
fout.open("thewalk.txt");
然后,在计算结果的循环中加入类似于下面的代码:
fout << result << endl;

这将调用友元函数operator<<(fout, result),导致引用参数os指向fout,从而将输出写入到文件中。您还可以使用fout将其他信息写入到文件中,如当前由cout显示的总结信息。

类的自动转换和强制类型转换

先来复习一下C++是如何处理内置类型转换的。将一个标准类型变量的值赋给另一种标准类型的变量时,如果这两种类型兼容,则C++自动将这个值转换为接收变量的类型。例如,下面的语句都将导致数值类型转换:

long count = 8; // 类型int:8转换为long类型
double time = 11; // 类型int:11转换为double类型
int side = 3.33; // 类型double:3.33转换为int类型3

C++语言不自动转换不兼容的类型。例如,下面的语句是非法的,因为左边是指针类型,而右边是数字:

int *p = 10; // 不允许

虽然计算机内部可能使用整数来表示地址,但从概念上说,整数和指针完全不同。例如,不能计算指针的平方。然而,在无法自动转换时,可以使用强制类型转换:

int *p = (int *) 10;

上述语句将10强制转换为int指针类型(即int *类型),将指针设置为地址10。这种赋值是否有意义是另一回事。

可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。在这种情况下,程序员可以指示C++如何自动进行转换,或通过强制类型转换来完成。

下面是磅转换为英石的程序:

// stonewt.h -- definition for the Stonewt class
#ifndef STONEWT_H_
#define STONEWT_H_
class Stonewt {
private:
	enum {Lbs_per_stn = 14};      // pounds per stone
	// can replace by: static const int Lbs_per_stn = 14;
	int stone;                    // whole stones
	double pds_left;              // fractional pounds
	double pounds;                // entire weight in pounds
public:
	Stonewt(double lbs);          // constructor for double pounds
	Stonewt(int stn, double lbs); // constructor for stone, lbs
	Stonewt();                    // default constructor
	~Stonewt();
	void show_lbs() const;        // show weight in pounds format
	void show_stn() const;        // show weight in stone format
};
#endif

这个类并非真的需要声明构造函数,因为自动生成的默认构造函数就很好。另一方面,提供显式的声明可为以后做好准备,以防必须定义构造函数。另外,Stonewt类还提供了两个显示函数。一个以磅为单位来显示重量,另一个以英石和磅为单位来显示重量。

// stonewt.cpp -- Stonewt methods
#include <iostream>
using std::cout;
#include "stonewt.h"
// construct Stonewt object from double value
Stonewt::Stonewt(double lbs) {
	stone = int(lbs) / Lbs_per_stn; // integer division
	pds_left = int(lbs) % Lbs_per_stn + lbs - int(lbs);
	pounds = lbs;
}
// construct Stonewt object from stone, double values
Stonewt::Stonewt(int stn, double lbs) {
	stone = stn;
	pds_left = lbs;
	pounds = stn * Lbs_per_stn + lbs;
}
Stonewt::Stonewt() { // default constructor, wt = 0
	stone = pounds = pds_left = 0;
}
Stonewt::~Stonewt() { // destructor
}
void Stonewt::show_stn() const { // show weight in stones
	cout << stone << " stone, " << pds_left << " pounds\n";
}
void Stonewt::show_lbs() const { // show weight in pounds
	cout << pounds << " pounds\n";
}

在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。因此,下面的构造函数用于将double类型的值转换为Stonewt类型:

Stonewt(double lbs) // double转Stonewt的模板

也就是说,可以编写这样的代码:

Stonewt myCat; // 创建一个Stonewt对象
myCat = 19.6; // 使用Stonewt(double)将19.6转换为Stonewt对象

程序将使用构造函数Stonewt(double)来创建一个临时的Stonewt对象,并将19.6作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到myCat中。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换。

只有接受一个参数(其他参数有默认值的也算)的构造函数才能作为转换函数。下面的构造函数有两个参数,因此不能用来转换类型:

Stonewt(int stn, double lbs); // 不是一个可以转换的函数
Stonewt(int stn, double lbs = 0); // 可以转换int

然而,如果给第二个参数提供默认值,它便可用于转换int:

Stonewt(int stn, double lbs = 0); // 可以将int转换为Stonewt
如有函数:
Stonewt(double lbs);
void display(const Stonewt& st, int n);
可以这样调用:display(422, 2);

display()的原型表明,第一个参数应是Stonewt对象(Stonewt和Stonewt &形参都与Stonewt实参匹配)。遇到int参数时,编译器查找构造函数Stonewt(int),以便将该int转换为Stonewt类型。由于没有找到这样的构造函数,因此编译器寻找接受其他内置类型(int可以转换为这种类型)的构造函数。Stone(double)构造函数满足这种要求,因此编译器将int转换为double,然后使用Stonewt(double)将其转换为一个Stonewt对象。

将构造函数用作自动类型转换函数似乎是一项不错的特性。然而,当程序员拥有更丰富的C++经验时,将发现这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。因此,C++新增了关键字explicit,用于关闭这种自动特性。也就是说,可以这样声明构造函数:

explicit Stonewt(double lbs) // 不允许隐式转换

这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换:

Stonewt myCat;
myCat = 19.6; // 不允许,因为Stonewt(double)声明为explicit
myCat = Stonewt(19.6); // OK,一个显示地转换
myCat = (Stonewt)19.6; // OK,原始地显示类型转换

只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。

编译器在什么时候将使用Stonewt(double)函数呢?如果在声明中使用了关键字explicit,则Stonewt(double)将只用于显式强制类型转换,否则还可以用于下面的隐式转换。

  • 将Stonewt对象初始化为double值时。如:Stonewt st(1.23);
  • 将double值赋给Stonewt对象时。如:Stonewt st; st = 1.23;
  • 将double值传递给接受Stonewt参数的函数时。如:display(Stonewt& st);-> display(1.23);
  • 返回值被声明为Stonewt的函数试图返回double值时。如:Stonewt& go(){ return 1.23; }
  • 在上述任意一种情况下,使用可转换为double类型的内置类型时。如:Stonewt& go() { return 123; }

函数原型化提供的参数匹配过程,允许使用Stonewt(double)构造函数来转换其他数值类型。也就是说,下面两条语句都首先将int转换为double,然后使用Stonewt(double)构造函数。

Stonewt Jumbo(7000); // 使用Stonewt(double),将int转为double类型
Jumbo = 7300; // 使用Stonewt(double),将int转为double类型

然而,当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造函数Stonewt(long),则编译器将拒绝这些语句,可能指出:int可被转换为long或double,因此调用存在二义性。

现在来看使用实例:

// stone.cpp -- user-defined conversions
// compile with stonewt.cpp
#include <iostream>
using std::cout;
#include "stonewt.h"
void display(const Stonewt& st, int n);
int main() {
	Stonewt incognito = 275; // uses constructor to initialize
	Stonewt wolfe(285.7);    // same as Stonewt wolfe = 285.7;
	Stonewt taft(21, 8);

	cout << "The celebrity weighed ";
	incognito.show_stn();
	cout << "The detective weighed ";
	wolfe.show_stn();
	cout << "The President weighed ";
	taft.show_lbs();
	incognito = 276.8;      // uses constructor for conversion
	taft = 325;             // same as taft = Stonewt(325);
	cout << "After dinner, the celebrity weighed ";
	incognito.show_stn();
	cout << "After dinner, the President weighed ";
	taft.show_lbs();
	display(taft, 2);
	cout << "The wrestler weighed even more.\n";
	display(422, 2);
	cout << "No stone left unearned\n";
	return 0;
}
void display(const Stonewt& st, int n) {
	for (int i = 0; i < n; i++) {
		cout << "Wow! ";
		st.show_stn();
	}
}
The celebrity weighed 19 stone, 9 pounds
The detective weighed 20 stone, 5.7 pounds
The President weighed 302 pounds
After dinner, the celebrity weighed 19 stone, 10.8 pounds
After dinner, the President weighed 325 pounds
Wow! 23 stone, 3 pounds
Wow! 23 stone, 3 pounds
The wrestler weighed even more.
Wow! 30 stone, 2 pounds
Wow! 30 stone, 2 pounds
No stone left unearned

转换函数

上面程序清单将数字转换为Stonewt对象。可以做相反的转换吗?也就是说,是否可以将Stonewt对象转换为double值,就像如下所示的那样?

Stonewt wolfe(285.7);
double host = wolfe; // ??可以吗??

可以这样做,但不是使用构造函数。构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的C++运算符函数——转换函数。

转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。例如,如果定义了从Stonewt到double的转换函数,就可以使用下面的转换:

Stonewt wolfe(285.7);
double host = double (wolfe);    // syntax #1
double thinker = (double) wolfe; // syntax #2

也可以让编译器来决定如何做:

Stonewt wells(20, 3);
double star = wells; // 隐式使用转换函数

编译器发现,右侧是Stonewt类型,而左侧是double类型,因此它将查看程序员是否定义了与此匹配的转换函数。(如果没有找到这样的定义,编译器将生成错误消息,指出无法将Stonewt赋给double。)

那么,如何创建转换函数呢?要转换为typeName类型,需要使用这种形式的转换函数:

operator typeName();

请注意以下几点:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数

例如,转换为double类型的函数的原型如下:

operator double();

typeName(这里为double)指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。

要添加将stone_wt对象转换为int类型和double类型的函数,需要将下面的原型添加到类声明中:

operator int();
operator double();
// stonewt1.h -- revised definition for the Stonewt class
#ifndef STONEWT1_H_
#define STONEWT1_H_
class Stonewt {
private:
	enum {Lbs_per_stn = 14};      // pounds per stone
	int stone;                    // whole stones
	double pds_left;              // fractional pounds
	double pounds;                // entire weight in pounds
public:
	Stonewt(double lbs);          // construct from double pounds
	Stonewt(int stn, double lbs); // construct from stone, lbs
	Stonewt();                    // default constructor
	~Stonewt();
	void show_lbs() const;        // show weight in pounds format
	void show_stn() const;        // show weight in stone format
	// conversion functions
	operator int() const;
	operator double() const;
};
#endif
// stonewt1.cpp -- Stonewt class methods + conversion functions
#include <iostream>
using std::cout;
#include "stonewt1.h"
// construct Stonewt object from double value
Stonewt::Stonewt(double lbs) {
	stone = int(lbs) / Lbs_per_stn;    // integer division
	pds_left = int(lbs) % Lbs_per_stn + lbs - int(lbs);
	pounds = lbs;
}
// construct Stonewt object from stone, double values
Stonewt::Stonewt(int stn, double lbs) {
	stone = stn;
	pds_left = lbs;
	pounds = stn * Lbs_per_stn + lbs;
}

Stonewt::Stonewt() {         // default constructor, wt = 0
	stone = pounds = pds_left = 0;
}

Stonewt::~Stonewt() {       // destructor
}
void Stonewt::show_stn() const { // show weight in stones
	cout << stone << " stone, " << pds_left << " pounds\n";
}
void Stonewt::show_lbs() const { // show weight in pounds
	cout << pounds << " pounds\n";
}
// conversion functions
Stonewt::operator int() const {
	return int(pounds + 0.5);
}
Stonewt::operator double()const {
	return pounds;
}
// stone1.cpp -- user-defined conversion functions
// compile with stonewt1.cpp
#include <iostream>
#include "stonewt1.h"
int main() {
	using std::cout;
	Stonewt poppins(9, 2.8);     // 9 stone, 2.8 pounds
	double p_wt = poppins;      // implicit conversion
	cout << "Convert to double => ";
	cout << "Poppins: " << p_wt << " pounds.\n";
	cout << "Convert to int => ";
	cout << "Poppins: " << int(poppins) << " pounds.\n";
	return 0;
}
Convert to double => Poppins: 128.8 pounds.
Convert to int => Poppins: 129 pounds.

如果省略了显式强制类型转换:

cout << "Poppins: " << poppins << " pounds.\n";

程序会像下面语句一样隐式转换吗?

double p_wt = poppins;

答案是否定的。在p_wt示例中,上下文表明,poppins应被转换为double类型。但在cout示例中,poppins有两个可选的转换,并没有指出应转换为int类型还是double类型。在缺少信息时,编译器将指出,程序中使用了二义性转换。该语句没有指出要使用什么类型。如果只定义了double转换函数,则编译器接受该语句。因为只有一种转换的可能,不存在二义性。

对于如下语句:

long gone = poppins;

在C++中,int和double值都可以被赋给long变量,所以编译器使用任意一个转换函数都是合法的。编译器不想承担选择转换函数的责任。然而,如果删除了这两个转换函数之一,编译器将接受这条语句(否则存在二义性不接受此条语句)。例如,假设省略了double定义,则编译器将使用int转换,将poppins转换为一个int类型的值。然后在将它赋给gone时,将int类型值转换为long类型。

当类定义了两种或更多的转换时,仍可以用显式强制类型转换来指出要使用哪个转换函数。可以使用下面任何一种强制类型转换表示法:

long gone = (double) poppins;
long gone = int (poppins);

第一条语句将poppins转换为一个double值,然后赋值操作将该double值转换为long类型。同样,第二条语句将poppins首先转换为int类型,随后转换为long。

和转换构造函数一样,转换函数也有其优缺点。提供执行自动、隐式转换的函数所存在的问题是:在用户不希望进行转换时,转换函数也可能进行转换。例如,假设编写了下面的代码:

int ar[20];
...
Stonewt temp(14, 4);
...
int Temp = 1;
...
cout << ar[temp] << "!\n"; // 使用了temp而不是Temp

通常,程序员以为编译器能够捕获诸如使用了对象而不是整数作为数组索引等错误,但Stonewt类定义了一个operator int(),因此Stonewt对象temp将被转换为int 200,并用作数组索引。原则上说,最好使用显式转换,而避免隐式转换。在C++98中,关键字explicit不能用于转换函数,但C++11消除了这种限制。因此,在C++11中,可将转换运算符声明为显式的:

class Stonewt {
    ...
    // 转换函数
    explicit operator int() const;
    explicit operator double() const;
};

有了这些声明后,需要强制转换时将调用这些运算符。

另一种方法是,用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数才会执行。也就是说,可以将:

Stonewt::operator int() { return int (pounds + 0.5); }
替换为:
int Stonewt::Stone_to_Int() { return int (pounds + 0.5); }

这样,下面的语句:

int plb = poppins; // 非法
int plb = poppins.Stone_to_Int(); // 合法

应谨慎地使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的函数。

总之,C++为类提供了下面的类型转换。

  • 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将int值赋给Stonewt对象时,接受int参数的Stonewt类构造函数将自动被调用。然而,在构造函数声明中使用explicit可防止隐式转换,而只允许显式转换。
  • 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员,没有返回类型、没有参数、名为operator typeName(),其中,typeName是对象将被转换成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自动被调用。

转换函数和友元函数

可以使用下面的成员函数实现加法:

Stonewt Stonewt::operator+(const Stonewt & st) const {
    double pds = pounds + st.pounds;
    Stonewt sum(pds);
    return sum;
}

也可以将加法作为友元函数来实现,如下所示:

Stonewt Stonewt::operator+(const Stonewt & st1, const Stonewt & st2) {
    double pds = st1.pounds + st2.pounds;
    Stonewt sum(pds);
    return sum;
}

上面任何一种格式都允许这样做:

Stonewt jennySt(9, 12);
Stonewt bennySt(12, 8);
Stonewt total = jennySt + bennySt;

另外,如果提供了Stonewt(double)构造函数,则也可以这样做:

Stonewt jennySt(9, 12);
double kennyD = 176.0;
Stonewt total = jennySt + kennyD;

但只有友元函数才允许这样做:

Stonewt jennySt(9, 12);
double pennyD= 146.0;
Stonewt total = pennyD + jennySt;

为了解其中的原因,将每一种加法都转换为相应的函数调用。首先:

total = jennySt + bennySt;
被转换为:
total = jennySt.operator+(bennySt); // 成员函数
或:
total = operator+(jennySt, bennySt); // 友元函数

上述两种转换中,实参的类型都和形参匹配。另外,成员函数是通过Stonewt对象调用的。

其次:

total = jennySt + kennyD;
被转换为:
total = jennySt.operator+(kennyD); // 成员函数
或:
total = operator+(jennySt, kennyD); // 友元函数

同样,成员函数也是通过Stonewt对象调用的。这一次,每个调用中都有一个参数(kennyD)是double类型的,因此将调用Stonewt(double)构造函数,将该参数转换为Stonewt对象。

另外,在这种情况下,如果定义了operator double()成员函数,将造成混乱,因为该函数将提供另一种解释方式。编译器不是将kennyD转换为double并执行Stonewt加法,而是将jennySt转换为double并执行double加法。过多的转换函数将导致二义性。

最后:

total = pennyD + jennySt;
被转换为:
total = operator+(pennyD, jennySt); // 友元函数

其中,两个参数都是double类型,因此将调用构造函数Stonewt(double),将它们转换为Stonewt对象。

如果通过成员函数,转换是这样:

total pennyD.operator+(jennySt); // 无意义的

这没有意义,因为只有类对象才可以调用成员函数。C++不会试图将pennyD转换为Stonewt对象。将对成员函数参数进行转换,而不是调用成员函数的对象。

这里的经验是,将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配。

实现加法时的选择

要将double量和Stonewt量相加,有两种选择。第一种方法是将下面的函数定义为友元函数,让Stonewt(double)构造函数将double类型的参数转换为Stonewt类型的参数:

operator+(const Stonewt &, const Stonewt &)

第二种方法是,将加法运算符重载为一个显式使用double类型参数的函数:

Stonewt operator+(double x); // 成员函数
friend Stonewt operator+(double x, Stronewt & s);

这样,下面的语句将与成员函数operator+(double x)完全匹配:

total = jennySt + kennyD; // Stonewt + double

而下面的语句将与友元函数operator+(double x, Stonewt &s)完全匹配:

total = pennyD + jennySt; // double + Stonewt

每一种方法都有其优点。第一种方法(依赖于隐式转换)使程序更简短,因为定义的函数较少。这也意味程序员需要完成的工作较少,出错的机会较小。这种方法的缺点是,每次需要转换时,都将调用转换构造函数,这增加时间和内存开销。第二种方法(增加一个显式地匹配类型的函数)则正好相反。它使程序较长,程序员需要完成的工作更多,但运行速度较快。

如果程序经常需要将double值与Stonewt对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了更保险,可以使用显式转换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值