2009年1月22日 星期四

建構式與解構式

預設建構式與解構式有許多的地雷,首先,如先前所說,C++在你沒有撰寫此兩個函式的時候,會自動生成,那麼因為建構式支援多載,預設解構式還會生成嗎? 答案是不會的,以下以範例說明:

class ClassHasDefaultConstruct

{

};

class ClassHasNoDefaultConstruct
{

public:

  ClassHasNoDefaultConstruct(int i){}
};

我們可以注意到第二個範例,有寫建構式但不是預設建構式(沒有任何參數的),此時C++就放棄幫你建立預設建構式了,可是有時候,我們在使用STL函式庫的時候,強迫要求你設計預設建構式,所以ClassHasNoDefaultConstruct這個類別是不能為STL容器所用。

這裡有個小伎倆,把有參數的建構式當成預設建構式用,利用預設參數的方式:
class ClassHasNoDefaultConstruct2
{
public:
  ClassHasNoDefaultConstruct2(int i=0){}
};

使用建構式有一些注意事項,如果你的類別支援繼承,在建構式中,不可呼叫虛擬函式,因為,物件建構的順序是:基底類別建構式完成後,然後才是衍生類別建構,所以在基底類別建構式中呼叫虛擬函式,永遠是基底類別的函式

其次養成習慣,最好在建構式中,給予所有變數初值,避免執行期間產生錯誤,因為一般而言,C++在除錯版的時候,會自動給定變數初值(0, NULL),而Release版不會,變數值是看當時配置的記憶體資料內容而定,所以典型的程式描述是"我Debug版跑起來都沒錯,但是Release版就是會當機..."。
class BadSample
{
private:
  char *ptr;
public:
  BadSample(){};
  ~BadSample()
  {
    if( ptr ) delete ptr; // 當機
  };
};

第三是當類別資料copy涉及記憶體時,永遠要寫複製建構式與operator =(); 或者,類別不允許這兩個函式的執行,原因很簡單,以下以程式說明之:
class CopyConstructor
{
private:
  char *ptr;
public:
  void ANewFunction()
  {
   ptr=new char[1024];
  }
  ~CopyConstructor()
  {
   if( ptr ) delete[] ptr; // 假設建構式有把ptr=NULL;
  }
};

以下例子解構時都會當機,因為ptr會被釋放多次:
CopyConstructor a;
a.ANewFunction();
CopyConstructor b(a);  // b物件刪除的時候,ptr釋放一次,等到a物件刪除時,又一次
CopyConstructor c;
c=a;  // 同上

所以要嗎你把類別這麼寫:
class CopyConstructor
{
private:
  char *ptr;
public:
  void ANewFunction()
  {
   ptr=new char[1024];
  }
  CopyConstructor(const CopyConstructor& c )
  {   
    ptr=new char[1024]; // 各自配置各自的記憶體
    memcpy( ptr, c.ptr, 1024 );
  }
  void operator =(const CopyConstructor& c ) // 簡化起見,不討論回傳值
  {   
    ptr=new char[1024]; // 各自配置各自的記憶體
    memcpy( ptr, c.ptr, 1024 );
  }
  ~CopyConstructor()
  {
   if( ptr ) delete[] ptr; // 假設建構式有把ptr=NULL;
  }
};

要嗎你把類別這麼寫:
class CopyConstructor

{
private:
  char *ptr;
public:
  void ANewFunction()
  {
   ptr=new char[1024];
  }
  CopyConstructor(const CopyConstructor& c )
  {   
    ASSERT(0);  //給他當機
  }
  void operator =(const CopyConstructor& c ) // 簡化起見,不討論回傳值
  {
    ASSERT(0);  //給他當機
  }
  ~CopyConstructor()
  {
   if( ptr ) delete[] ptr; // 假設建構式有把ptr=NULL;
  }
};

解構式的注意事項是,如果想讓後續類別繼承,一定要用virtual,讓繼承解構的順序正確,還有,同建構式般,不可以在解構式中,呼叫虛擬函式,因為解構是繼承物件先解構,然後才基底物件,衍生物件已經不見了,基底物件去哪呼叫正確的虛擬函式呢?
 

2009年1月21日 星期三

澤澤的話語

澤澤開始學說話了,他對人類的語言有著十足的好奇心,會說的字算來應該不少吧?

ㄅ字頭的字: 爸、不要、布布
ㄇ字頭的音: 媽、嬤、馬
ㄉ字頭的音: 丟、到
ㄓ字頭的音: 找不到 (超順,一口氣唸完...)
ㄐ字頭的音: 舅舅(尾巴還會可愛的拉高音調),舅媽媽(一定要多個媽)
ㄧ字頭的音: 要、姨、姨嬤、衣服、衣架
ㄨ字頭的音: 我
....

那日心血來潮,發現他會說"我"、ㄧ字頭的音,想說把這些組在一起,多個"是",多個"澤",就可以說出"我是乙澤"這個令人充滿驚奇的句子。

開始了,他很奇怪地跟著我說了,"我"..."素".....,歐買尬,原來我兒子有嚴重的台灣國語,老婆在旁邊大笑,澤澤跟著笑起來,"哈哈哈...",混亂了好一陣子後,他才又專心跟著我念,"我...素...乙...",乙字發的不是很準,有點像一,不管如何,眼看著快要大功告成,我還沒說"澤"的時候,這小子忽然高興的大叫著,"衣服!","我素衣服...",啊? 我兒子是衣服?? 沒錯,到今天為止,只要我說,"我是.....",他一定接"衣服....."。

2009年1月16日 星期五

類別的設計

類別是C++實踐物件導向程式的基礎,一般人對類別設計應該都有了些基本的概念,這篇想要以系統開發的角度,來強調類別設計與使用的注意事項。

1. 變數封裝
基本上除了特別特別的需求,一般都建議你,把資料放在private區段,為什麼呢,這樣我才可以設計函式,管控如何存取這些變數值,避免變數被莫名奇妙的變更,造成錯誤時,難以追蹤的困擾,所以一個基本的變數,如果需要被外界存取,那需要一個設定函式,一個讀取函式,如果不需要,內部使用,那一個都不需要提供。

可以放在protected嘛?基本上,類別設計,建議你忘記有這個區間的存在,為什麼?protected區間可以被後續繼承類別存取,除非你有特殊用途,否則你埋下了一個炸彈,你如何知道後續繼成出去的類別,不會胡亂更動其值?不會又寫了一個函式,把資料public出去?




class NPC

{

private:

  int m_iHp; // 生命力,除了初值化外,不應該由外界來操作這個變數

  int m_iAcc; // 物理攻擊準確度,由企劃公式產生,更不應該由外界來操作

};

這樣的寫法,也斷絕了,系統開發時,有人妄想修改資料的念頭,減少將來系統長大時,潛藏錯誤發生的機會,真的需要處裡內部數值時,再以下列方法處理:

class AStrangeClass

{

private:

  NPC *m_pNpc;

public:

  void SetNpc( NPC *npc ){ m_pNpc=npc; }

  NPC *GetNpc(){ return m_pNpc; }

};

關於全域變數呢,基本上建議,寫程式應該要把他視為罪惡,這意味著有一個完全不能改的機制在那邊,無法控制其變數名稱,只要跟這個變數有瓜葛的類別,形成了一個盤根糾結的系統,如同先前我提到的,少用繼承一樣的結果,將來改其中任何一個地方,潛在需要修改的程式是全部。

可是全域變數一定會用到的,這個地方建議去參考"Design Patterns"的Singleton樣式,提供了一個將變數全域化,但卻被類別管控的方法。

2. 成員函式設計
成員函式設計,視目的而定,沒有一定的規範(除非專案本身的規定),這裡建議,請在函式放置位置的時候,多思考一下,要放在哪個區段,一般的建議是這樣,如果函式需要被外界存取,放public(廢話...),不需要的時候,請放private(廢話兩枚)。

是廢話嗎?回去隨便拿一個別人寫的類別看看,是不是有很多函式被外界呼叫後,如果數值亂填,可能造成系統錯誤之類的?你也許會反駁說,說明中有寫,這是內部使用的,錯誤是因為外界亂用啊?可是我要說,設計類別的基本原則,就是讓人容易使用,且不容易出錯,與其你放任函式public,裡頭寫一大堆exception,為什麼不把這些函式private起來,錯誤發生率也小多了不是嗎?

常在程式之間流傳的對答:啊,我知道bug發生原因了,是因為某某某呼叫了我一個函式,而這個函式是內部使用的,不可以這樣呼叫...,謹慎考慮多少可以減少潛藏可能發生問題的機會。

3. 注意別人類別的規範
接下來是使用別人類別的注意事項,不要企圖去改變回傳值的屬性,原因是來自C++轉型的方便,許多人喜歡把參數或是回傳值,亂轉型,變成其他的類型來操作,這本是無可厚非的事情,最明顯的就是const回傳值,但是,系統開發的時候,標明const,就是不希望你去改變這個數值,嚴重的說來,改變這個值,搞不好會發生嚴重的錯誤,而經常有人就愛去碰:




class A

{

public:

  const char *GetTypeName(){ return "this is a type"; }
};

void main()

{

  A a;

  const char *ptr=a.GetTypeName();

  strcpy( (char *)ptr, "this is not a type" );

}

編譯都對,執行的時候,就當給你看,所以
(a) 當你看見函式回傳值有const時,不要嘗試去改變回傳值內容

(b) 當函式宣告有throw時,請處理exception
因為throw表示,函式內可能發生無法預期的錯誤,catch它,可以明瞭錯誤的原因,方便除錯

(c) 當類別裡面沒有虛擬函式時,不要嘗試去繼承它
如果你寫個類別,會給後續類別繼承,你會怎麼寫?一定是在裡面寫一些虛擬函式,暗示後來的程式師,這幾個函式可能以後會有其他的寫法,如果你寫個類別,沒有任何虛擬函式,那表示什麼?不想被別人繼承?功能已經完備?不論哪個答案,都不該再有類別繼承它了。
除此之外,還有一個原因,虛擬解構式,在繼承情況下,解構的記憶體才會正確的釋放,你的類別什麼虛擬函式都沒有(含解構式),繼承它,未來背負著一個memory leak的風險。

(d) 正確的繼承
假設,CHuman是個基底類別,CHuman人物會動、可裝備、會說話....,假設遊戲中出現了一種東西叫"假人",人類經常習慣用名稱將之歸類為人類的一種,所以他該繼承CHuman,然後把所有不能用的功能重寫。你做了什麼事?把一個功能齊全的人類別,降級成什麼都不會的假人?只因為它長得像人?或者應該由一個物品,開始去繼承,增加功能比較適合?

這裡提一個繼承基本上的判斷標準: 假人是不是一種人? 意思是,人類會什麼,應該假人也要會什麼,少把類別寫成,假人是人的一種,但是他太多不會這不會那時...,大概就是錯誤的繼承關係了。
  

2009年1月11日 星期日

變數初值

程式寫這麼久,以前經常會遇到,"啊? C++不可以這樣嗎?"或者是"這樣寫有差別嗎?"的驚訝,想說這可能不只是只有我有的感覺,因此把這些東西分享出來,給大家做參考,你也可以參考Effective C++這本書籍:

初值化(Initial)和給定數值(Assign)

(a) 什麼是initial?
變數宣告的時候,直接給了數值,像是:
int a=10;
(b)什麼是Assign?
變數宣告完後,再另外給定數值,像是:
int a;
a=10;

這兩種寫法有差異嗎? 對build-in類型,像是int, float, ...沒有差別,但是對物件而言,就決定了一點點效能上的差別了,我以一個類別A來解釋:
class A
{
}; // 什麼事都沒有的類別
使用assign你必須先宣告一個物件 A a;
此時,物件已經執行了一次建構式,把內部數值初始一次,你也許會問,類別中沒有建構式啊?何來呼叫建構式? 答案是,類別雖然沒有設計建構式,但是C++標準中有幾個函式你不需要寫,C++都會幫你生出來,而你寫了,就不會幫你產生,分別是:
預設建構式: A::A(){}
複製建構式: A::A(const A &a ){}
解構式: A::~A(){}
指定運算元: A::operator =( const A& a ){}

所以當你下A a;a=b;這樣的程式時,已經執行了一次建構式,之後再一行assign,物件又會執行一次"指定運算元",所以你跑了兩次物件的函式。
而Initial呢? 看起來A a=b; 這樣的程式碼,同樣是執行一次建構式,然後再執行一次指定運算元?答案是,編譯器直接執行複製建構式,所有數值一次搞定,比起Assign快了一倍,也許不多,但是,如果你的物件內部,變數數量超多,一個assign需要執行的指令碼很多,那也許就有點可觀了:)

說到建構式,也許有人會問,他看過以下兩種程式碼,有差別嗎?
A::A()
: a(120), c(0.3)
{
}

A::A()
{
a=120;
c=0.3;
}

答案依照結果論而言,沒有差別,但是同樣潛在一點點效能上的差別,首先第一種方法A()後面接著的":a(...." 那行稱為 "member initializer list",專供給物件內成員變數的初值化,與呼叫基底類別的建構式所用,在這個時候,物件的記憶體還在配置中,C++會一邊配置記憶體,一邊把你指定的數值塞進去,所以,變數給定數值的次數只有一次,而下面那種A::A(){ ... } 的方式呢? 此時記憶體已經配置完成,變數數值已經給定過了一次,而此時又再設定一次,所以跑了兩次數值給定的程序,所以會慢一滴滴。

member initializer list這麼好用,有什麼限制嗎?
有的,指定順序要跟變數宣告的順序一致,原因說過,C++會一邊配置記憶體,一邊給定數值,如果順序不對,以下的程式就可能出錯:
class A
{
vector buf;
int size;
A() : buf(size), size(20)
....
};

你希望配置大小為size的vector的陣列,但是在 呼叫buf(size)建構時,size記憶體還沒配置出來,根本不知道size的數值,而,size(20)初值後,buf(size)已經執行過了,所以會發生錯誤,所以正確的順序:
class A
{
int size;
vector buf;
A() : size(20), buf(size)
....
};

澤澤的異想世界



澤澤是我的兒子,才一歲半,帶出去,每個稍有審美觀念的路人,都會卯起來說好可愛,這對身為父母的我們而言,除了不擔心未來找不到老婆之外,也滿足了我(不含老婆)無聊的虛榮心。



但是,這小子經常不知哪裡學來一些怪把戲,遇到尷尬的時刻,嗯的一聲,用一根指頭胡亂的指著一個方向,圖片裡那兩個小妹妹,很認真地看著我兒子指的方向許久,後來聽說,這個所謂的怪把戲是我教的...。

最近他迷戀上有毛的質料,恰巧冬天來了,看到有阿姨身上穿有毛絨絨的衣裳時,都會舉起他的小指頭,"毛毛、毛毛"地大叫,因為模樣可愛,所以大人都會友善地把毛絨絨的部份,大方地給他玩,可是他不喜歡用手摸,最後都會把小臉湊到上面,小心翼翼輕輕地摩擦著臉...,真擔心剛剛他哭過,眼淚鼻涕會殘留在上面....,只是不知情的阿姨們還是很高興地稱讚著可愛。

可愛歸可愛,他發起脾氣來,也是讓人又愛又恨,不管三七二十一地右手猛搖,飆著眼淚,"不要、不要",地哀叫著,一般這時候,所有人都會投降的,只能順著他的意,可是有時候像是喝牛奶、洗澡、睡覺等,大人們可是不能退讓的,有一次晚上,喝完牛奶該睡了,不知怎麼就是不肯睡,發起脾氣來,又是哭又是鬧,猛搖著右手,使出他拿手的"不要不要~",正當無計可施的時候,我老婆靈機一動,把他抱在一張毛絨絨的毯子上,只見他心滿意足地把小臉靠在上面,安安靜靜地睡著了,又恢復成人見人愛的小天使模樣。

2009年1月8日 星期四

0.01秒的代價

我的畢業論文,跟跳舞機遊戲相關,就是那種只要聽音樂,然後按上下左右的遊戲,論文論什麼呢? 就是眼睛閉起來,只看上下左右按下的時間,跟正確時間的相關關係,來推測現在是BOT在玩還是人類在玩? 聽起來很玄,但是經過兩年學校的洗禮,發現方法還頗為容易理解,就是線性代數與資料探勘的應用,此篇不是技術文章,所以不用太擔心我會介紹這兩者。

首要工作呢,就是要蒐集遊戲中的資料,來推估一些準確度上的特性,以作為分類的依據,因為不方便找開發公司要玩家資料(一方面開發公司也會以千百種理由拒絕你),因此腦筋動到駭別人程式的方面。

從九月到現在,駭客程式寫了一個多月,難歸難,總是學到一些東西,推算了一些可能,給老師打包票,誤差應該在0.033秒內,老師面有難色,他委婉地希望壓到0.01秒以內。

10月起就開始撰寫在別人家程式裡面,錄製玩家資料,與推算正確時間點,那是瞎子摸象的過程,我怎麼知道對方程式怎麼寫? 怎麼取時間? 標準時間又是什麼? 程式邏輯改了又改,每測一次單位時間是5分鐘,因為每次開始前,都要經過長長的更新時間,不能用除錯工具,當了不知道為什麼,錯了只能從LOG去猜,要怎麼調整,都是為了那0.01秒。

摸索來到了12月,沒錯,一個月內,只在喬公式,猜測這邊可能加個係數,那邊可能多除個4,無聊且痛苦的日子,誤差一直在+-0.033秒左右徘迴,後來問到指導教授一句: "對方遊戲公司有跟我們合作,所以可以去找他們解答...",一時間愈哭無淚,我怎麼不早問,白白浪費了這麼久,花在猜測的時間上。

現在1月了,情況如何? 是的,有改善,所有錄製時間的誤差最大也不超過0.01秒,我過關了嗎?

沒有,原因有兩個
(1) 不要忘了,蒐集資料的程式只是第一步,我的論文書面資料隻字未動
(2) 老師很好心,找人與我共同研究,他做分析,但是0.01秒關係到按鍵分數的評分一個等級,會造成論文結果的誤差

所以需要把這個0.01秒的誤差解決...

學校論文繳交時間是2/2日,必要條件是,通過口試+繳交總圖書館4x頁論文正本,扣除過年,好了,只剩下十來天,我這裡先上演mission impossible4了! 結果這0.01秒的代價,就是再花半年的時間+一學期註冊費60,000元+沒辦法專職工作的損失.....好貴的0.01秒啊

2009年1月7日 星期三

少用繼承

物件導向程式設計中,為了提升程式碼的再利用性,提供了繼承這樣的架構給我們使用,只要看不順眼的類別,就可以用繼承改寫舊功能,達到擴展的目的,範例如下:
class AI
{
private:
virtual void FSM(); // AI類別實做FSM
};

class Monster : public AI
{
private:
virtual void FSM(); // Monster類別改寫原來的函式
};

但就"程式碼再利用的"目的而言,事實上,我們常用的方法還有一種,"組成",利用物件彼此間的協力,完成特定工作,譬如,老闆要求去做一件A、B君都只會做部分功能的工作,依照繼承的觀念,是否要生成第三個類別C,繼承A、B的能力,然後改寫部分關聯而增加的程式碼? 然而我們是不是可以換個方向思考,我可以設計個類別C,工作交代C,C會先請A君做完他會做的,然後B繼續接手,一樣可以完成工作,與繼承兩者,事實上複雜度一樣,都需要3個類別,但是程式維護的小心程度卻是不可相提並論,程式如下

class AI
{
public:
void FSM();
};

class TimeControl
{
public:
void DoSomething();
};

用繼承:
class Npc:public AI, public TimeControl
{
void DoSomething(); // 這裡遭遇第一個難關
// 多重繼承,要小心AB的成員變數、成員函式設計關係
};

用組成:
class Npc
{
private:
AI ai;
TimeControl tc;

public:
void DoSomething(); // 我可以在這裡,靈活運用ai, tc物件的函式達成老闆目標
};

你可以看見,往後ABC三個類別的修改,相依性是否會降低很多很多,說白話,只要AB類別確定功能正常,發生BUG一定就是C,A類別新增怎樣的功能,不會影響C或B的運作,同理C增減怎樣的函式,AB都不會影響,而使用繼承呢? 就算你確定ABC三類別功能都正確,卻有可能因為繼承的關係,造成邏輯錯誤,像是C發生BUG的原因,是因為C修改了一個變數值,然後是A或B非常重要的變數,不允許由外部變更的(這裡有個小伏筆,避免修改到別人家的變數,變數請用private封裝...)。

加上有許多程式濫用繼承,一個遊戲專案裡頭隨便都成千上百個類別,加上彼此繼承的關係,專案像是N坨大肉粽樹,盤根錯節,某Z類別有BUG,原因卻是在千里之外八竿子打不到關係的A類別造成,改了A,卻連鎖反應要把某個肉粽樹的函式都要改過,想到就累人啊,所以,有得選的情形下,請少用繼承多用組成

再舉個生活上的例子,老闆要設計個簡報在客戶面前推銷商品,為了達到目標,他需要繼承文書小姐的能力+產品部門對商品規格特色了解的能力+簡報能力+....,或者,他可以把公司內文書小姐、產品部經理、行銷推廣部經理叫來,說出需求,接下來大家就自動會把事情搞定,而老闆則可專心挑毛病去,愛用繼承的,通常工作累得要死,愛用組成的,通常是老闆,你選哪個?

2009年1月1日 星期四

好的遊戲程式系統簡介

日前發於DCI的文章...
關於遊戲系統架構的一點淺見

STL5.2.1版編譯與VC安裝

STL安裝:

1. 首先,當然需要取得STLport原始檔案

請到http://sourceforge.net/projects/stlport/去下載最新版本,並將之解壓縮到一個你覺得名字夠響亮的資料夾下,就"C:\mighty-STL"好了,為了方便,以後稱這個資料夾為"[STLport]"。



2. 設定編譯變數

用來指定VC DOS指令的相關變數、執行檔路徑等

(2.a)開一個DOS視窗(應該不用教了...)

(2.b)然後執行你VC資料夾下面的一個批次檔

2003版,沒有意外,是這樣"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\vsvars32.bat"

2008版,沒有意外,是這樣"C:\Program Files\Microsoft Visual Studio .NET 2008\Common7\Tools\vsvars32.bat"

記得喔,在DOS視窗下,頭尾兩個 " 都要打喔,不然會被視為錯誤的指令



3. 編譯STLport

(3.a) 設定STLport編譯環境

DOS視窗路徑移動到[STLport]下,執行"configure 你的vc版本代號",代號清單如下:

msvc6 VC6.0

msvc7 VC.net 2002

msvc71 VC.net 2003

msvc8 VC.net 2005

msvc9 VC.net 2008

例如,我的VC是2008,指令就是: "configure msvc9"

(3.b) 開始編譯

DOS視窗路徑移動到[STLport]\build\lib

執行 "nmake clean install"

正常的話,開始等約數分鐘,可以在[STLport]\lib\發現編譯好的函式庫了

STLport編譯預設為多行緒的函式庫,如果想要產生非多行緒的函式庫,請在configure 時,加入"--without-thread"選項,不過,非SingleThread的專案,就link不過了,因為兩者編譯出來的函式庫名稱相同,所以想要兩者共存,需要自行指定函式庫。

4. VC環境設定
(4.a) 新增include資料夾 [STLPort]\stlport,需要放在第一個,避免VC編譯時仍然採用舊版隨機送的STL
(4.b) 新增lib資料夾 [STLPort]\lib,先後順序無所謂...
(4.c) 執行時預設VC自動連結相關函式庫,除非你太閑,把[STLport]\stlport\stl\config\user_config.h中定義 _STLP_DONT_USE_AUTO_LINK打開,每個專案都要定義_STLP_VERBOSE_AUTO_LINK一次
(4.d) 如果你不想使用STLport了,可以於編譯時定義以下兩值,強迫使用STD版(對某些情況,如怎麼編譯就是不會成功的,我有一個MFC專案就是如此...文件說可以定義_STLP_USE_MFC...)
#define _STLP_DONT_REDEFINE_STD
#define _STLP_WHOLE_NATIVE_STD