文章目錄
- 10.1 了解OOP
- 10.2 對象的含義
- 10.3 Point: 一個簡單的類
-
- 私有:僅成員可用(保護資料)
- 例 10.1:測試 Point 類
- 練習
-
- 練習10.1.1
- 練習10.1.2
- 練習10.1.3
- 練習10.1.4
- 10.4 Fraction 類基礎
-
- 内聯函數
- 找出最大公因數
- 找出最小公倍數
- 例10.2:Fraction 類的支援函數
- 練習
-
- 練習 10.2.1
- 練習 10.2.2
- 例10.3:測試 Fraction 類
-
-
-
- 一種新的 #include ?
-
-
- 練習
-
- 練習10.3.1
- 練習10.3.2
- 練習10.3.3
- 例10.4:分數加法和乘法
-
-
-
- 工作原理
-
-
- 練習
-
- 練習10.4.1
- 練習10.4.2
- 練習10.4.3
- 練習10.4.4
- 小結
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的正常步驟。按這個順序,你可以多做一些,也可以少做一些,雖然實際上可能要在這些步驟中反複。
- 聲明類,或從庫中擷取一個現成的。
- 建立該類的一個或多個執行個體(稱為對象)。
- 操縱對象來達成目标。
下面依次讨論一下。首先設計并編寫類。類是擴充的資料結構,定義了其執行個體的行為(以成員函數的形式,或稱方法)和資料字段。
聲明好類并定義好它的成員之後,程式就可以建立類的任意數量的執行個體(對象)。如下圖所示,這是一種一對多關系。
最後,程式用對象存儲資料,還可向對象送出請求,要求其執行任務,如下圖所示,雖熱每個對象都包含它自己的資料,但函數代碼在同一個類的所有對象之間共享。
講得并不完整,還存在其他可能性,比如對象包含其他對象,但是類、對象和程式其餘部分之間的關系仍然可見一斑。為了有一個更形象的了解,本章剩餘部分将着眼于兩個簡單的類: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,18 | 6 |
12,10 | 2 |
25,50 | 25 |
50, 75 | 25 |
以下是用遞歸函數寫的歐幾裡得最大公因數算法:
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 時也預設使用類作用域。C+編譯器每次遇到一個變量或函數名時,一般按以下順序查找與該名稱對應的聲明。int Fraction::lcm(int a, int b) { int n = gcf(a, b); return a/n * b; }
- 在同一個函數中查找(比如局部變量)。
- 在同一個類中查找(比如類的成員函數)。
- 在函數或類的作用域中沒有找到對應聲明,就查找全局聲明。
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就同時對分子和分母取反。
normalize 剩餘部分很容易了解:分解最大公因數,分子分母都用它來除:if(den < 0) { num *= -1; den *= -1; }
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>
兩種文法的效果幾乎完全一樣,但如使用引号,C+編譯器會首先查找目前目錄,其次才會查找标準include檔案目錄(通常由作業系統的環境變量或環境設定決定)。取決于C+編譯器的版本,庫檔案和項目檔案或許都能使用引号文法。但慣例是用尖括号開啟标準庫的功能,本書将沿用該做法。#include "Fraction.h"
練習
練習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
相比之下,兩個分數的乘法運算就要簡單得多。
- 将新分數的分子設為 A * c
- 将新分數的分母設為 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;}
- 類必須先聲明再使用。相反,函數定義可放到程式中的任何地方(甚至能放到一個單獨的子產品中),但必須放在類聲明後面。
- 如函數傳回類型是類,就必須傳回該類的對象,可在函數定義中先聲明該類的一個對象(作為一個局部變量),并在最後傳回它。