CPP遊戲攻略01

前言

幾年前接觸到一款很好玩的RPG遊戲,叫作CPP。最近想着懷念一下,又不想乾巴巴地去玩。於是乎,我打算寫幾篇攻略,主要是記錄一下游戲中一些奇妙的點。遊戲的第一章是面向對象程序設計,其中又分爲基於對象(object-based)的關卡和麪向對象(object-oriented)的關卡,而基於對象的關卡中又有兩個BOSS,一個是無指針的類,另一個是有指針的類。今天就寫寫第一關基於對象的無指針的類吧。

基於對象的無指針類

首先介紹一個complex類,這個類是從標準庫中提出出來的,有着大佬們的氣息。source code

class complex {
public:
  complex (double r = 0, double i = 0) : re(r), im(i) {}
  complex& operator += (const complex&);
  complex& operator -= (const complex&);
  complex& operator *= (const complex&);
  complex& operator /= (const complex&);
  double real() const { return re; }
  double imag() const { return im; }
private:
  double re, im;

  friend complex& __doapl(complex *, const complex&);
  friend complex& __doami(complex *, const complex&);
  friend complex& __doaml(complex *, const complex&);
};

現在我們可以倒過來看,想想怎麼設計一個表示複數的類呢?

首先,複數有實部和虛部,那麼我們的類裏一定有兩個變量,我們先不考慮模版類,那麼就是:

private:
  double re, im;

想想如果不加上private可以嗎?當然可以了,只是這樣幹也太懶了,太隨意了,我好不容易弄出一個class,你在哪裏都可以直接訪問我的數據,還不如直接用回C的struct,怎麼能這麼幹呢!這不符合封裝的設計藝術,也不符合大佬的作風。private必須加!

接下來,一個類當然要有構造函數了。那麼幹脆直接不寫了,直接使用默認構造函數吧!其實也不是不行,不過讓人感覺很蠢,因爲你怎麼設值呢?當然也可以弄一些setter函數,不過也不夠優雅。我們的使用者應該可以換不同的姿勢來創建對象,比如:

complex c1;
complex c2(1);
complex c3(1,2);
complex *p = new complex();

這樣一來,我們的構造函數這樣設計:

public:
  complex (double r = 0, double i = 0) : re(r), im(i) {}

我們的給實參寫上默認值0,一個構造函數就可以應對上面4種寫法。不過,我們爲什麼使用: re(r), im(i)這樣奇怪的寫法?而不是在函數題內直接賦值,

{
  this->re = r;
  this->im = i;
}

這樣不是更清晰嗎,清晰的代碼不好嗎?其實也可以,不過這樣不夠逼格!你可知道變量的生死是怎樣的嗎?變量需要經歷兩個階段,首先初始化,其次賦值。而我們奇怪的初始化列表(initialization list)的寫法,就讓我們直接初始化就完事!效率UPUP!

有了成員變量,我們接下來應該考慮怎麼讓其他人來訪問我的數據呢?當讓是寫getter了!

public:
  double real() const { return re; }
  double imag() const { return im; }

等等,函數名後面那個const是什麼鬼!大佬們在寫這個函數的時候,考慮到這個函數不會修改本身的數據,以及使用者會這樣寫:

const complex c4(1,2);
cout << c4.real() << " " << c4.imag() << endl;

這樣一來,萬一我們把函數名之後的const刪掉了,編譯器會認爲你的函數可能會修改數據,因此就會報錯,而使用者就會對你破口大罵!千萬別這麼幹。。。

接下來,我們拿出中學數學課本,查了一下複數有四則運算,那麼使用者可以這麼用:

  cout << c1+c2 << endl;
  cout << c1-c2 << endl;
  cout << c1*c2 << endl;
  cout << c1 / 2 << endl;
  cout << (c1 += c2) << endl;
  cout << (c1 == c2) << endl;
  cout << (c1 != c2) << endl;
  cout << +c2 << endl;
  cout << -c2 << endl;
  cout << (c2 - 2) << endl;
  cout << (5 + c2) << endl;

聰明的你肯定立馬反應到,操作符重載唄!沒錯,我們接下來就來看看操作符重載裏有什麼可以 dig dig!

public:
  complex& operator += (const complex&);
  complex& operator -= (const complex&);
  complex& operator *= (const complex&);
  complex& operator /= (const complex&);

注意到,我們是在類裏面聲明的操作符重載,那麼可不可以在外面聲明呢?其實也可以的,但是大佬們爲什麼這樣寫?

inline complex&
complex::operator += (const complex& r)
{
  return __doapl (this, r);
}

inline complex&
__doapl (complex* ths, const complex& r)
{
  ths->re += r.re;
  ths->im += r.im;
  return *ths;
}

設計一個函數,我們必須考慮參數和返回值,我們可能會這樣使用操作符:

c1 += c2;

這時候應該有兩個參數,c1和c2。實際上編譯器處理到這裏,會這樣調用函數:

complex& complex::operator += (complex *this, const complex &r);

注意到,第一個參數this就是左邊的c1,第二個是右邊的c2,至於爲什麼使用const,我想你已經知道了。我們知道編譯器會這樣調用函數,但是我們寫的時候可不能這樣寫,否則會被罵哭。
接下來考慮返回值,爲什麼我們有時用引用,有時候不使用引用呢?其實使用引用還是爲了提高效率,就跟C語言裏面傳指針4個字節的速度一樣,引用的速度和傳指針一樣快。而使用引用和指針都需要考慮,返回值是不是局部變量(local object),如果不是,好的,使用引用吧!
這下我們知道了寫成類成員函數的原因,c1的this指針默認的傳入重載函數,c2的數據加到c1上,再返回c1自己,於是乎,使用者可以這樣來用:

c1 += c2 += c3;

你可能會對函數friend complex& __doapl(complex *, const complex&);感到奇怪,這是個朋友,不過是我們單方面宣稱的朋友。我們單方面認爲他是我們的朋友,於是乎我們把他當自己人,朋友可以自由訪問自己的私有數據。同樣地,同一個類創建出來的對象互相都是自己人,自己人都可以訪問自己的私有數據。

下面我們看一看聲明在類外部的函數:

inline complex
operator + (const complex& x, const complex& y)
{
  return complex (real (x) + real (y), imag (x) + imag (y));
}

這裏我們的返回值變了,變成了complex,不是引用了。

complex (real (x) + real (y), imag (x) + imag (y));

這裏我們函數裏使用了臨時對象,臨時對象的生命在下一行代碼就結束了。於是立即推出,不能使用引用,而直接創建一個臨時對象,然後返回臨時對象。注意到,臨時對象的生命在下一行代碼就結束了,返回值實際上就是一個臨時對象,用過了,就死了。

最最最後,我們還可以花裏胡哨地重載一下輸出:

ostream&
operator << (ostream& os, const complex& x)
{
  return os << '(' << real (x) << ',' << imag (x) << ')';
}

這裏有幾個問題,爲什麼這個函數定義在類外部呢?假如我們定義在類內部,那麼使用者就該這麼用:

c1 << cout;

emmmmmm...這一定會被噴死!那爲什麼我們的返回值不是void呢,返回void就行了啊?萬一使用者這樣使用:

cout << c1 << " love love";

第一個cout << c1返回值是void,編譯器報錯,GG。那麼第一個參數爲什麼不添加const呢?const ostream& os
當然不能這樣幹,因爲return os << '(' << real (x) << ',' << imag (x) << ')';已經修改了os的狀態。

小問題

這裏拋出一個問題,看看下面這個函數:

inline complex
operator + (const complex& x)
{
  return x;
}

這個函數的意思是輸出正的複數,比如:

cout << +c1;

你也許會有疑問,依照我們先看函數內是否使用了局部變量的原則,這裏的返回值是可以使用引用的,而且應該使用引用。嗯,看起來確實如此,這是編寫標準庫的大佬們寫出的代碼,他們也會犯錯,或者說也會寫出不是最優的代碼。當然了,這樣的寫法沒有錯,只是不是最優的寫法,標準庫並不是聖經,你可以去研究她,發現她的亮點和不足。

總結

設計類是最基本的技能,也是決定代碼效率的關鍵一環,以下是我的認爲設計類需要考慮的問題:

  1. 設計一個類,首先考慮有什麼數據
  2. 考慮數據的類型,是public,還是private,還是friend
  3. 構造函數:是否有默認參數;有沒有使用初始化列表
  4. 函數參數:首先考慮傳引用;要不要加const;使用者會不會創建一個const的對象!
  5. 函數返回:是不是適合傳引用?局部變量不適合傳引用!
  6. 函數重載:使用者以不同的方式進行操作,比如double+complex complex+double
  7. cout重載:應該放在類外面,不適合做成員函數,因爲沒有人會倒着寫
  8. this指針:調用成員函數都會隱藏的加上this指針,除了static
  9. static變量:必須在類的外面進行定義!類裏面只是聲明!聲明沒有內存!

第一篇攻略就先到這了,我們下次再見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章