对象也需要运算
在学完第12章以后,你知道如何编写类(对象类型)才能让它们像标准的数据类型那样工作了,对吗?
对是对,但还差那么一点点。int、float、double和char等标准数据类型的重要特点之一是,你可以对它们使用各种操作符。如果没有这些操作符,想在C++里完成任何一种计算都会非常困难。
在C++里,在你声明某个类的时候,可以对各种标准的操作符(如+、-、*、/等)的行为方式作出定义。前提是它们的操作数是该类的对象。你还可以定义“等于”比较操作符的行为,而这可以帮你判断两个对象是否相等,就像下面这段代码演示的那样:
```c++
Fraction a(1,2), b(1,3);
if (a+b == Fraction(5, 6))
cout << "1/2 + 1/3 equals 5/6";
13.1 类操作符函数简介
编写类操作符函数的基本语法相当简单。
```c++
return_type operator@(argument_list){}
在这个语法里,把符号“@”替换为合法的C++操作符如+、-、*或/就是相应的类 操作符函数的声明。C++标准类型支持的任何一个操作符符号都可以用来声明类操作符函数。这些符号在作为类操作符使用时仍必须遵守它们被用作普通操作符时的优先级规则。(参见附录A)
- [ ] 你既可以把操作符函数定义为成员函数,也可以把它定义为全局函数(或者说,非成员函数)。
- [ ] 如果把操作符函数声明为成员函数,调用该函数的对象将成为左操作数。口如果把操作符函数声明为全局函数,每个操作数将对应于该函数的一个输入参数。
下面是在Point类的内部声明+和-操作符函数的代码:
```c++
class Point
{
//...
public:
Point operator+(Point pt);
Point operator-(Point pt);
//...
};
有了这些声明,你就可以把这两个操作符用在Point对象身上了:
```c++
Point pointl, point2, point3;
point1 = point2 + point3;
编译器将把这条语句解释为使用左操作数(本例中为point2)调用operator+函数。右操作数(本例中的point3)将成为该函数的输入参数。你可以把这种关系想象成如图13-1所示的样子。

在point2身上会发生什么事?它的值会被忽略吗?不会。这个operator+函数将把point2视为“这个对象”,它会把不带范围限定前缀的x和y解释为point2里的x和y的副本。你可以在下面的函数定义里看到这种行为:
```c++
Point Point::operator+(Point pt)
{
Point new_pt;
new_pt.x = x + pt.x;
new_pt.y = y + pt.y;
return new_pt;
}
不带范围限定前缀的数据成员x和y代表左操作数(本例中的point2)里的值。表达式pt.x和pt.y代表右操作数(本例中的point3)里的值。
这个操作符函数的返回类型被声明为Point类,这意味着它将返回Point对象。这符合常识:如果你把两个点加在一起,应该得到另外一个点。但C++允许你把操作符函数的返回值的类型声明为任意一种合法的类型。
如果Point类有一个Point(int,int)构造器,你还可以把这个函数编写得更紧凑,如下所示:
```c++
Point Point::operator+(Point pt)
{
return Point(x+pt.x, y+pt.y);
}
输入参数表可以包含任意类型。这里允许重载:在声明操作符函数去处理int类型的同时,还可以再声明一个与之同名的操作符函数去处理float类型,等等。
具体到Point类,把Point对象和整数相乘也有实际意义。相应的操作符函数的声明(在Point类的内部)应该是下面这样:
```c++
Point operator*(int n);
这个操作符函数的函数定义按道理应该是下面这样。
```c++
Point Point::operator(int n)
{
Point new pt:
new_pt.x = x n;
new_pt.y = y * n;
return new_t;
}
这个操作符函数将返回一个Point对象,但你完全可以让它返回你选择的任何对象。
作为参考示例,你可以创建操作符函数来计算两点之间的距离并返回浮点(double)结果。我为这个例子选择的操作符是,但你可以随意选择在C++里定义的任何一个二元操作符。这里最重要的是根据你打算进行的操作而选择一种最适当的返回类型。
```c++
include <cmath>
double Point::operator%(Point pt)
{
int dl = pt.x - x;
int a2 = pt.y - y;
return sgrt(d1dl + d2d2);
}
有了这个函数定义,下面的代码将正确地显示出(20,20)和(24,23)这两点之间的距离5.0:
```c++
Point pt1(20, 20);
Point pt2(24, 23);
cout << "Diatance between points is:"<< pt1 % pt2;
13.2 声明操作符函数为全局函数
在前一小节里,我曾说过你可以把操作符函数声明为全局函数。这么做有一个坏处:你将无法把所有的相关函数都集中放置在类声明里。但在某些场合(我马上就要讨论 13到),你或许只能选用这个办法。
全局性的操作符函数必须声明在类的外部。其输入参数表里的类型决定该函数将作用于什么类型的操作数。比如说,Point类的加法操作符函数可以被改写为全局函数。下面是这个函数的声明(原型),它必须出现在这个函数的首次调用之前:
```c++
Point operator+(Point pt1, Point pt2);
下面是这个函数的函数定义:
```c++
Point operator+(Point pt1, Point pt2)
{
Point new_pt;
new_pt.x = pt1.x + pt2.x;
new_pt.y = ptl.y + Pt2.y;
return new_pt;
}
你可以把这个函数的调用情况想象成如图13-2所示的样子。

现在,两个操作数都被解释为函数的输入参数。左操作数(本例中是point2)把它的值传递给第一个输入参数pt1。此时已没有“这个对象”的概念了,对Point数据成员的所有引用都必须加以限定。
这里可能会遇到一个问题:如果某个Point数据成员不是公用的,这个函数将无法访问它。解决这个问题的办法之一,是通过函数调用去访问有关数据:
```c++
Point operator+(Point pt1, Point pt2)
{
Point new_pt;
int a = pt1.get_x() + pt2.get_x();
int b = pt1.get_y() + pt2.get_y();
new_pt.set(a, b);
return new_pt;
}
但这并不是一个好办法,对某些类来说,它甚至有可能根本不能奏效。(比如说,你有一个类,但其私有成员根本无法访问它,但你仍需要为它编写一个全局性的操作符函数。)更好的解决方案是把函数声明为友元函数,这意味着该函数是全局的,但它可以访问到私有成员。
```c++
class Point
{
//...
public:
friend Point operator+(Point pt1, Point pt2);
};
有时候,操作符函数必须被编写成全局函数。在成员函数里,左操作数会在函数定义里被解释为“这个对象”。可如果左操作数根本就不是某种对象类型该怎么办?换句话说,如果你想支持一种像下面这样的操作该怎么办?
```c++
point1= 3 * point2;
这里的问题是,左操作数是int类型,不是Point类型。支持这样一种操作的唯一办法是编写全局函数:
```c++
Point operator(int n, Point pt)
{
Point new_pt;
new_pt.x = pt.x n;
new_pt.y = pt.y * n;
return new_pt;
}
为了访问Point类里的私有数据成员,这个函数可能需要声明为Point类的友元函数;
```c++
class Point
{
//...
public:
friend Point operator*(int n, Point pt);
};
你可以把这个函数的调用情况想象成如图13-3所示的样子。

13.3 用引用来提高效率
正如第12章指出的那样,每当对象被传递或是被返回为某个值的时候,副本构造器就会被调用一次,系统也必须为那个副本对象分配内存。如果编写的类只在有绝对必要的时候才会去创建对象,那么程序会更高效。有个简单的办法可以做到这一点:使用引用类型。
在下面的代码片段里,我们可以看到Point类有一个add函数和一个加法操作符(+)函数,后者调用了前者。这些代码在编写时没有使用引用类型:
```c++
class Point
{
// ...
public:
Point add(Point pt);
Point operator+(Point pt);
};
Point Point::add(Point pt)
{
Point new_pt;
new_pt.x=x + pt.X;
new_pt.y=y + pt.y;
return new_pt;
}
Point Point::operator+(Point pt)
{
return add(pt);
}
这是编写这些函数时最容易想到的办法,但我们来看看表达式pt1+pt2将会创建多少个新对象。
- [ ] 当右操作数被传递到operator+函数时,需要制作pt2的副本并把它传递给这个函数。
- [ ] 当operator+函数调用add函数时,需要再制作一个pt2的副本并把它传递给add函数。
- [ ] add函数创建了一个新对象new_pt,调用默认构造器。当ada函数返回时,程序将制作一个new_pt的副本并把它传递回operator+函数。
- [ ] 当operator+函数把对象传递回它的调用者时,需要再制作一个new_pt的副本。
这么多次复制!总共创建了5个新对象,其间调用默认构造器1次,调用副本构造器4次。这是效率极其低下的代码。
注意 在现如今的超高速CPU时代,效率似乎已经不再是人们关注的焦点。对于如此简单的Point类来说,即使你做了一些效率低下的事情,恐怕也要重复成千上万次操作才能让人觉察到有明显的延时。但这决不应该成为偷懒的借口,因为谁都无法断言某个类的使用情况将会是怎样的。如果有简单的办法可以让代码变得更高效,你当然应该用上这个办法。
我们可以改用引用型输入参数来减少2次复制操作。下面是改进后的版本,修改过的代码行以粗体字给出:
```c++
class Point
{
// ...
public:
Point add(conet Point &pt);
Point operator+(conat Point &pt);
};
Point Point::add(const Point &pt)
{
Point new_pt;
new pt.x = x+ pt.x;
new_pt.y = y+ pt.y;
return new_t;
}
Point Point::operator+(conat Point &pt)
{
return add(pt);
}
改用引用类型(如Points)的好处之一是简便易行:只需修改相关函数调用的实现,其余的源代码都不需要修改。记住,当你传递某个引用的时候,函数获得的是对原始数据的引用,但不需要使用指针语法。
我在这里还使用了const关键字。这个关键字的作用是防止传递的输入参数被意外修改。在函数获得了输入参数的副本之后,无论它对那个副本做什么、怎么做,都不会改变原始数据的值。const关键字为原始数据提供了保护,即使你再不小心也不会改变受其保护的操作数的值。
我们对源代码作出的修改减少了2次对象复制操作。但每当这些函数之一返回时,它还是要制作对象的副本。你可以把这两个函数之一或者全部改写为内嵌函数,以进一步减少制作副本的次数。operator+函数除调用add函数以外不做任何其他的事情,所以应该改写为内嵌函数:
```c++
class Point
{
//...
public:
Point operator+(conat Point &pt) { return add(pt); }
};
在把operator+函数改写为如上所示的内嵌函数之后,像P1+p2这样的操作将被直接转换为对add函数的调用,而这将再减少一次复制操作。现在,复制操作已经大大减少了。
### 示例 Point 类的操作符
你手里现在已经有了为Point类编写一些高效、有用的操作符函数所需要的全部工具了。下面的代码包括一份完整的Point类的声明和一些通过在对象上进行操作而测试它的代码。
与第12章相同代码以正常字体印刷,新增加或被修改过的代码行以粗体字印刷。
```c++
/* point3.cpp */
#include <iostream>
using namespace std;
class Point
{
private: // Data member(private)
int x;
int y;
public: // Constructor
Point(){x = 0; y = 0;}
Point(int new_x, int new_y){ x = new_x; y = new_y; }
Point(const Point &src){ set(src.x, src.y); }
//Operation
Point add(const Point &pt);
Point sub(const Point &pt);
Point operator+(const Point &pt){ return add(pt); }
Point operator-(const Point &pt){ return sub(pt); }
// Other member functions
void set(int new_x, int new_y){x = new_x; y = new_y; }
int get_x(){ return x; }
int get_y(){ return y; }
};
int main()
{
Point p1(20, 20);
Point p2(0, 5);
Point p3(-10, 25);
Point p4 = p1 + p2 + p3;
cout << "The value of pt1 is ";
cout << pt1.getx() << ", " << pt1.get_y() << endl;
system("PAUSE");
return 0;
}
Point Point::add(Point pt)
{
Point new_pt;
new_pt.x=x + pt.X;
new_pt.y=y + pt.y;
return new_pt;
}
Point Point::sub(Point pt)
{
Point new_pt;
new_pt.x=x - pt.X;
new_pt.y=y - pt.y;
return new_pt;
}
代码分析
这个示例程序给Point类增加了一系列操作符函数:
```c++
//Operation
Point add(const Point &pt);
Point sub(const Point &pt);
Point operator+(const Point &pt){ return add(pt); }
Point operator-(const Point &pt){ return sub(pt); }
add和sub函数负责具体完成点加法和点减法操作,它们使你可以编写出下面这样的语句:
```c++
Point p1 = p2.add(p3);
这条语句把point2和point3加在一起,以生成一个新的Point对象。operator+函数是一个内嵌函数,它会把下面这样的语句转换为对add函数的调用:
```c++
Point p1 = p2 + p3; //Point p1 = p2.operator+(p3);
因为这个函数是内嵌的并且使用了引用型输入参数(const Point &),所以它的调用开销应该是最小的。表达式point2 + point3先被转换为operator+函数调用,再被转换为对add函数的调用。
add函数先创建一个新Point对象,再把“这个对象”(本例中的point2)的坐标和输入参数(point3)的坐标相加以对新对象进行初始化。
```c++
point2.add(point3);
operator-和sub函数的工作流程与此类似。
这个示例程序在声明get_x和get_y函数的时候还使用了const关键字。在此处的上下文里,const关键字的含义是“本函数同意不去修改任何数据成员,也不会去调用任何不是const函数的其他函数”。
```c++
int get_x() const {return x;}
int get_y() const {return y;}
这是一个很有用的修改。它可以防止数据成员被意外修改,它允许这两个函数被其他同意不修改Point对象的const函数调用。
> 练习题
练习题13.1.1 编写一个测试统计默认构造器和副本构造器被调用了多少次。(提示:在这两个构造器的函数定义里插入一些语句把输出发送到cout;如有必要,你可以插入多个代码行,但前提是必须保证那些函数定义的语法是正确的。)接下来运行程序,一次使用引用型输入参数(const Pointa),另一次使用原始的输入参数(Point),看看前一种做法能带来多大的效率提升。
练习题13.1.2 对Point类进行扩展,让它支持Point对象和整数的乘法运算,然后测试它。这需要用到全局函数,还需要用到friend声明,详见前一小节里的描述。
练习题13.1.3 为三维空间里的点编写一个相似的类(Point3D)。
示例 Fraction 类的操作符
这个编程示例使用了和编程示例13-1里相似的技巧,以扩展Fraction类对基本操作符的支持。和前面一样,这里的代码使用了引用型输入参数(const Fraction &)来提高效率。
```c++
/ Fract5.cpp /
include <iostream>
using namespace std;
class Fraction
{
private:
int num, den; // Numerator and denominator.
public:
Fraction() { set(0, 1); }
Fraction(int n, int d) { set(n, d); }
Fraction(Fraction const &src);
void set(int n, int d){num = n; den = d; normalize();}
int get_num() const {return num; }
int get_den() const {return den; }
Fraction add(const Fraction other);
Fraction mul(const Fraction other);
Fraction operator+(const Fraction &other){ return add(other); }
Fraction operator*(const Fraction &other){ return mul(other); }
private:
void normalize(); // Convert to standard form.
int gcf(int a, int b); // Greatest Common Factor.
int lcm(int a, int b); // Lowest Common Denomin.
}
int main()
{
Fraction f1(1, 2);
Fraction f2(1, 3);
Fraction f3 = f1 + f2;
cout << "The value of f3 is ";
cout << f3.get_num() << "/" << f3.get_den() << endl;
system("PAUSE");
return 0;
}
// Fraction Class Functions
Fraction::Fraction(Fraction const &src)
{
cout << "Now calling copy constructor. " << endl;
num = src.num;
den = src.den;
}
Fraction Fraction::add(const Fraction other)
{
Fraction fract;
int lcd = lcm(den, other.den);
int quot1 = lcd / den;
int quot2 = lcd/other.den;
fract.set(num quot1 + other.num quot2, lcd);
return fract;
}
Fraction Fraction::mul(const Fraction other)
{
Fraction fract;
fract.set(num other.num, den other.den);
return fract;
}
// Normalize: put fraction into standard form, unique
// for each mathematically different value.
void Fraction::normalize()
{
// Handle cases involving 0
if (den == 0 || num == 0)
{
num = 0;
den = 11;
}
// Put neg. sign in numerator only.
if (den < 0)
{
num *= -1;
den *= -1;
}
// Pactor out GCP from numerator and denominator.
int n= gcf(num, den);
num = num / n;
den = den / n;
}
// Greatest Common Factor
int Fraction::gcf(int a, int b)
{
if (b== 0)
return abs(a);
else
return gcf(b, a%b);
}
// Lowest Common Multiple
int Fraction::lcm(int a, int b)
{
int n = gcf(a, b);
return a / n * b;
}
### 代码分析
add和mul函数取自Fraction类里的原有代码,我只是改变了输入参数的类型,以便让这两个函数都改用引用型输入参数,这样可以得到一个更有效率的实现。
> ```c++
> Fraction add(const Fraction other);
> Fraction mul(const Fraction other);
> ```
当这些函数的声明发生变化时,它们的函数定义也必须修改才能适应新的输入参数类型。但这个改动只影响函数的头部(见粗体字部分)。函数定义的其余部分仍维持原样。
> ```c++
> Fraction Fraction::add(const Fraction other)
> {
> Fraction fract;
> int lcd = lcm(den, other.den);
> int quot1 = lcd / den;
> int quot2 = lcd/other.den;
> fract.set(num * quot1 + other.num * quot2, lcd);
> return fract;
> }
>
> Fraction Fraction::mul(const Fraction other)
> {
> Fraction fract;
> fract.set(num * other.num, den * other.den);
> return fract;
> }
> ```
新增加的两个操作符函数除了调用相应的成员函的数(add和mult)并返回值以外,其他什么都没做。例如,当编译器看到下面这个表达式时,
> ```c++
> f1 + f2;
> ```
它会把这个表达式转换为如下所示的函数调用:
> ```c++
> f1.operator+(f2);
> ```
接下来,operator+函数里的代码再把上述调用进一步转换如下:
> ```c++
> t1.add(f2);
> ```
main函数里的语句对那两个操作符函数的代码进行了测试,其基本流程是先声明几个分数,再把它们相加,最后显示结果。
### 代码优化
从第12章开始,Fraction类有了一个很有用的Fraction(int,int)构造器。你可以利用这个构造器把ada和mult函数改写得更加紧凑。虽然此前给出的add和mult函数的版本运行得很好,但下面的代码更加优化。请看,当执行到return语句的时候,Fraction(int, int)构造器会自动创建可以被返回的Fraction对象并调用set函数,set函数在设置好数据成员之后又会自动调用normalize函数。
> ```c++
> Fraction Fraction::add(const Fraction other)
> {
> int lcd = lcm(den, other.den);
> int quot1 = lcd / den;
> int quot2 = lcd/other.den;
> return Fraction(num * quot1 + other.num * quot2, lcd);
> }
>
> Fraction Fraction::mul(const Fraction other)
> {
> return Fraction(num * other.num, den * other.den);
> }
> ```
> > **练习题**
>
> 练习题13.2.1 改写编程示例13-1中的main函数,让它反复提示用户输入一系列分数值,直到用户输入0作为分母的时候才退出输入循环。让程序跟踪计算
>
> 输入的所有分数的总和并显示结果。
>
> 练习题13.2.2 为Fraction类编写operator-函数(减法)。
>
> 练习题13.2.3 为Fraction类编写operator/函数(除法)。
13.4 操作符函数的重载
操作符函数也可以重载,你可以为每个操作符编写多个不同的函数,让每个函数去处理不同的类型。比如说,你可以为Fraction类编写几个不同版本的operator+函数:
> ```c++
> class Fraction
> {
> //...
> public:
> operator+(const Fraction &other);
> friend operator+(int n, const Fraction &fr);
> friend operator+(const Fraction &fr, int n);
> };
> ```
这些函数当中的每一个都可以处理一种int和Fraction操作数的组合,使你能够支持以下这些表达式:
> ```c++
> Fraction fract1;
> fract1 = 1 + Fraction(1,2) + Fraction(3,4) + 4;
> ```
但还有一种更简单的办法来支持涉及整数的操作。你真正需要的是能够把整数转换为Fraction对象的函数。有了这个函数,你只要编写一个operator+函数就足够了。 13在如下所示的表达式里,编译器会先把整数1转换为Fraction格式,然后再调用
Fraction::operator+函数把两个分数相加。
> ```c++
> Fraction fract1 = 1 + Fraction(1, 2);
> ```
事实证明,这样的转换函数很容易编写,它其实就是只有一个int输入参数的Fraction构造器!这是一个很简单的构造器,应该把它写成内嵌函数以提高效率:
> ```c++
> Fraction(int n) { set (n, 1); }
> ```
## 13.5 类赋值操作符
在你编写类的时候,友好的C++编译器会自动提供如下所示的三个特殊的成员函数。我到目前为止已经介绍过它们当中的两个。
> - [ ] 默认构造器。编译器提供的版本什么都不做。不仅如此,在你编写了自己的构造器之后,编译器就会收回这个构造器。为保险起见,你应该编写自己的默认构造器。
> - [ ] 副本构造器。编译器提供的版本是以简单的“成员到成员”的方式制作一个源对象的副本。
> - [ ] 赋值操作符函数(=)。这是我们以前没介绍过的。
只要你没有编写赋值操作符函数,编译器就会自动替你提供一个。这也正是你可以进行下面这样的操作的原因:
> ```c++
> f1 = f2 + f3;
> ```
编译器提供的operator=函数和编译器提供的副本构造器很相似,它也是以简单的“成员到成员”的方式进行复制。但副本构造器会创建出一个新对象,所以它们并非完全一样。
编写自己的赋值操作符函数,使用如下所示的语法:
> ```c++
> class_name &operator=(const class_name &source_arg);
> ```
这个声明语法有一个有意思的细节,它类似副本构造器,但operator=函数将返回一个对class_name类的对象的引用,而不是创建一个新对象。Fraction类的operator=函数看起来应该是下面这样的:
> ```c++
> class Fraction
> {
> //...
> public:
> Fraction &operator=(const Fractio &src)
> {
> set (arc.num, src.den);
> return *this;
> }
> };
> ```
这段代码用到了一个新的关键字this。我将在下一章解释this关键字的作用以及赋值操作符函数的其他细节。
就目前而言,你只要知道根本不需要为像Fraction这么简单的类编写赋值操作符函数就足够了。只要你没有编写这个操作符函数,编译器总是会替你提供一个,而它的默认行为在这里已经够用了。
## 13.6 “等于”比较操作符函数
“等于”比较操作符却是另一番光景。编译器不会为你的类自动提供operator\==函数。如果你自己不去编写一个operator==函数,下面的代码将无法工作:
> ```c++
> Fraction f1(2,3);
> Fraction f2(4,6);
> if(f1 == f2)
> cout << "The fractions are oqual.";
> else
> cout << "The fractions are not equal.";
> ```
尽管“==”操作符的两个操作数(2/3和4/6)不一样,但这段代码的预期行为显然应该是显示一条消息说这两个分数是相等的。
因为我们已经有了normalize函数并且set函数总是会调用它,所以这个比较操作的结果是显而易见的,但前提是我们必须让它真的发生才行!如果两个分数的分子和分母都相等,那它们就肯定是相等的。因此,Fraction类的operator==函数可以编写成下面这样:
> ```c++
> bool Fractiont:operator==(const Fraction &other)
> {
> if (num == other.num & den == other.den)
> return true;
> else
> return false;
> }
> ```
这个函数定义还可以改写得更加紧凑;
> ```c++
> bool Fractiont:operator==(const Fraction &other)
> {
> return (num == other.num & den == other.den);
> }
> ```
这个函数定义现在已经足够简短,很适合被改写为内嵌函数:
> ```c++
> class Fraction
> {
>
> public:
> bool Fractiont:operator==(const Fraction &other){
> return (num == other.num & den == other.den);
> }
> };
> ```
## 13.7 类与流: operator<<函数
在此之前,只要我们想显示某个分数的内容就不得不写出类似下面的这样一些代码
行。我觉得这很麻烦:
> ```c++
> cout << f3.get_num() << "/" << f3.get_den()<< "." << endl;
> ```
要想解决这个问题,最容易想到的办法是编写一个函数。把这个思路扩大,因为每个类都有它自己的数据格式,所以每个类都应该有它自己的内容显示函数。你甚至可以把这样的成员函数命名为print,因为它并不是C++里的保留字。
> ```c++
> void Fraction::print()
> {
> cout << f3.get_num() << "/" << f3.get_den()<< "." << endl;
> }
> ```
尽管这个解决方案很不错,但它距离最好还是有一大段差距。更加面向对象的解决方案利用了cout是一个对象的事实。理想的print函数应该能够和cout以及与cout同属一类的所有对象配合使用。
我们的目标是可以使用如下所示的语句来显示某个对象的内容:
> ```c+=
> cout << fract;
> ```
支持这种语句的办法是编写一个operator<<函数,与cout的父类ostream打交道。这个函数必须是全局函数,这是因为它的左操作数是ostream类的对象,而我们没有办法去更新或修改ostream类的代码。
这个函数必须被声明为Fraction类的个友元函数,这样才能访问到Fraction类的私有成员。
> ```c++
> claas Fraction
> {
> // ...
> public:
> friend ostream &operator<< (ostream &os, Fraction &fr);
> };
> ```
注意,这个函数将返回一个代表ostream对象的引用。只有这样,如下所示的语句才能正确地工作:
```c++
cout << "The fraction's value is " << fract << endl;
最后,下面是我为Fraction类编写的operator<<函数的函数定义:
```c++
ostream &operator<< (ostream &os, Fraction &fr)
{
os << fr.num << "/"<< fr.den;
return os;
}
这个解决方案可以直接把Fraction对象的内容发送到任何一个给定的ostream对象去输出。比如说,如果outfi1e是某个文本文件输出对象的话,你就可以通过它把一个分数输出到与之相关的文件里去:
```c++
outfile << fract;
cout << "The object "<< fract;
cout << " was Drinted to a file." << endl;
示例 完整的 Fraction类
尽管你还可以把更多的扩展功能添加到Eraction类里去,(尤其是对减法和除法操作的支持,参见编程示例13-2的练习题。)但目前这个类现在已经很完备了。
下面是Fraction类到目前为止最完整的版本以及一些用来对它进行测试的代码。和往常一样,只有那些最近新增的代码是以粗体字印刷的。
```c++
/ Fract5.cpp /
include <iostream>
using namespace std;
class Fraction
{
private:
int num, den; // Numerator and denominator.
public:
Fraction() { set(0, 1); }
Fraction(int n, int d) { set(n, d); }
Fraction(int n) { set(n, 1); }
Fraction(Fraction const &src);
void set(int n, int d){num = n; den = d; normalize();}
int get_num() const {return num; }
int get_den() const {return den; }
Fraction add(const Fraction other);
Fraction mul(const Fraction other);
Fraction operator+(const Fraction &other){ return add(other); }
Fraction operator*(const Fraction &other){ return mul(other); }
bool operator==(const Fraction &other);
friend ostream &operator<<(ostream &os, const Fraction &fr);
private:
void normalize(); // Convert to standard form.
int gcf(int a, int b); // Greatest Common Factor.
int lcm(int a, int b); // Lowest Common Denomin.
};
int main()
{
Fraction f1(1, 2);
Fraction f2(1, 3);
Fraction f3 = f1 + f2;
cout << "The value of f3 is ";
cout << f3.get_num() << "/" << f3.get_den() << endl;
system("PAUSE");
return 0;
}
// Fraction Class Functions
Fraction::Fraction(Fraction const &src)
{
cout << "Now calling copy constructor. " << endl;
num = src.num;
den = src.den;
}
Fraction Fraction::add(const Fraction other)
{
Fraction fract;
int lcd = lcm(den, other.den);
int quot1 = lcd / den;
int quot2 = lcd/other.den;
fract.set(num quot1 + other.num quot2, lcd);
return fract;
}
Fraction Fraction::mul(const Fraction other)
{
Fraction fract;
fract.set(num other.num, den other.den);
return fract;
}
// Normalize: put fraction into standard form, unique
// for each mathematically different value.
void Fraction::normalize()
{
// Handle cases involving 0
if (den == 0 || num == 0)
{
num = 0;
den = 11;
}
// Put neg. sign in numerator only.
if (den < 0)
{
num *= -1;
den *= -1;
}
// Pactor out GCP from numerator and denominator.
int n= gcf(num, den);
num = num / n;
den = den / n;
}
// Greatest Common Factor
int Fraction::gcf(int a, int b)
{
if (b== 0)
return abs(a);
else
return gcf(b, a%b);
}
// Lowest Common Multiple
int Fraction::lcm(int a, int b)
{
int n = gcf(a, b);
return a / n * b;
}
bool Fraction::operator==(const Fraction &other)
{
return (num == other.num & den == other.den);
}
friend ostream &operator<<(ostream &os, const Fraction &fr)
{
os << fr.num << "/"<< fr.den;
return os;
}
### 代码分析
这个示例程序里的Fraction类又多了几样新东西:
> - [ ] 只有一个int输入参数的构造器;
> - [ ] 用来支持“等于”比较操作符(==)的操作符函数;
> - [ ] 用来把Fraction对象输出到一个ostream对象(如cout)的全局函数。
这个示例程序又给Fraction类增加了一个构造器,只用一个整数作为其输入参数。这个构造器带来的好处之一是,程序可以在必要时自动地把整数转换为Fraction对象。
> ```c++
> Fraction(int n) { set(n, 1); }
> ```
这个函数的动作是以给定整数为分子,以1为分母创建Fraction对象。这意味着1将被转换为1/1,2将被转换为2/1,5将被转换为5/1,依此类推。
这个功能完全符合有关的数学原理。以整数5为例,当它被转换为5/1的时候,它的大小在本质上没有发生变化,只是变成了分数格式而已。
Fraction类的其他新扩展来自此前几个小节里介绍过的代码。首先,类声明里增加了两个新函数的声明:
> ```c++
> bool operator==(const Fraction &other);
> friend ostream &operator<<(ostream &os, const Fraction &fr);
> ```
因为operator\==函数是Fraction类的一个成员函数,所以该函数的名字在Fraction类的外部必须写成Fraction::operator==。
> ```c++
> bool Fraction::operator==(const Fraction &other)
> {
> return (num == other.num & den == other.den);
> }
> ```
注意,不带范围限定前缀的num和den代表的是“这个对象”的成员。换句话说, 13代表左操作数。表达式other.num和other.den代表的则是右操作数的值。
operator<<函数是全局函数,但它同时也是Fraction类的友元函数。因此,它可以访问到Fraction类的私有数据num和den。
> ```c++
> friend ostream &operator<<(ostream &os, const Fraction &fr)
> {
> os << fr.num << "/"<< fr.den;
> return os;
> }
> ```
> > **练习题**
>
> 练习题13.3.1 修改编程示例13-4里的operator<<函数,让它以(n, d)的格式显示一个分数,其中n和d分别是那个分数的分子和分母(num和den成员)。
>
> 练习题13.3.2 为Fraction类编写“大于”函数(>)和“小于”(<)函数,然后改写编程示例13-4中的main函数以测试这些函数。比如说,测试1/2+1/3是否大于5/9。(提示:给定两个分数A/B和C/D,如果A*D>B*C,则A/B大于C/D。)
>
> 练习题13.3.3 为Point类编写operator<<函数,它将把Point对象的内容发送到ostream对象(例如cout)。假设这个函数已经声明为Point类的友元函数,只编写它的函数定义即可。
## 13.8 仅限 C++0x:用戶定义字面值
> **C++0x 本小节里的讨论只适用于符合C++0x规范的编译器。**
C++0x规范是为了响应众多C++程序员多年来的呼声而制定的。用户定义字面值是这些呼声当中最广泛和最强烈的之一。
作为变量或符号的对立面,一个字面值会在编译器读到它的时候给出一个固定不变的值,而这个值就是它本身。下面这些全都是C++中标准的字面值:
> ```c++
> 100
> 0xffff108e
> -25
> 3.1415
> ```
C++有内建的十六进制、八进制、浮点和十进制字面值(以及字符串字面值),但并无能提供程序员需要的所有东西。比如说,程序员在某些场合可能更希望能够像下面这样写出一个二进制字面值:
> ```c++
> 111100001100B
> ```
这里的“B”后缀用来表明这是一个二进制数而在另外一些场合,程序员可能更希望能够像下面这样写出一个带虚数部分的复数:
> ```c++
> 2i
> ```
C++0x规范允许你对C++语言进行扩充,允许你创建像上面这样的新的字面值格式并把它们用在程序里。这种灵活性实在是令人叹服。
既然我们已经和Fraction类打了那么长时间的交道,现在就借这个机会为Fraction字面值创建一种新格式好了。根据人们的日常习惯,如果能够使用像3/7r(r代表rational,意思是实数)这样的表达式来写出一个分数当然是最好的,但可惜的是这不现实。斜线字符(/)是一个操作符,C++会把3单独解释为一个数字而执行整数除法并对结果进行舍入。因此,我们只能选用其他字符来充当新格式里的分隔符,比如下划线字符(_)。
> ```c++
> 1_2r // one nalf (1/2)
> 2_3r // Two thirds (2/3)
> 13_50r //13/50
> ```
### 字符串字面值
定义一种新型字面值的办法之一是,直接读取并处理字符串数据。这个办法需要使用以下语法:
> ```c++
> type operator "suffix" (const char *str){ }
> ```
在这个语法里,type是你预期生成的对象的类型。具体到我们这个例子,它应该是Fraction。suffix是由一个或多个字符构成的后缀式格式标记。当这个标记出现在某个字面值的末尾时,这个函数就会被调用。(C++只支持后缀式格式标记,不支持前级式。)
我们可以参考第12章末尾的代码创建一个函数,为Fraction类定义一种使用r后缀的新型字面值。(注意这个函数利用了一个构造器“即时”创建出一个Fzaction对象并返回之。)
> ```c++
> #include <cstring>
> #include <cstdlib>
> //...
> Fraction operator "r" (const char *s)
> {
> int n = 0;
> int d = 1;
> char *p1 = strtok(s,"_");
> char *p2 = strtok(nullptr,"_");
> if(p1)
> n = atoi (pl);
> if(p2)
> d = atoi(p2);
> return Fraction(n, d);
> }
> ```
这里使用了下划线字符作为新的字面值格式中的分隔符,该字符与任何C++操作符 1、都不冲突。
函数定义完成后,我们就可以编写出如下代码:
> ```c++
> Fraction a = 11_12r; // Assign 11/12.
> Fraction b = 1_2r + 1_3r; // Assign 5/6.
> ```
### 定制型字面值
与字符串型字面值相比,定制型字面值其实更容易使用。定制型字面值需要从一种现有的数据格式(通常是int或double)开始定义。
下面是以浮点类型(double)定义一种新型字面值的语法:
> ```c++
> type operator"suffix"(double data){}
> ```
当suffix出现在某个值常数的末尾时,suffix前面的字符将被解释为double类型的值(整数值将被自动转换为double类型)并成为这个函数的输入参数。比如说,假设你已经声明了如下所示的Complex类:
> ```c++
> class Complex
> {
> public:
> double real;
> double imag;
> Complex(double r){real = r; imag = 0;}
> Complex(double r, double i){real = r; imag = i;}
> //...
> };
> ```
现在,你决定创建”i”后缀来表示虚数,这很容易做到:
> ```c++
> Complex operator "i" (double x)
> {
> return Complex(0, x);
> }
> ```
有了这个函数定义,你就可以写出下面这样的表达式了:
> ```c++
> Complex number = 52.7i;
> Complex y = 1.5 + 2i; // Assuming there is an operator+ fnct. Gefined
> ```
## 13.9 小结
- [ ] 下面是为某个类声明操作符函数的语法,其中的“&”字符代表着任何一个合法的C++操作符。
> ```c++
> return_type operator@(argument_list){ /*...*/ }
> ```
- [ ] 操作符函数可以被声明为成员函数,也可以被声明为全局函数。如果它是成员函数,它将只有一个输入参数(对二元操作符而言)。例如,我们可以为Point类的operator+函数写出下面这样的声明和定义;
> ```c++
> class Point
> {
> //...
> public:
> Point operator+(const Point &pt);
> };
>
> Point Point::operator+(const Point &pt)
> {
> Point new_pt;
> new_pt.x=x + pt.X;
> new_pt.y=y + pt.y;
> return new_pt;
> }
> ```
- [ ] 有了这些代码,编译器就会知道如何处理加法操作符和两个Point对象的加法运算了:
```c++
point1 + point2;
```c++
Point operator+(Point ptl, Point pt2)
{
Point new_pt;
new_pt.x = pt1.x + pt2.x;
new_pt.y = ptl.y + pt2.y;
return new_pt;
}
- [ ] 把操作符函数编写为全局函数的局限性之一,是它将无法访问那个类里的私有成员。为了克服这一局限性,你可以把这个全局函数声明为那个类的友元函数。如下所示:
```c++
class point
{
//...
public:
friend Point operator+(Point pt1, Point pt2);
};
```c++
Fraction(int n) {set (n, 1);};
-
[ ] 如果你没有编写赋值操作符函数,编译器会自动提供一个。编译器提供的赋值操作符函数的行为是进行一次简单的“成员到成员”复制。
-
[ ] 口编译器不会提供“等于”比较函数(==),所以如果你需要比较两个对象是否相等,就必须自己去编写一个。对于这个函数,只要你的编译器支持bool类型,你就应该使用bool返回类型,否则,应该使用int返回类型。
-
[ ] 如果你想简便、快捷地显示某个类的对象的内容,就应该把那个类的operator<<函数编写为一个全局函数。它的第一个输入参数应该是ostream类型,这样你才能使用流操作符(<<)把那个类的对象的内容发送到cout或是其他的输出流类。你应该先把这个函数声明为那个类的友元函数;如下所示:
```c++
class Point
{
// ...
public:
friend ostream &operator<<(ostream &os, Fraction &fr);
};
- [ ] 在operator<<函数的函数定义里,有关语句应该把来自右操作数(下例中的fr)的数据写到ostream的输入参数。这个函数最后应该返回ostream输入参数本身。如下所示:
friend ostream &operator<<(ostream &os, const Fraction &fr)
{
os << fr.num << "/"<< fr.den;
return os;
}
- [ ] C++0x规范允许编写一个名为operator suffix的函数,来创建用户定义字面值。字符串字面值的语法如下所示:
```c++
Fraction operator "r" (const char *s)
{
// ...
}