電腦遊戲製作開發設計論壇 首頁 電腦遊戲製作開發設計論壇
任何可以在PC上跑的遊戲都可以討論,主要以遊戲之製作開發為主軸,希望讓台灣的遊戲人有個討論、交流、教學、經驗傳承的園地
 
 常見問題常見問題   搜尋搜尋   會員列表會員列表   會員群組會員群組   會員註冊會員註冊 
 個人資料個人資料   登入檢查您的私人訊息登入檢查您的私人訊息   登入登入 

Google
[C++][10]函式、變數範疇和常態變數

 
發表新主題   回覆主題    電腦遊戲製作開發設計論壇 首頁 -> 遊戲程式初級班:語法及基礎概念
上一篇主題 :: 下一篇主題  
發表人 內容
yag
Site Admin


註冊時間: 2007-05-02
文章: 688

2673.35 果凍幣

發表發表於: 2007-5-24, PM 1:55 星期四    文章主題: [C++][10]函式、變數範疇和常態變數 引言回覆

[2]C++入門中,我們有簡單提到過函式(或稱函數)了

一個程式中,最基本的函數就是main,我們可以把所有的東西都寫在main裡面,但是這樣會造成閱讀的困難,以及重複的片段程式碼過多

因此,我們可以將某些常常用到的重複性程式碼或者可成為一完整功能的程式碼,寫成另一個函數,然後藉由呼叫的方式來使用它,我們直接來看以下範例:
代碼:
//**************************************************************************
// 簡易文字RPG第一版
// 2007年6月20日 下午 04:30
// by yag
//**************************************************************************
#include <iostream>
#include <ctime>
#include <string>   // 為了使用getline()函式

using namespace std;

const int XMAX = 2;      // 遊戲世界的寬度(場景數目)
const int YMAX = 2;      // 遊戲世界的長度(場景數目)

// 初始化遊戲世界的各場景說明以及給予隨機函數一個種子
void Init( string MapDesc[][YMAX + 1] );
// 讀取輸入並處理,第三參數為遇到金幣時金幣的數量(預設為0)
void InputCmd( int &X, int &Y, short money = 0 );
// 顯示目前所在場景的說明、此場景之出口以及主角身上金幣數量
void Desc( const string MapDesc[XMAX + 1][YMAX + 1], int X, int Y );
// 決定進入新場景時是遇到金幣還是丁丁,傳回值為金幣的數量(遇到丁丁時是0)
short CoinOrDingDing();
void fight();   // 跟丁丁戰鬥的處理
void flee();   // 遇到丁丁時選擇溜走的處理
void drop();   // 遇到丁丁時將身上的金幣全部交出的處理

long long MyMoney = 0;      // 主角身上的金幣數量,一開始是 0 枚
short status = 0;   // 主角在場景中的狀況。 0:無事 1:發現金幣 2:遇到丁丁 99:遊戲結束

int main()
{
   string MapDesc[XMAX + 1][YMAX + 1];      // 儲存遊戲世界各場景的說明,預設最左下角為(0, 0)
   int X = 1, Y = 1;   // 主角所在位置,預設為(1, 1)

   Init( MapDesc );
   Desc( MapDesc, X, Y );
   InputCmd( X, Y );      // 因為主角剛出現時要預設為無事狀態,所以先在迴圈外呼叫一次

   while( status != 99 )   // 遊戲未結束前
   {
      Desc( MapDesc, X, Y );      // 顯示場景說明
      InputCmd( X, Y, CoinOrDingDing() );   // 參數中的CoinOrDingDing()會先執行,顯示出特殊情況說明
                                 // 然後InputCmd會等待接收輸入
   }

   return 0;
}

// 各場景說明請在此修改,預設最左下角為世界座標的起點,往右則增加X座標,往上則增加Y座標
void Init( string MapDesc[][YMAX + 1] )
{
   MapDesc[0][2] = "這裡是左上角的房間。";
   MapDesc[1][2] = "這裡是正上方的房間。";
   MapDesc[2][2] = "這裡是右上角的房間。";
   MapDesc[0][1] = "這裡是左邊的房間。";
   MapDesc[1][1] = "這裡是中間的房間。";
   MapDesc[2][1] = "這裡是右邊的房間。";
   MapDesc[0][0] = "這裡是左下角的房間。";
   MapDesc[1][0] = "這裡是正下方的房間。";
   MapDesc[2][0] = "這裡是右下角的房間。";

   srand( (unsigned) time( NULL ) );
}

void Desc( const string MapDesc[XMAX + 1][YMAX + 1], int X, int Y )      // 場景說明
{
   cout << MapDesc[X][Y].c_str() << endl;   // c_str()會將string類別裡存的字串變成傳統字串

   // 以下依地圖邊界判斷有何出口*************************
   cout << "此地的出口有( ";
   if( X < XMAX )
      cout << "e:東方 ";
   if( X > 0 )
      cout << "w:西方 ";
   if( Y > 0 )
      cout << "s:南方 ";
   if( Y < YMAX )
      cout << "n:北方 ";
   cout << ")" << endl;
   // ***************************************************

   cout << "你目前身上有 " << MyMoney << " 枚金幣。" << endl << endl;
}

// 第三參數不可寫為short money = 0,因為函數原型中已有定義過了,會造成重複定義
void InputCmd( int &X, int &Y, short money )
{
   string Move;
   bool Flag = true;   // 輸入迴圈判斷,只有當移動了場景,才會跳出輸入迴圈
                  // 在同一個場景內的fight、flee、drop、get coins都會導致迴圈持續

   while( Flag )
   {
      cout << "接下來該做什麼好呢?" << endl;
      getline( cin, Move );   // 要用string類別從cin中讀取字串就要使用getline()函式
                        // 要使用getline()函式就必須#include <string>

      Flag = false;   // 預設輸入過一次後就會離開迴圈

      // 以下的if跟一堆else if中,雖然都是以Move在判斷,但因Move的值無法轉型為單一整數型態,
      // 所以無法使用switch來做判斷
      if( Move == "e" )   // 往東走
      {
         if( X == XMAX && status != 2 )   // 判斷邊界以及是否戰鬥中
         {
            cout << endl;
            cout << "已到了盡頭!" << endl;
            Flag = true;   // 若是輸入不符合離開場景的條件,則將繼續迴圈
         }
         else if( status == 2 )   // 戰鬥中不可離開場景
         {
            cout << endl;
            cout << "丁丁擋住了你的路!" << endl;
            Flag = true;
         }
         else
            X++;   // 向右移動一格
      }
      else if( Move == "w" )   // 往西走
      {
         if( X == 0 && status != 2 )
         {
            cout << endl;
            cout << "已到了盡頭!" << endl;
            Flag = true;
         }
         else if( status == 2 )
         {
            cout << endl;
            cout << "丁丁擋住了你的路!" << endl;
            Flag = true;
         }
         else
            X--;
      }
      else if( Move == "s" )   // 往南走
      {
         if( Y == 0 && status != 2 )
         {
            cout << endl;
            cout << "已到了盡頭!" << endl;
            Flag = true;
         }
         else if( status == 2 )
         {
            cout << endl;
            cout << "丁丁擋住了你的路!" << endl;
            Flag = true;
         }
         else
            Y--;
      }
      else if( Move == "n" )   // 往北走
      {
         if( Y == YMAX && status != 2 )
         {
            cout << endl;
            cout << "已到了盡頭!" << endl;
            Flag = true;
         }
         else if( status == 2 )
         {
            cout << endl;
            cout << "丁丁擋住了你的路!" << endl;
            Flag = true;
         }
         else
            Y++;
      }
      else if( Move == "get coins" )   // 撿錢
      {
         if( status == 1 )   // 如果地上有金幣
         {
            MyMoney += money;
            cout << endl;
            cout << "身上金幣共有 " << MyMoney << " 枚" << endl;
            status = 0;      // 撿起來了記得把狀態改回來
         }
         else
         {
            cout << "地上並沒有任何東西。" << endl;
         }
         
         Flag = true;   // 因為撿錢並沒有離開場景,所以繼續迴圈
      }
      else if( Move == "fight" )   // 戰鬥
      {
         if( status == 2 )   // 丁丁出現才能戰鬥
            fight();
         else
            cout << "你想要跟誰戰鬥?" << endl;

         if( status != 99 )   // 如果戰鬥後沒有死亡,迴圈繼續,否則遊戲結束,離開迴圈
            Flag = true;
      }
      else if( Move == "flee" )   // 逃跑
      {
         if( status == 2 )   // 丁丁出現才能逃跑
            flee();
         else
            cout << "房間裡有小強嗎?跑那麼快做什麼?" << endl;

         Flag = true;   // 無論逃跑成功與否,迴圈都繼續
      }
      else if( Move == "drop" )   // 將金幣交出
      {
         if( status == 2 )   // 丁丁出現才能輸入此指令
            drop();
         else
            cout << "你傻了嗎?沒事把金幣丟到地上做什麼?" << endl;

         if( status != 99 )   // 如果將金幣交出後沒有死亡,迴圈繼續,否則遊戲結束,離開迴圈
            Flag = true;
      }
      else if( Move == "quit" )   // 離開遊戲
         status = 99;   // 將狀態改成遊戲結束
      else   // 只有以上是合法指令,其餘都顯示錯誤訊息並繼續迴圈
      {
         cout << "錯誤的輸入!" << endl;
         Flag = true;
      }
   }

   system( "cls" );   // 唯有離開了場景才會將螢幕清空
}

short CoinOrDingDing()   // 隨機產生突發事件
{
   short money = 0;   // 預設為0,因為此值會回傳,而且除了發現金幣外不會有更改的機會,所以務必給予初始值

   status = rand() % 3;   // 無事、發現金幣、丁丁出現的機率各3分之1

   switch( status )
   {
   case 0:      // 無事
      break;
   case 1:      // 發現金幣
      money = rand() % 500 + 1;   // 金幣的數量從1到500
      // 提示撿錢指令
      cout << "在地上發現了金幣 " << money << " 枚,要撿(get coins)嗎?" << endl << endl;
      break;
   case 2:      // 丁丁出現
      cout << "丁丁!人才丁丁出現了!丁丁拿起雷射光刀向你發起攻擊!!!" << endl;
      cout << "丁丁:丁丁是丁丁!丁丁是棒棒棒!丁丁是丁丁!雷此渡了虧機霍!!" << endl;
      // 提示戰鬥中可選擇之指令
      cout << "你可以選擇戰鬥(fight)、逃走(flee)或將金幣全部交出(drop)。" << endl << endl;
      break;
   }

   return money;
}

void fight()
{
   short WinOrLose = rand() % 6;   // 有6種戰鬥的可能結果,其中3種是勝利,3種是失敗
   short money = rand() % 500 + 1;      // 殺死丁丁會隨機得到1~500枚金幣
   
   cout << "你向丁丁發起了攻擊!!" << endl;

   switch( WinOrLose )
   {
   case 0:      // 此為勝利情況1
      cout << "丁丁將雷射光刀向你扔過來!!" << endl;
      system( "pause" );
      cout << "你閃過了雷射光刀!!" << endl;
      system( "pause" );
      cout << "丁丁手無寸鐵!!你幹掉了丁丁!!" << endl;
      system( "pause" );
      cout << "你掠奪了丁丁的屍體,得到了 " << money << " 枚金幣。" << endl;
      MyMoney += money;
      cout << endl;
      cout << "身上金幣共有 " << MyMoney << " 枚" << endl;
      status = 0;      // 打贏了就將狀態改回無事
      break;
   case 1:      // 此為勝利情況2
      cout << "丁丁突發性腦殘!將雷射光刀送給了你!!" << endl;
      system( "pause" );
      cout << "你使用丁丁的雷射光刀幹掉了丁丁!!" << endl;
      system( "pause" );
      cout << "你掠奪了丁丁的屍體,得到了 " << money << " 枚金幣。" << endl;
      MyMoney += money;
      cout << endl;
      cout << "身上金幣共有 " << MyMoney << " 枚" << endl;
      status = 0;
      break;
   case 2:      // 此為勝利情況3
      cout << "丁丁舉起雷射光刀向你衝了過來!" << endl;
      system( "pause" );
      cout << "你輕巧地閃開了丁丁的攻擊!" << endl;
      system( "pause" );
      cout << "丁丁不小心踢到石頭跌倒了!!丁丁的雷射光刀刺中了自己!!" << endl;
      system( "pause" );
      cout << "丁丁死了!" << endl;
      system( "pause" );
      cout << "你掠奪了丁丁的屍體,得到了 " << money << " 枚金幣。" << endl;
      MyMoney += money;
      cout << endl;
      cout << "身上金幣共有 " << MyMoney << " 枚" << endl;
      status = 0;
      break;
   case 3:      // 此為失敗情況1
      cout << "丁丁將雷射光刀向你扔過來!!" << endl;
      system( "pause" );
      cout << "你被丁丁的雷射光刀射中了!!" << endl;
      system( "pause" );
      cout << "你掛了!Game Over!" << endl;
      system( "pause" );
      // 死了將金幣歸零,不過這行實際上不用,因為死了就會離開遊戲,那麼下次再開始時,金幣自然會重新設定為0
      MyMoney = 0;
      status = 99;   // 死亡則將遊戲狀態改為遊戲結束
      break;
   case 4:      // 此為失敗情況2
      cout << "丁丁突發性腦殘!將雷射光刀送給了你!!" << endl;
      system( "pause" );
      cout << "你舉起丁丁的雷射光刀往丁丁頭上砍去!!" << endl;
      system( "pause" );
      cout << "丁丁空手入白刃!!將雷射光刀搶了回去!!" << endl;
      system( "pause" );
      cout << "你手無寸鐵,被丁丁用雷射光刀砍死了!" << endl;
      system( "pause" );
      cout << "你掛了!Game Over!" << endl;
      system( "pause" );
      MyMoney = 0;
      status = 99;
      break;
   case 5:      // 此為失敗情況3
      cout << "丁丁舉起雷射光刀向你衝了過來!" << endl;
      system( "pause" );
      cout << "丁丁不小心踢到石頭跌倒了!!手中雷射光刀飛射而出!" << endl;
      system( "pause" );
      cout << "你被丁丁甩出來的雷射光刀刺中了!!" << endl;
      system( "pause" );
      cout << "你掛了!Game Over!" << endl;
      system( "pause" );
      MyMoney = 0;
      status = 99;
      break;
   }
}

void flee()
{
   int SuccessOrFail = rand() % 2;      // 逃跑成功與失敗機率各2分之1

   switch( SuccessOrFail )
   {
   case 0:
      cout << "你成功地逃跑了!" << endl;
      status = 0;      // 成功跑掉則將狀態改回無事
      break;
   case 1:      // 逃跑失敗則狀態不變,依然在戰鬥中,並且會失去身上10分之1的金幣
      cout << "你逃跑失敗了!忙亂中掉了 " << MyMoney / 10 << " 枚金幣!" << endl;
      MyMoney -= MyMoney / 10;
      cout << "身上剩下 " << MyMoney << " 枚金幣" << endl;
      break;
   }
}

void drop()
{
   int LiveOrDie = rand() % 10;   // 將金幣全部交出後依然有10分之1的機率被殺死

   cout << "你將身上的金幣全部交出!" << endl;
   MyMoney = 0;

   if( LiveOrDie == 0 )   // 不一定要是0,0~9中任選一個數字都行
   {
      cout << "丁丁對你的總財產感到不滿意,隨手用雷射光刀砍死了你!!" << endl;
      system( "pause" );
      cout << "你掛了!Game Over!" << endl;
      system( "pause" );
      status = 99;   // 死了就記得改成遊戲結束狀態
   }
   else
   {
      cout << "丁丁心情不錯放了你一馬。" << endl;
      system( "pause" );
      status = 0;      // 沒事就記得把狀態改回0
   }
}

這是一個仿照Mud遊戲做的範例,目前主角唯一的目標就只有盡量累積金幣數量,而且還沒有存檔功能,在之後的教學中,我會持續修改此範例,讓其越來越趨近於RPG。

那麼我們來看程式碼,首先我們有了一個沒看過的#include <ctime>,實際上它跟#include <time.h>是一樣的意思,都是為了使用time()函式所以才include的,而這兩者不同的地方在於,有在<>中標出.h會使編譯器在系統預設資料夾中找尋同樣檔名的檔案;而沒有標出.h的,代表這是ANSI C++標準的標頭檔,編譯器會自動找尋「相對應」的檔案(也就是檔名可能會依編譯器不同而有所不同),在這裡來說的話,<ctime>應該就是會去找尋time.h檔,雖然VC++中確實有ctime.h這個檔案,但你可以試著去看看它的內容,就可以發現這並不是我們所要的檔案,其中並沒有time()函式的宣告,而如果你將此行改成#include <ctime.h>,編譯器會告訴你找不到這個檔案,因為它並不是放在系統預設資料夾中。

再來我們看到一個新的標準標頭檔,<string>,這是為了要使用getline函式以從標準輸入串流cin中讀取資料到string類別裡。我們在InputCmd()函式裡會看到它的用法,因為cin的萃取運算子(>>)並沒有實作string類別的寫入,所以我們才需要使用getline,而不是以cin >> string;來讀取輸入。
接著是兩個常態變數,所謂的常態變數就是在變數定義時加上const關鍵字,而且加上了const也就代表這一定是一個有給予變數初值的變數定義(應該還記得定義宣告的差別吧?),因為加上了const,代表此變數在往後的程式碼中都不能改變其值,所以要是不給予其初值就會變得沒有意義。
一般來說,我們會將常態變數的名稱寫成全部大寫,而常態變數是用來代表某個具有特別意義的數字,像是如果我們在程式中要用到圓周率,我們可能就會寫成:
代碼:
const float PI = 3.14;

這麼一來在程式中要用到圓周率數值時,我們就會以PI來代表,而不是以3.14這個數字來代表,這可以避免所謂的神奇數字或魔法數字(magic number)的出現。我們常常會在寫完程式碼後,過一段時間再回顧時,發現其中有些莫名其妙出現的數字不知道代表什麼意思,這就是所謂的神奇數字,而避免這種情況的方法,就是將這個數字給予名字上的意義,也就是常態變數的名稱,而且這種作法的另一個優點是,當你需要更改其數值時,只需要更新常態變數的定義就行了,不需要一一修改程式碼中所有此數字有出現過的地方。

像是在這個範例中,我用了數次的XMAX跟YMAX,尤其是拿它來宣告遊戲場景說明陣列的大小,這麼一來,當我要擴充這個世界的大小時,我只需要更改這兩個變數的定義初始值即可,而不用一一更改陣列的大小、邊界的判定等等。

再來是我們在此程式中所用到的所有函數的原型宣告。函數原型就是指此函數的回傳值型態函數名稱以及其所有的參數型態。我們在程式碼中要使用函數之前,必定得要先宣告其函數原型,這樣編譯器才能夠知道要去哪裡找對應的函數程式碼內容。要注意的是,函數原型宣告最後面必須加上「;」當成結尾

在此我們必須說一下函數的參數傳遞有三種方法,分別是傳值傳址傳參考
首先請看Desc函數的第二個以及第三個參數,X跟Y,還有InputCmd函數的第三個參數,money,此三者皆是傳值的例子,傳值也就是說,我們將引數(*註1)丟給函式後,函式會將引數的值複製一份,而參數會在記憶體中取得另一個空間,程式再將複製好的引數的值指定給參數。因此在此函數中,無論怎麼改變其參數之值,對引數的值都是不會有影響的,換句話說,如果我在Desc中寫了個X++;,實際上在main裡面的X並不會有任何的改變。

(*註1)在函數呼叫中,傳給函數的值我們叫做引數,而在函數中相對應於引數的變數,我們將其稱為參數。這是兩個不同的東西,我們必須分清楚,舉個例子來說,在main中呼叫Desc函式時,我們傳給Desc的MapDesc、X跟Y都是引數,它們都是main當中的變數,而在Desc函式的定義部份,相對應於這三個引數的同名變數MapDesc、X跟Y則稱為參數,這三個參數都是Desc函式當中的變數,雖然它們跟main中的變數有相同的名稱,但卻分屬於不同的區塊,也就是說他們有不同的範疇(scope),因此這實際上是六個不同的變數,不可以混為一談。

第二種傳遞的方法是為傳址,也就是傳遞記憶中的位址,像是Init中唯一的參數以及Desc中的第一個參數都是以傳址在傳遞,這是因為這兩個參數都是陣列的型態,而C++中,為了避免花過多的時間一一複製陣列的每個元素之值,所以傳陣列時一律以位址傳遞,換句話說,Init中的MapDesc跟main中的MapDesc雖然是不同的兩個變數,但是它們都存放了同一個位址,因此它們所存取的都是同一個空間中的值,這麼一來,在Init中對MapDesc的值進行變動,在main中的MapDesc的值也就會有著同樣的改變。(這部份涉及指標的理論,目前看起來可能比較不懂,不過這在之後的教學中會再次詳細講述,不用太過在意,只需要知道傳陣列時,在函數中更動其值會造成引數陣列的值也跟著變動就行了。)

第三種方式叫做傳參考,例子就是InputCmd中的第一個跟第二個參數,有看到參數名稱前多了一個「&」符號嗎?這就是代表其為參考值變數,什麼意思呢?這就像幫變數取了個暱稱是一樣的意思,也就是在InputCmd中的這兩個參數,跟main中的那兩個引數,代表的都是同一個記憶體空間,我們並沒有額外取得一個空間複製引數的值過來,而是直接將引數的空間拿來用,所以這四個變數實際上只是兩個變數而已,就好像曹操又叫曹孟德,兩個名字,但指的是同一個人。
因此大家應該猜得到,在函數中對傳參考的參數做值的變更,當然引數那邊也會有同樣的影響,因為它們實際上就是同一個變數。

接著是兩個全域變數(global variable):MyMoney跟status。這就要講到變數範疇了,什麼是範疇(scope)?就是其所能被使用的地方。像這兩個全域變數,不論是在main中還是在其他所有我們宣告的函式中,都可以自由自在地使用它們,不用搞什麼傳來傳去的引數參數,在任何一個地方使用它們的名稱都是代表這個變數。那什麼變數不是全域變數呢?像是main裡面的MapDesc、X跟Y就不是,它們是所謂的區域變數,只能夠在main中被使用,一但在其他函式中想要使用它們,就必須把它們當成引數傳進函數裡。
那麼為什麼我們不要把所有變數都宣告成全域的呢?不是很方便嗎?可以隨便拿來用,也不用搞什麼傳來傳去的。在小型程式裡面的確是可以這樣,不過當你程式越寫越大時,你就會發現:
第一,全域變數在任何地方都可以自由的使用跟改變其值,這本來是優點的特徵卻反而會變成其缺點,因為當你函式變成數十個、數百個之後,你很難發現你是否在某個函式中不小心改變了其值,而造成了程式結果的錯誤,這會極大地增加程式除錯(Debug)的困難性,你必須把所有程式從頭到尾檢查,而使用區域變數時,你可以很確信這些變數在函數之外不會被使用到,所以可以縮小檢查的範圍。
第二,全域變數從程式開始直到程式結束都會存在,而區域變數只要程式執行點離開了其被宣告的函式,它就會自動"死亡",其所佔用的記憶體也會被系統回收。沒錯,就是記憶體全域變數會全程佔用住記憶體,對於一個只在某一個函數或兩、三個函數中使用到的變數來說,將其宣告成全域的,只會讓你程式佔用記憶體的量變大,這在寫遊戲時是很要不得的,成千上萬個變數全宣告成全域的,那想玩你這個遊戲的玩家沒有個1、2G的記憶體大概就別想執行了。
因此為了除錯容易以及節省記憶體空間,能不宣告成全域的就盡量不要這麼做。

細心的人應該有發現,MyMoney全域變數的型態有點怪,long long,這是什麼呢?這也是一個整數型態,只是它是用8 bytes在儲存,所以其值域是從9223372036854775807 ~ -9223372036854775808,夠大了吧?保證你金幣數量不會爆掉...

說到這,麻煩請再回頭看一下Desc函式的第一個參數,我剛忘了講,其前面有加了一個const關鍵字,我們已經了解這是宣告成常態變數的意思,但在參數這邊使用代表了什麼呢?代表我們在函式中完全不會更動到引數的值
我們前面說過這是傳址的參數,加上const可以避免我們無意間更動了我們不想更動的值,對於使用這個函數的人來說,也可以了解到將引數傳入此函數的第一個參數,可以不用擔心其值被變更了。這在我們將此函數提供給其他程式設計師使用時是很方便且重要的,在往後為了能夠重複利用寫過的程式碼,我們會將一些常用到的函式寫成函式庫,就好像我們現在利用標準函式庫中的cin、cout、pow()、time()、rand()、srand()等函式及物件一樣,如果在函式原型處有標上了const,我們自然就可以放心把變數傳進去而不用擔心值的變更,所以養成良好習慣是很重要的,不過嘛...傳值的參數就免標示了,因為傳值的參數本來就不管怎麼改也不會改到引數的。

講了一堆終於到了我們的main函式,是不是出乎意料之外的短呢?跟之前的範例比起來,這個main的結構應該更能夠方便我們理解整個程式碼,這就是使用函數的功勞。
我們一開始先初始化場景說明(Init),然後顯示場景說明(Desc),再要求玩家輸入(InputCmd),然後開始進入遊戲迴圈,迴圈中一樣是先顯示場景說明(Desc)再要求玩家輸入(InputCmd),那為什麼迴圈外還要先顯示說明跟要求輸入一次呢?仔細看看,應該可以發現在要求輸入地方的不同,是的,最早呼叫的InputCmd()少掉了第三個參數的傳遞

為什麼可以這樣做呢?這就是所謂的預設值的功勞了,請看回InputCmd()的函式宣告處,有看到第三個參數那我們事先設定了short money = 0嗎?這就是預設值的設定法,只要在函數原型宣告的地方設定好就行了,而實際在函數定義的地方(下面會看到),我們是不行再次設定money = 0的唷,這會造成重複定義的情況。
當然也是不行宣告的地方寫short money而定義的地方寫short money = 0,為什麼呢?因為我們在定義之前就先以有預設值的型態使用了InputCmd(),除非我們將InputCmd()的定義移到int main()的上方,否則預設值的設定就必須在原型宣告的地方事先設好。

另外還有個必須注意的地方,那就是money必須要是最後一個參數才行,當然放在第一個參數也是合法的語法,但是這麼一來我們就不能夠在呼叫InputCmd()時省略掉它,因為參數傳遞時,引數對參數是一個對一個以位置來傳進來的,所以若是有10幾個參數,而其中隨便安插幾個有預設值的參數,那麼呼叫時該怎麼分辨哪個引數要傳給哪個參數呢?
代碼:
// 以下函數原型有14個參數,其中摻有5個有預設值
Example( int a, int b, int c = 0, int d, int e = 5, int f, int g = 3, int h, int i, int j = 2, int k = 1, int L, int m, int n );
// 下面是函數呼叫,傳了12個引數進來,你分辨得出來是哪兩個參數使用了預設值嗎?
Example( o, p, q, r, s, t, u, v, w, x, y, z );

要知道,參數雖然設了預設值,並不代表我們就再也不能傳引數給它,所以你到底要跳過不跳過,編譯器又該如何得知?或許有人說可以用逗點「,」來把不傳引數的空下來:
代碼:
Exam2( a, , b, , c, d, , e, , , f, g );

不過這樣的語法如果接受了,那麼編譯器就會失去檢查引數遺漏的功能,而且在編寫程式碼時很可能會漏看或什麼的引起不必要的混亂,因此,C++中規定,若要省略參數傳遞,那麼就必須把參數放在最後面。可是並不是放在後面就可以隨便亂省略唷,像這樣:
代碼:
// 最後三個參數有預設值
Exam3( int a, int b, int c = 5, int d = 4, int e = 3 );
// 呼叫時,q肯定是傳給c,因為不接受用逗號分開的方式,
// 所以我們要嘛省略e、要嘛省略d和e、要嘛省略cde,
// 沒有辦法光省略c而傳引數給de,當然也沒有辦法光省略d而傳引數給ce
Exam3( o, p, q );
// 因此,若要傳引數給d,卻又想要c維持5這個值,那麼就要像這樣:
Exam3( o, p, 5, r );


也因為這樣,所以我們才會在迴圈外事先呼叫一次Desc()跟InputCmd(),當然啦,這兩個也可以當成Init()的內容放在Init()的定義裡去,這樣main()看起來就會更簡潔一些,不過我個人是覺得放在main()裡面比較好,才不會讓Init()的作用亂掉。

接下來就進入了Init()的定義了,我們可以看到參數的第一個[]中沒有註明大小,這是因為編譯器會將當引數的陣列的第一維改成指標型態,不需要註明大小,不過後面那個[YMAX + 1]就是必不可省略的。指標部份我們以後的教學中會講到,先記住第一維可以省略大小就行了。不過這麼一來,我們在傳陣列時勢必要把第一維的大小當成另一個參數傳進來,不然我們就不知道陣列到底有多大了,但因為我把XMAX設成了全域的常態變數的關係,我們可以很容易的取用到XMAX,所以也就省下了傳第一維大小的工夫。

再下面是Desc()的定義,這裡要注意一下,我們在上面函數原型宣告時,Desc()是放在InputCmd()的下面宣告的,但這邊卻在InputCmd()之前定義,這代表什麼呢?代表了定義的順序不需要按照宣告的順序,事實上隨便要放在哪定義都行,當然照順序的話比較容易讓人有種循序的美感,在查詢定義時也比較容易知道函數定義在哪裡(像是如果有循序,那看到InputCmd()時我們就會想說,再往下拉就可以看到Desc()的定義,而不會拉來拉去到處找)。

Desc()的第一個參數跟Init()的參數是一樣的,不過這邊有把第一維的大小標出來,而實際上標不標都一樣,因為它會轉成指標,我只是標一下讓大家知道要不要標都可以,不過標了應該會比較好,可以讓人一目瞭然大小是多少。

我們接著可以在InputCmd()中看到getline()的用法,非常簡單,只要把cin當成第一個參數,把要讀取字串的string變數當成第二個參數,就可以讀入使用者的輸入。string是個很好用的字串類別,比起C中的傳統字串(char*)要好用得多,這個我之後會再寫篇教學講解兩者之不同,現在只要先了解在此範例中string的用法即可。

再往下看是一堆判斷玩家輸入的指令的地方,我想我註解已經頗清楚了,這裡並沒有什麼太難的地方,就不多講解了。

然後嘛,我突然發現我忘了講解void,哈哈,這是代表無回傳值的意思,函數原型第一項就是回傳值型態,這在上面講過,我們在函數中可以使用return關鍵字來將「一個」值回傳,並沒有辦法可以讓函數回傳兩個以上的值,不過如上面所介紹的,我們可以用傳址傳參考的方式讓引數的值被函數改變,就某種層面上來說,這也可以算是回傳值了。

整個範例程式中,唯一有回傳值的函數就是接下來的CoinOrDingDing(),它會回傳一個short型態的值,也就是因為它有回傳這個值,我們才能在main()中將它寫成這種型式:
代碼:
InputCmd( X, Y, CoinOrDingDing() );

在這裡,CoinOrDingDing()會先執行,然後回傳一個short的值,我們再將這個值當成引數傳給InputCmd的第三個參數,是不是很有趣的寫法呢?會有一種環環相扣的感覺,呵呵。

至於接下去的函式嘛,我想應該沒有什麼難得倒你們的地方了,而且我們可以發現,最後這三個函式都沒有在main中出現過,它們主要是在InputCmd()中被呼叫,所以並不是所有的函式都必須在main()裡面被使用不可

最後有個可以提一下的地方,請看fight()中switch的敘述,仔細看你會發現case 0、1、2的最後面幾行都一樣,而case 3、4、5的最後面幾行也都一樣,實際上我們可以把這些一樣的地方抽出來再寫個函式,那麼程式碼就會縮減很多行,也或者我們可以把switch分成兩個,後面那個switch可以像這樣:
代碼:
   switch( WinOrLose )
   {
   case 0:
   case 1:
   case 2:
      system( "pause" );
      cout << "你掠奪了丁丁的屍體,得到了 " << money << " 枚金幣。" << endl;
      MyMoney += money;
      cout << endl;
      cout << "身上金幣共有 " << MyMoney << " 枚" << endl;
      status = 0;      // 打贏了就將狀態改回無事
      break;
   case 3:
   case 4:
   case 5:
      system( "pause" );
      cout << "你掛了!Game Over!" << endl;
      system( "pause" );
      MyMoney = 0;
      status = 99;
      break;
   }

那麼前面一個switch裡那些重複的部份就都可以刪掉了。不過嘛,這種作法會讓人在看第一個switch時有種到一半斷掉的感覺,寫成函式的作法或許會比較好一點,但因為重複的行數說起來不多,再寫成函式有點畫蛇添足之感,所以我最後還是保留了目前這種寫法。

程式碼下載:main.cpp
執行檔下載:example10-1.exe


yag 在 2007-8-6, PM 1:54 星期一 作了第 6 次修改
回頂端
檢視會員個人資料 發送私人訊息 發送電子郵件
yag
Site Admin


註冊時間: 2007-05-02
文章: 688

2673.35 果凍幣

發表發表於: 2007-6-21, PM 4:50 星期四    文章主題: 引言回覆

呼...終於寫完了,這篇被我一直往後延得太久了,所以在這裡推一下,免得沒人知道我補完了@_@"

話說這篇總共花了我8小時以上的時間(包含寫程式的2小時),實在是有夠久的,從前天寫程式,昨天寫文章,一直到今天才搞定。

這次在最後面還附加了程式碼跟執行檔的下載,希望能讓各位方便一點,不過還請記得版權所有,轉貼或引用時請記得標明原作者出處出處尤其重要,具有幫此論壇打廣告的作用... Laughing Laughing Laughing

那麼,這次也來出些習題吧,首先是初級的:遊戲中每次要撿金幣都要輸入get coins,實在是頗麻煩,請將其改成輸入get coins也行,光打一個g也可以吧。

再來嘛是中級的:遊戲中人物掛掉後程式就會結束,想接著玩都要不斷重複執行程式很麻煩,請加寫個掛掉後詢問是否重新開始遊戲的功能吧。

最後是高級的:在之後的發展遊戲過程中,不可避免地可以想像得到,get會作為一個常常被使用的指令,像是打死敵人後可能就要使用get來取得其身上的戰利品,那麼,雖然目前只有金幣可以get,也請將get獨立寫成一個函式吧!此函式不需要回傳值,但要有一個參數,此參數是玩家打算撿起來的東西,以get coins為例,coins就是要傳入get函式的參數(string型態),當然了,請兼顧初級習題中的規則,光打一個g也是要可以撿起金幣的,給個提示就是要善用這次講解到的參數預設值

那麼,請加油吧。
回頂端
檢視會員個人資料 發送私人訊息 發送電子郵件
satanupup
喜歡上這裡的冒險者


註冊時間: 2007-05-29
文章: 80

68.10 果凍幣

發表發表於: 2007-6-26, PM 3:19 星期二    文章主題: 引言回覆

等你介紹完C++ 你差不多就可以
用這些文章出本書了...
把你的程式碼也加進去..
我對你這篇很有興趣耶
ㄧ起把它加強吧
^^
先寫習題去...
初級把get coins按取代成g就好了
中級
string s;
cout<<"是否重新開始遊戲(y/n)"<<endl;
getline(cin, s);
system("pause");
if( s == "y" || s == "Y"){
status = 0;
return main();
}
if( s == "n" || s == "N"){
return 0;
}
回頂端
檢視會員個人資料 發送私人訊息
yag
Site Admin


註冊時間: 2007-05-02
文章: 688

2673.35 果凍幣

發表發表於: 2007-6-27, AM 1:41 星期三    文章主題: 引言回覆

satanupup 寫到:
等你介紹完C++ 你差不多就可以
用這些文章出本書了...
把你的程式碼也加進去..

呵呵,我也有這個意思,等我寫完後再整理一下,可能會去找找看有沒有出版社願意幫忙出版吧。

satanupup 寫到:
我對你這篇很有興趣耶
ㄧ起把它加強吧
^^
先寫習題去...
初級把get coins按取代成g就好了

嗯,一起加油吧。^^
初級的部份,因為不管是get coins還是g都要可以撿,所以要用 || 來實作才對喔,如果拿g取代了get coins,那打get coins就沒用了。

satanupup 寫到:
中級
string s;
cout<<"是否重新開始遊戲(y/n)"<<endl;
getline(cin, s);
system("pause");
if( s == "y" || s == "Y"){
status = 0;
return main();
}
if( s == "n" || s == "N"){
return 0;
}

拿main()做遞迴還滿有創意的,不過這樣做會比較耗資源,所以改成將main()中的一部份用do...while包起來會比較好,只是要多加一道手續--把X跟Y改回(1, 1)。
你這種寫法還會有個bug,就是當使用者輸入"y"、"Y"、"n"、"N"此四者之外的任何字元或字串時,main()就沒了回傳值,所以簡單一點的話,把下面那個if改成else;複雜一點的話,改成if...else if...else的型態,最後一個else提示使用者正確的輸入y或n,然後把整段包含輸入的地方用迴圈包起來,這樣程式才有正確的結束流程。
回頂端
檢視會員個人資料 發送私人訊息 發送電子郵件
hello147258369
偶而上來逛逛的過客


註冊時間: 2009-08-01
文章: 6

40.05 果凍幣

發表發表於: 2009-8-21, PM 2:28 星期五    文章主題: 引言回覆

請問大大..判斷輸入是否合法...數字是用isdigit,請問英文字是用什麼??
回頂端
檢視會員個人資料 發送私人訊息
yag
Site Admin


註冊時間: 2007-05-02
文章: 688

2673.35 果凍幣

發表發表於: 2009-8-21, PM 8:19 星期五    文章主題: 引言回覆

hello147258369 寫到:
請問大大..判斷輸入是否合法...數字是用isdigit,請問英文字是用什麼??

isalpha
http://www.cppreference.com/wiki/c/string/isalpha
回頂端
檢視會員個人資料 發送私人訊息 發送電子郵件
Alchemist
偶而上來逛逛的過客


註冊時間: 2010-06-10
文章: 7

130.57 果凍幣

發表發表於: 2010-6-15, AM 12:41 星期二    文章主題: 哇- - 引言回覆

原來還有這種遊戲唷˙˙

我還以為那看起來很古老的東西只應用在BBS上哩- -

應該是我出生前的東西吧- -?
回頂端
檢視會員個人資料 發送私人訊息
yag
Site Admin


註冊時間: 2007-05-02
文章: 688

2673.35 果凍幣

發表發表於: 2010-6-15, AM 10:15 星期二    文章主題: Re: 哇- - 引言回覆

Alchemist 寫到:
原來還有這種遊戲唷˙˙

我還以為那看起來很古老的東西只應用在BBS上哩- -

應該是我出生前的東西吧- -?

現在在BBS上也還有Mud存在喔
回頂端
檢視會員個人資料 發送私人訊息 發送電子郵件
Alchemist
偶而上來逛逛的過客


註冊時間: 2010-06-10
文章: 7

130.57 果凍幣

發表發表於: 2010-6-16, AM 1:00 星期三    文章主題: - - 引言回覆

嗯..

我有找到幾個- -

只是這種遊戲...

真的還有人在玩嗎?

感覺差不多該沒落了
回頂端
檢視會員個人資料 發送私人訊息
從之前的文章開始顯示:   
發表新主題   回覆主題    電腦遊戲製作開發設計論壇 首頁 -> 遊戲程式初級班:語法及基礎概念 所有的時間均為 台灣時間 (GMT + 8 小時)
1頁(共1頁)

 
前往:  
無法 在這個版面發表文章
無法 在這個版面回覆文章
無法 在這個版面編輯文章
無法 在這個版面刪除文章
無法 在這個版面進行投票
可以 在這個版面附加檔案
可以 在這個版面下載檔案


Powered by phpBB © 2001, 2005 phpBB Group
正體中文語系由 phpbb-tw 維護製作