第10章 类和对象


C++最令人着迷的主题之一就是面向对象。理解它并用面向对象编程(Object-Oriented Programming,OOP)技术写了几个程序之后肯定会爱上它。不过,它背后的概念刚开始的时候还是比较模糊,有一定挑战性。

总体上说,面向对象是完成分析和设计的一种方式。C++提供了一些有用的工具,但只有理解了OOP设计是什么之后才好用。

接着6章将围绕这一主题展开,许多项目不采用面向对象的方式会很难。

10.1 理解OOP

面向对象编程(OOP)是一种模块化编程方式:对密切相关的代码和数据进行分组。主要规范如下。

进行OOP设计首先要问:操作的主要数据结构是什么?每种数据结构要执行什么操作?

第15章会讨论如何通过面向对象的设计方法简化一个表面上复杂和困难的项目(视频扑克)。这里可以先简单地解释一下。扑克牌游戏要用到下面两个类。

  • Deck(牌墩)类。负责一副牌的所有随机化、洗牌和重新洗牌程序其余部分便不必关心这些细节。
  • Card(牌张)类。包含跟踪一张牌所需的信息:牌点(2到A)和花色(黑桃、红桃、梅花和方块)。为每个Card对象赋予显示自身的能力。

写好这两个类之后,写主程序来玩游戏就简单多了。记住每个类都是密切相关的函数和数据结构的组合。许多书都在讲“封装”和“数据抽象”,但意思都一样:隐藏细节!

写好类之后,下一步是用类创建对象。但何为对象?

10.2 对象的含义

类是一种数据类型而且可能是较“智能”的那种。数据类型和该类型的实例之间存在“一对多”的关系。例如,只有一种 int 类型(和几种相关类型,比如unsigned),但可以有任意数量的整数,几百万个都没有问题

对象在C++中是指实例,尤其是类的实例。扑克牌游戏要创建Deck 类的一个实例和card 类的至少5个实例。

简单地说,对象是一种智能数据结构,具体由它的类决定。对象就像一条数据记录,但能做更多的事情。它能响应通过函数调用发来的请求。初次接触这一概念,你可能感觉非常新奇。我希望你能保持这种兴趣!

下面是OOP的常规步骤。按这个顺序,你可以多做一些,也可以少做一些,虽然实际上可能要在这些步骤中反复。

  1. 声明类,或从库中获取一个现成的。
  2. 创建该类的一个或多个实例(称为对象)。
  3. 操纵对象来达成目标。

下面依次讨论一下。首先设计并编写类。类是扩展的数据结构,定义了其实例的行为(以成员函数的形式,或称方法)和数据字段。

声明好类并定义好它的成员之后,程序就可以创建类的任意数量的实例(对象)。如下图所示,这是一种一对多关系。

在这里插入图片描述

最后,程序用对象存储数据,还可向对象发出请求,要求其执行任务,如下图所示,虽热每个对象都包含它自己的数据,但函数代码在同一个类的所有对象之间共享。

在这里插入图片描述

讲得并不完整,还存在其他可能性,比如对象包含其他对象,但是类、对象和程序其余部分之间的关系仍然可见一斑。为了有一个更形象的理解,本章剩余部分将着眼于两个简单的类:Point 和Fraction。

10.3 Point: 一个简单的类

以下是C++class关键字的常规语法:

class类名{
	声明
};

除非要写子类,否则上述语法不会变得更复杂。可在声明中包含数据声明和/或函数声明。下面是只涉及数据声明的一个简单例子。

class Point{
	int x, y;// 私有,也许不能访问
};

类成员默认私有,不能从类的外部访问。所以上面声明的 Point 类实际无用。要变得有用,类至少要包含一个公共成员:

class Point{
public:
	int x, у;
};

这样就好得多,类现在能用了。Point 类声明好后,就可以开始声明 Point 对象,比如 pt1,pt2 和pt3:

Point pt1, pt2, pt3;

创建好对象后就可向单独的数据字段(称为数据成员)赋值:

pt1.x = 1; //将pt1设为1,-2
pt1.y = -2;
pt2.x = 0;//将pt2设为0,100
pt2.y = 100;
pt3.x = 5;//将pt3设为5,5
pt3.y = 5;

Point 类声明指出每个Point 对象都包含两个数据字段(成员):x 和 y,它们可当作整数变量使用.

cout << pt1.y + 4;//打印两个整数之和

用以下语法引用对象的数据字段:

对象.成员

本例的对象是指 Point 类的某个实例,成员是x或y。
结束这个简单版 Point 类的讨论之前,一个语法问题值得强调:类声明以分号结尾

class Point{
public:
int x, y
};

新手经常在分号的使用上“拎不清”。类声明要求在结束大括号(})后添加分号,而函数定义无此要求(如添加,相当于一个空语句)。语言规范如下所示。

类或数据声明总是以分号结尾。

总之,类声明要在结束大括号后添加分号,函数定义不用。

C程序员必读:结构和类
C++语言的 struct 和 class 关键字等价,只是 struct 的成员默认公共。两个关键字在C+中都创建类。这意味着“类”一词和关键字 class 并不严格对应。换言之,可能存在不是用class 关键字创建的类。

在C语言中声明结构,凡是出现新类型名称的地方都必须重用 struct 关键字。例如:

struct Point pt1,pt2,pt3;

C++语言无此要求。一旦用 struct 或 class 关键字声明了类,在涉及类型的地方都可以直接使用名称。从C语言代植到C++语言之后,上述数据声明应替换成以下代码:

Point pt1,pt2,pt3;

C+语言对 struct 的支持是为了向后兼容。C语言的代码经常使用 struct 关键字;

struct Point{
int x,y;
}

C语言不支持 public 或 private 关键字,而且 struct 类型的用户必须能访问所有成员。为保持兼容,用 struct 声明的类型的成员必须默认公共。

如此一来,C++还需要 class 关键字做什么?技术上确实不需要,但 class 显著增强了可读性,让人一看就知道该类型可能封装了某些行为(使用类的目的通常就是添加函数成员)。此外,一个很好的设计条款是让类成员默认私有。在面向对象程序设计中,只有在有充分理由的前提下,才应考虑让成员成为公共。

私有:仅成员可用(保护数据)

上一节的 Point 类允许直接访问数据成员,因为它们被声明为公共。但要控制对数据成员的访问怎么办(例如为了限制数据的范围)?解决方案是使数据成员成为私有,再通过公共函数来访问.

以下Point类的修改版本禁止从类的外部直接访问 x 和 y .

class Point{
private:	//私有数据成员
	int x,y;
public:		//公共成员函数
	void set(int newx, int newy);
	int get_x();
	int get_y();
};

声明了三个公共成员函数,即 set ,get_x 和 get_y,还声明了两个私有数据成员。创建好Point对象后,只能通过调用某个成员函数来处理类的数据。
Point point1;point1.set(10,20);cout <s point1.get-x()<<","<< point1.get-y();上述语句将打印以下输出:
10,20语法并不新鲜。过去几章为字符串和cin等对象用过。圆点(.)语法意指将一个特定函数
(例如get-x)应用于特定对象。
point1.get_x()
当然,函数成员不可能凭空生成。和其他函数一样,必须在某个地方定义。可将函数定义放在你喜欢的任何位置,只要之前已声明好类。

Point:: 前缀界定函数定义作用域,使编译器知道该定义应用于 Point 类。前缀很重要,因为其他类可能有同名函数。

void Point::set (int new_x, int new_y){
	x = new_x;
	y = new_y;
}

int Point::get_x() (
	return x;
)

int Point::get_y()
{
	return y;
}

作用域前缀 Point:: 应用于函数名。返回类型(void 或 int)仍然在它们应该在的位置,即函数定义最开头。所以可将 Point:: 想象成函数名修饰符。

现在可以总结出成员函数定义的语法:

类型 类名::函数名(参数列表)
{
	语句
}

声明并定义好成员函数之后,就可凭借它们来控制数据。例如,可以重写 Point::set 函数,将负的输入值转换成正值。

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
		new_x *= -1;
	if (new_y < 0)
		new_y *= -1;
	x = new_x;
	y = new_y;
}

这里使用了乘后赋值操作符(*=)。new_x *= -1 等价于 new_x = new_x * -1。

虽然类外的函数不能直接引用私有数据成员 x 和 y,但类内的成员函数可以,无论是否私有。可以想象,Point 类的每个对象都共享同一种结构,如下图所示。

在这里插入图片描述

类声明描述类型(Point)的结构和行为,而每个 Point 对象都存储了自己的数据值。例如,以下语句打印 pt1 中存储的 x 值:

cout << pt1.get_x();	// 打印 pt1 中的 x 值

以下语句打印 pt2 中存储的 x 值:

cout << pt2.get_x();	// 打印 pt2 中的 x 值

例 10.1:测试 Point 类

以下程序对 Point 类进行简单测试,设置并获取一些数据。

# include <iostream>
using namespace std;

class Point {
private:	// 私有成员变量
	int x, y;
public:		// 公有成员变量
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main() {
	Point pt1, pt2;	// 创建两个 Point 对象
	pt1.set(10, 20);
	cout << "pt1是 " << pt1.get_x();
	cout << ", " << pt1.get_y() << endl;
	pt2.set(-5, -25);
	cout << "pt2是 " << pt2.get_x();
	cout << ", " << pt2.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y) {
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x() {
	return x;
}

int Point::get_y() {
	return y;
}

运行后输出如下:

​ p1 是 10,20

​ p2 是 5,25

练习

练习10.1.1

修改set函数,为 × 和 y 值规定一个100的上限;大于100的输入值减小为100,修改 main 测试这一行为。

答案:

# include <iostream>
using namespace std;

class Point {
private:
	int x, y;
public:
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main() {
	Point pt1, pt2;

	pt1.set(120, 20);
	cout << "pt1 是 " << pt1.get_x();
	cout << ", " << pt1.get_y() << endl;
	pt2.set(-5, -25);
	cout << "pt2 是 " << pt2.get_x();
	cout << ", " << pt2.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}

	if (new_x > 100)
	{
		new_x = 100;
	}
	if (new_y > 100)
	{
		new_y = 100;
	}

	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

练习10.1.2

为point类写两个新成员函数set-x和sety来分开设置x和y。记住和set函数一样,要反转可能输入的负号。

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	void set(int new_x, int new_y);
	void set_x(int new_x);
	void set_y(int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point pt1, pt2;

	pt1.set(10, 20);
	cout << "pt1 是 " << pt1.get_x();
	cout << ", " << pt1.get_y() << endl;
	pt2.set(-5, 25);
	cout << "pt2 是 " << pt2.get_x();
	cout << ", " << pt2.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

void Point::set_x(int new_x)
{
	if (new_x < 0)
	{
		new_x *= -1l;
	}
	x = new_x;
}

void Point::set_y(int new_y)
{
	if (new_y < 0)
	{
		new_y *= -1;
	}
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

练习10.1.3

修改例子显示5个Point对象的x和y值。

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point ptA, ptB, ptC, ptD, ptE;

	ptA.set(5, -5);
	ptB.set(11, 20);
	ptC.set(20, -200);
	ptD.set(1, 0);
	ptE.set(-8, -8);

	cout << "ptA 是 " << ptA.get_x();
	cout << ", " << ptA.get_y() << endl;

	cout << "ptB 是 " << ptB.get_x();
	cout << ", " << ptB.get_y() << endl;

	cout << "ptC 是 " << ptC.get_x();
	cout << ", " << ptC.get_y() << endl;

	cout << "ptD 是" << ptD.get_x();
	cout << ", " << ptD.get_y() << endl;

	cout << "ptE 是 " << ptE.get_x();
	cout << ", " << ptE.get_y() << endl;

	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

练习10.1.4

修改例子创建7个Point对象的一个数组。用一个循环提示输入每个对象的值,再用一个循环打印全部值。提示:可用类名声明数组,和其他任何类型一样。
point array_of_points[7]

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point array_of_points[7];

	for (int i = 0; i < 7; ++i)
	{
		int x, y;
		cout << "第 " << i+1 << " 个点" << "..." << endl;
		cout << "输入 x 的坐标:";
		cin >> x;
		cout << "输入 y 的坐标:";
		cin >> y;
		array_of_points[i].set(x, y);
	}

	for (int i = 0; i < 7; ++i)
	{
		cout << "第【" << i+1 << "】个点的坐标是";
		cout << array_of_points[i].get_x() << ", ";
		cout << array_of_points[i].get_y() << "。" << endl;
	}

	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

10.4 Fraction 类基础

理解面向对象编程的好办法是着手定义一个新的数据类型。在C++中,类成为对语言本身的一种扩展。分数类 Fraction(也称为有理数类)就是一个很好的例子。该类存储两个数字来代表分子和分母。
如果需要精确存储 1/3 或 2/7 这样的数,就适合使用 Fraction类。甚至可用此类存储货币值,比如$1.57。
出于多方面的原因,创建 Fraction 类时要限制对数据成员的访问。最起码要防止分母为零,1/0不合法。

甚至一些合法的运算,也有必要对比值进行合理简化(标准化),确保每个有理数都有唯一表达式。例如,3/3 和 1/1 是同一个数,2/4 和 1/2 同理。

后面几个小节将开发函数来自动处理这些事务,防止分母为零并进行标准化。类的用户可创建任意数量的Fraction对象,而且类似以下操作能自动完成:

Fraction a(1, 6);	//a = 1/6
Fraction b(1, 3);	//b= 1/3
if(a + b == Fraction(1, 2))
cout << "1/6 + 1/3 等于 1/2";

是的,就连加法(+)都能支持,详情在第18章讲述。但先从类的最简单版本开始。

class Fraction{
private:
    int num, den;	//num代表分子,den代表分母
public:
    void set(int n, int d);
    int get_num();
    int get_den();
private:
    void normalize();	//分数化简
	int gcf(int a, int b);	//gcf代表最大公因数(Greatest Common Factor)
     int lcm(int a, int b);	//lcm代表最小公倍数(Lowest Common Multiple)
};

类声明由三部分组成。

  • 私有数据成员 num 和 den,分别存储分子和分母。例如,对于分数 1/3,1是分子,3是分母。

  • 公共函数成员。提供类数据的访问渠道。

  • 私有函数成员。一些支持函数,本章以后会用到。目前只是返回零值,作为私有成员,它们不能从外部调用,只限内部使用。

声明并定义好这些函数之后,就可用类来执行一些简单操作,例如:

Fraction my_fract;
my_fract.set(1, 2);
cout << my_fract.get_num();
cout << "/";
cout << my_fract.get_den();

目前似乎没什么新鲜,但我们才刚刚开头。可像下图这样想象 Fraction 类。

在这里插入图片描述

成员函数的定义需要放到程序的某个地方,类声明之后的任何地方都可以。

void Fraction::set(int n, int d){
	num = n;
    den = d;
}

int Fraction::get_num(){
	return n;
}

int Fraction::get_den(){
	return d;
}
//尚未完工...
//剩余函数语法上正确,但还不能做任何有用的事情
//以后补充
void Fraction::normalize()
{
    return;
}

int Fraction::gcf(int a, int b)
{
    return 0;
}

int Fraction::lcm(int a, int b)
{
    return 0;
}

内联函数

Fraction 类有三个函数所做的事情十分简单:设置(set)或获取(get)数据。它们特别适合
“内联”。

函数内联后,程序不会将控制转移到单独的代码块。相反,编译器将函数调用替换成函数主体。下例将 set 函数内联:

void set() {num = n; den = d;}

一旦在程序代码中遇到以下语句:

fract.set(1, 2);

编译器就会在该位置插入 set 函数的机器码指令。相当于替换成以下C+代码:

{fract.num = 1; fract.den = 2};

即使 num 和 den 私有,上述代码也合法,因其由成员函数执行。
函数定义放到类声明中即可使函数内联。这种函数定义不要在末尾加分号(;),即使它们是成员声明。

在下面的例子中,改动过的代码加粗显示:

class Fraction
{
private:
    int num, den; // num 代表分子, den 代表分母
public:
    void set(int n, int d){num = n; den = d; normalize();}
    int get_num(){return num;}
    int get_den(){return den;}
private:
    void normalize();	// 分数化简
    int gcf(int a, int b);	// gcf 代表最大公因数(Greatest Common Factor)
    int lcm(int a, int b);	// lcm 代表最小公因数(Lowest Common Multiple)
};

没有内联的三个私有函数仍需在程序某个地方单独定义。

void Fraction::normalize()
{
    return;
}

int Fraction::gcf(int a, int b)
{
    return 0;
}

int Fraction::lcm(int a, int b)
{
    return 0;
}

短函数课通过内联提升效率。记住, 由于函数定义包含在类声明中,所以不需要在其他地方定义。下表中对内联函数和类的其他函数进行了比较。

内联函数类的其他函数
在类声明中就定义好了(而非仅是声明)在类声明外部定义,在类中给出原型
不需要作用域前缀(如 Point:: )定义时要写作用域前缀
编译时函数主体就“内联”(插入)到代码中运行时发出真正的函数调用控制转至另一个代码位置
适合小函数适合较长的函数
有些限制,不可递归调用无特殊限制

找出最大公因数

Fraction 类中的行动基于数论的两个基本概念:最大公因数(Greatest Common Factor,GCF)和最小公倍数(Lowest Common Multiple,LCM)。第5章介绍了欧几里德最大公因数算法,这里直接用就好了,见下表。

数字最大公因数
12,186
12,102
25,5025
50, 7525

以下是用递归函数写的欧几里得最大公因数算法:

int gcf(int a, int b)
{
    if(b == 0)
    {
        return a;
    }
    else
    {
        return gcf(b, a%b);
    }
}

添加Fraction:: 前缀,即变为成员函数:

int Fraction::gcf(int a, int b)
{
    if(b == 0)
    {
        return a;
    }
    else
    {
        return gcf(b, a%b);
    }
}

向GCF函数传递负数发生奇怪的事情:仍然产生正确的结果,gcf(35,-25)返回5,但正负号不好预测。解决方案是用绝对值函数 abs 确保仅返回正数,改动部分加粗显示。

int Fraction::gcf(int a, int b)
{
    if(b == 0)
    {
        return abs(a);
    }
    else
    {
        return gcf(b, a%b);
    }
}

找出最小公倍数

另一个有用的支持函数获取最小公倍数(lowest common multiple, LCM)。GCF函数已创建好,LCM应该很轻松。

LCM是两个数的最小整数倍数。例如,200 和 300的 LCM是600,而GCF是100。

找出LCM关键是先分解最大公因数,确保该公因数最后只乘一次。否则,假如直接让A和B相乘,就相当于公因数被乘两次。所以,必须先从A和B中移除公因数。公式是;

n = GCF(a, b)
LCM(A, B) = n * (a / n) * (b / n)

第二行简化如下:

LCM(A, B) = a / n * b

这样就可以很容易地写出LCM函数:

int Fraction::lcm(int a, int b)
{
    int n = gcf(a, b);
    return a / n * b;
}

例10.2:Fraction 类的支持函数

GCF和LCM函数现在可加入Fraction类。以下是该类的第一个能实际工作的版本。添加了 normalize 函数的代码,作用是在每次运算后对分数进行简化。

# include <cstdlib>

class Fraction
{
private:
	int num, den;	// num 代表分子,den 代表分母
public:
	void set(int n, int d)
	{
		num = n; den = d; normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
private:
	void normalize();	// 分数化简
	int gcf(int a, int b);	// gcf 代表最大公因数
	int lcm(int a, int b);	// lcm 代表最小公倍数
};

// Normalize(标准化):分数化简
// 数学意义上每个不同的值都唯一
void Fraction::normalize()
{
	// 处理涉及0的情况
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}
	// 仅分子有负号
	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}
	// 从分子和分母中分解出GCF
	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

// 最大公因数
//
int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

// 最小公倍数
//
int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

gcf 函数递归调用自身时不必使用 Fraction:: 前缀。这是因为在类成员函数内部,默认使用该类的作用城,类似地,Fraction::lcm 函数调用 gcf 时也默认使用类作用域。

int Fraction::lcm(int a, int b)
{
 int n = gcf(a, b);
 return a/n * b;
}

C+编译器每次遇到一个变量或函数名时,一般按以下顺序查找与该名称对应的声明。

  • 在同一个函数中查找(比如局部变量)。

  • 在同一个类中查找(比如类的成员函数)。

  • 在函数或类的作用域中没有找到对应声明,就查找全局声明。

normalize函数是唯一出现的新面孔。函数做的第一件事情是处理涉及 0 的情况。分母为0非法,此时分数标准化为0/1。此外,分子为0的所有分数都是同一个值:

​ 0/1 0/2 0/5 0/-1 0/25

以上分数全部标准化为0/1。

Fraction 类的主要设计目标之一就是确保在数学意义上相等的所有值都标准化为同一个值。以后实现 “测试相等性” 操作符时,这会使问题变得简单许多。还要解决负数带来的问题。以下两个表达式代表同一个值:

​ -2/3 2/-3

类似的还有:

​ 4/5 -4/-5

最简单的解决方案就是测试分母;小于0就同时对分子和分母取反。

if(den < 0)
{
    num *= -1;
    den *= -1;
}

normalize 剩余部分很容易理解:分解最大公因数,分子分母都用它来除:

int n = gcf(num, den);
num = num / n;
den = den / n;

以 30/50 为例,最大公因数是10。在 normalize 函数执行了必要的除法运算之后,化简为 3/5。

normalize 函数的重要性在于,它确保相等的值采取一致的方式表示。另外,以后为 Fraction 类定义算术运算时,分子和分母可能积累起相当大的数字。为避免溢出,必须抓住任何机会简化分数。

练习

练习 10.2.1

重写 normalize 函数,使用除后赋值操作符(/)。记住,以下表达式:

a /= b

等价于:

a = a / b

答案:

# include <cstdlib>

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction fr;
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num /= n;
	den /= n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
		return gcf(b, a%b);
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

练习 10.2.2

内联所有你觉得合适的函数。提示:gcf 函数是递归的所以不可内联,而 normalize 又太长。

答案:

# include <cstdlib>

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
private:
	void normalize();
	int gcf(int a, int b);

	int lcm(int a, int b)
	{
		int n = gcf(a, b);
		return a / b * b;
	}
};

int main()
{
	Fraction fr;
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num /= n;
	den /= n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

例10.3:测试 Fraction 类

类声明好之后就可创建并使用对象来测试。以下代码提示输入值,显示最简分式。

答案:

# include <iostream>
# include <string>
# include <cstdlib>

using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	int a, b;
	string str;
	Fraction fract;
	while (true)
	{
		cout << "输入分子:";
		cin >> a;
		cout << "输入分母:";
		cin >> b;
		fract.set(a, b);
		cout << "分子是 " << fract.get_num() << endl;
		cout << "分母是 " << fract.get_den() << endl;
		cout << "再来一次?(Y 或 N)";
		cin >> str;
		if (!(str[0] == 'Y' || str[0] == 'y'))
		{
			break;
		}
	}
	return 0;
}

// Fraction 类的成员函数

// Normalize(标准化):分数化简,
// 数学意义上每个不同的值都唯一
void Fraction::normalize()
{
	// 处理涉及0的情况
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}
	// 仅分子有负号
	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}
	// 从分子和分母中分解出 GCF
	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

// 最大公因数
int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

// 最小公倍数
int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

惯例是将类声明连同其他必要的声明和预编译指令放到一个头文件中。假定头文件的名称是 Fraction.h,需在使用 Fraction 类的任何程序中添加以下代码:

# include "Fraction.h"

没有内联的函数定义必须放在程序的某个地方,或单独编译并链接到项目。
main 的第三行创建一个未初始化的Fraction对象:

Fraction fact;

main 的其他语句设置 Fraction 对象并打印它的值。注意,对 set 函数的调用会进行赋值操作,但set 函数会调用 normalize 函数进行分数化简。

fract.set(a, b);
	cout << "分子是 " << fract.get_num() << endl;
	cout << "分母是 " << fract.get_den() << endl;
一种新的 #include ?

上个例子引入 #include 指令的新语法。记住,为获取某个 C++ 标准库的支持,首选方法是使用尖括号:

#include <iostream>

但包含自己项目的声明就要使用引号:

#include "Fraction.h"

两种语法的效果几乎完全一样,但如使用引号,C+编译器会首先查找当前目录,其次才会查找标准include文件目录(通常由操作系统的环境变量或环境设置决定)。取决于C+编译器的版本,库文件和项目文件或许都能使用引号语法。但惯例是用尖括号开启标准库的功能,本书将沿用该做法。

练习

练习10.3.1

写程序用 Fraction 类设置一组值:2/2,4/8,-9/-9,10/50,100/25.
打印结果并验证每个分数都正确化简。例如,100/25 化简为 5/4.

答案:

# include <cstdlib>
# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction f1, f2, f3, f4, f5;
	f1.set(2, 2);
	f2.set(4, 8);
	f3.set(-9, -9);
	f4.set(10, 50);
	f5.set(100, 25);

	cout << "f1 是 " << f1.get_num() << "/"
		<< f1.get_den() << "." << endl;
	cout << "f2 是 " << f2.get_num() << "/"
		<< f2.get_den() << "." << endl;
	cout << "f3 是 " << f3.get_num() << "/"
		<< f3.get_den() << "." << endl;
	cout << "f4 是 " << f4.get_num() << "/"
		<< f4.get_den() << "." << endl;
	cout << "f5 是 " << f5.get_num() << "/"
		<< f5.get_den() << "." << endl;

	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = -1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num /= n;
	den /= n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

练习10.3.2

创建5个 Fraction 对象的一个数组。写循环输入各自的分子分母。最后写循环打印每个对象(用get函数)。

答案:

# include <cstdlib>
# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num;}
	int get_den() { return den;}
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction fract_arr[5];
	int num, den;

	for (int i = 0; i < 5; ++i)
	{
		cout << "这个数组第 " << i + 1 << " 个对象" << endl;
		cout << "输入分子:";
		cin >> num;
		cout << "输入分母:";
		cin >> den;
		fract_arr[i].set(num, den);
	}
	for (int i = 0; i < 5; ++i)
	{
		cout << "这个数组第 " << i + 1 << " 个对象是:";
		cout << fract_arr[i].get_num() << "/";
		cout << fract_arr[i].get_den() << endl;
	}
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num /= n;
	den /= n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

练习10.3.3

再写一个成员函数来同时显示分子分母。甚至可以显示分式,比如 1/2 或 2/5.

答案:

# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num;}
	int get_den() { return den;}
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction fract1, fract2, fract3;

	fract1.set(1, 2);
	fract2.set(1, 3);
	fract3 = fract1.add(fract2);
	cout << "1/2 + 1/3 = ";
	cout << fract3.get_num() << "/" << fract3.get_den() << endl;
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(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::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

例10.4:分数加法和乘法

为了创建实用的 Fraction 类,下一步是添加两个简单的数学函数;add(加)和 mult(乘),分数加法最难,假定以下两个分数相加:

​ A/B + C/D

诀窍在于先找到最小公分母(Lowest Common Denominator,LCD),即B和D的最小公倍数(LCM):

​ LCD = LCM(B,D)
幸好我们已写好了lcm函数,然后,AB必须用该LCD通分;

A		*		LCD/B
——			——
B		*		LCD/B

这样就得到分母是LCD的一个分数.CD如法炮制:

C		*		LCD/D
——			——
D		*		LCD/D

通分后分母不变,分子相加:

(A * LCD/B)+ (C * LCD/D)
—————————————————————————————
​				LCD

完整算法如下所示。

1. 计算LCD,它等于LCM(B,D)
2. 将Quotient1(商1)设为LCD/B
3. 将Quotient2(商2)设为LCD/D
4. 将新分数的分子设为A * Quotient1 + C * quotient2
5. 将新分数的分母设为LCD

相比之下,两个分数的乘法运算就要简单得多。

  1. 将新分数的分子设为 A * c
  2. 将新分数的分母设为 B * D

现在可以写代码来声明并实现两个新函数。和往常一样,新增或改动的代码行加粗显示:
其他所有代码都来自上个例子。

// Fract3.cpp
# include <iostream>
# include <string>
# include <cstdlib>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	int a, b;
	string str;
	Fraction fract;
	while (true)
	{
		cout << "输入分子:";
		cin >> a;
		cout << "输入分母:";
		cin >> b;
		fract.set(a, b);
		cout << "分子是:" << fract.get_num() << endl;
		cout << "分母是:" << fract.get_den() << endl;
		cout << "再来一次?(Y或N)";
		cin >> str;
		if (!(str[0] == 'Y' || str[0] == 'y'))
		{
			break;
		}
	}
	return 0;
}

// Fraction 类的成员函数

// Normalize(标准化):分数化简
// 数学意义上每个不同的值都唯一
void Fraction::normalize()
{
	//处理涉及0的情况
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}
	// 仅分子有负号
	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}
	// 从分子和分母中分解出 GCF
	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

// 最大公因数
int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

// 最小公倍数
int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(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::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}
工作原理

函数 add 和 mult 应用了之前描述的算法。还使用了一种新的类型签名:获取一个 Fraction 类型的参数,返回一个 Fraction 类型的值。下面来研究 add 函数的类型声明:

Fraction 	Fraction::add	(Fraction other);
—————————	  ——————————	  	————————————
	①				②				③

上述声明中,Fraction的每个实例都具有不同用途。

  • 最开头的 Fraction表明函数返回 Fraction 类型的对象。
  • 前缀 Fraction::表明这是在 Fraction 类中声明的add函数。
  • 圆括号中的 Fraction表明要获取一个 Fraction 类型的参数 other.

每个 Fraction 都是独立使用的。例如,可声明一个不在 Fraction 类中的函数,获取一个 int 参数,返回一个 Fraction 对象。如下所示:

Fraction my_func(int n);

由于 Fraction::add 函数返回一个 Fraction 对象,所以必须先新建对象。

Fraction fract;

然后应用前面描述的算法

int lcd = lcm(den, other.den);
int quot1 = lcd/den;
int quot2 = lcd/other.den;

最后,在设置好新 Fraction 对象(fract)的值之后,函数返回该对象。

return fract;

mult 函数的设计思路与此相似。

练习

练习10.4.1

修改 main 函数,计算任意两个分数相加的结果,并打印结果。

答案:

# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction fract1, fract2, fract3;
	int num, den;

	cout << "输入 fract1 的分子:";
	cin >> num;
	cout << "输入 fract1 的分母:";
	cin >> den;
	fract1.set(num, den);

	cout << "输入 fract2 的分子:";
	cin >> num;
	cout << "输入 fract2 的分母:";
	cin >> den;
	fract2.set(num, den);

	fract3 = fract1.add(fract2);
	cout << "fract1 + fract2 = ";
	cout << fract3.get_num() << "/" << fract3.get_den() << endl;
	return 0;
}

// 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 = 1;
	}

	// put neg. sign in numerator only
	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	// Factor out GCF from numerator and denominator.
	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

// Greatest Common Fator
int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

// Lowest Common Denominator
int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(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::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

练习10.4.2

修改 main 函数,计算任意两个分数相乘的结果,并打印结果。

# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction fract1, fract2, fract3;
	int num, den;

	cout << "输入 fract1 的分子:";
	cin >> num;
	cout << "输入 fract1 的分母:";
	cin >> den;
	fract1.set(num, den);

	cout << "输入 fract2 的分子:";
	cin >> num;
	cout << "输入 fract2 的分母:";
	cin >> den;
	fract2.set(num, den);

	fract3 = fract1.mult(fract2);
	cout << "fract1 * fract2 = ";
	cout << fract3.get_num() << "/" << fract3.get_den() << endl;
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(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::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

练习10.4.3

为早先介绍的 Point 类写一个 add 函数。该函数能将两个 x 值加起来,获得新 x 值;将两个 y 值加起来,获得新 y 值。

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	void set(int new_x, int new_y);
	int get_x();
	int get_y();

	Point add(Point other)
	{
		Point pt;
		pt.set(x + other.x, y + other.y);
		return pt;
	}
};

int main()
{
	Point pnt;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

练习10.4.4

为 Fraction 类写 sub(减)和 div(除)函数,并在 main 中添加相应的代码来测试。注意,sub 的算法与 add 相似。但还可以写一个更简单的函数,也就是是用 -1 来乘参数的分子,再调用一下 add 函数)。

答案:

# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
	Fraction add(Fraction other);
	Fraction mult(Fraction other);

	Fraction sub(Fraction other);
	Fraction div(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction fract1, fract2, fract3;

	fract1.set(1, 2);
	fract2.set(1, 3);

	fract3 = fract1.sub(fract2);
	cout << "1/2 减 1/3 = ";
	cout << fract3.get_num() << "/" << fract3.get_den() << endl;

	fract3 = fract1.div(fract2);
	cout << "1/2 除 1/3 = ";
	cout << fract3.get_num() << "/" << fract3.get_den() << endl;

	return 0;
}

void Fraction::normalize()
{
	if (den ==0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(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::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

Fraction Fraction::sub(Fraction other)
{
	Fraction fract;
	fract.set(other.num * -1, other.den);
	return add(fract);
}

Fraction Fraction::div(Fraction other)
{
	Fraction fract;
	fract.set(num * other.den, den * other.num);
	return fract;
}

小结

  • 类声明具有以下形式:
    class 类名{
    声明
    };

  • C++的 struct 和 class 关键字等价,只是 struct 的成员默认公共。

  • 由于用 class 关键字声明的类的成员默认私有,所以至少要声明一个公共成员。

    class Fraction{
    private:
    	int num, den;
    public:
    	void set(n, d);
    	int get_num();
    	int get_den();
    private:
    	void normalize();
        int gcf();
        int 1cm();
    };
    
  • 类声明和数据成员声明必须以分号结尾,函数定义不需要。

  • 类声明好后可作为类型名称使用,和使用 int,float 和 double 等没什么两样。例如,声明好 Fraction 类之后,就可以声明一系列 Fraction 对象:

      Fraction a, b, c, my_fraction, fract1;
    
  • 类的函数可引用该类的其他成员(无论是否私有),无需作用域前缀(::)。

  • 成员函数的定义要放到类声明的外部,需要使用以下语法:
    类型 类名::函数名(参数列表){
    语句
    }

  • 将成员函数定义放到类声明内部,该函数会被“内联”。不会产生像普通函数那样的调用开销。相反,用于实现函数的机器指令会内嵌到函数调用的位置。

  • 内联函数不需要在结束大括号后添加分号:

    void set(n, d){num =n; den = d;}
    
  • 类必须先声明再使用。相反,函数定义可放到程序中的任何地方(甚至能放到一个单独的模块中),但必须放在类声明后面。

  • 如函数返回类型是类,就必须返回该类的对象,可在函数定义中先声明该类的一个对象(作为一个局部变量),并在最后返回它。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值