精華區beta mud 關於我們 聯絡資訊
作者 jjchen.bbs@csie.nctu.edu.tw (jjchen), 看板 MUD 標題 LPC 入門-1 時間 交大資工鳳凰城資訊站 (Wed Aug 19 01:22:51 1998) 來源 SimFarm!news.ntu!news.mcu!news.cs.nthu!news.csie.nctu!phoenix ─────────────────────────────────────── 野人獻xx一下,底下的文章歡迎流傳,修改,但請勿作為商業用途, wade@Fantasy.Space 8/18/87 ----------------------------------------------------------------- LPC 入門: 一、解釋: LPC 是一個程式語言,目前用來在 MudOS(及 LPmud)作業系統下撰寫物件, 它是由 Lars Pensj| 所建立,如您所想,LPC 其實是基於 C 語言的語法而 建立其既特殊又方便的程式環境,不過,LPC 並不是編譯式的,而是採解譯式 完成其物件的載入。在進一步說明 LPC 之前,先提醒讀者,LPC 是泥巴世界 中,屬於 LPMud 的程式語言,您學習它的目的無非是為了撰寫物件建構您心 目中的幻想世界。 二、LPC 的組成--敘述: 任何程式語言都是由敘述堆砌而成的,每一個 LPC 敘述均以分號 ';' 結 尾,而敘述則由運算子、函數、變數等構成,我們以最簡單的實例來說明: int a = 3; 上一行是 LPC 合法的敘述,「int」 是 LPC 的保留字,也就是我們不能 以「int」 這種保留字作為其它用途,事實上,「int」 是用來宣告整數變數 用的,如本例,既宣告了 a 這個整數變數,在宣告的同時,也告訴 MudOS 它的值是 3。既是變數,在往後是可以改變變數 a 的值,因此,我們稱此種 宣告為,「給定初始值」;而「給定初始值」除了在宣告的同時給定外,還可 以在用到變數 a 之前再給,如 「a = 3;」。 細心的讀者也許注意到我們在 每個敘述後面加上分號。在進一步說明 LPC 之前,先討論範例中的「=」。 很多第一次接觸 LPC 的人都會把,「=」 跟「==」搞混,前者是指定值用, 後者是判斷前後運算元是否相等,也有人把「=」 想成「<-」,即 「a = 3;」 想成「a <- 3;」。 「=」 跟「==」在語法上是完全不同的,不幸的,LPC 無 法判斷使用者是企圖,因此,若使用者用錯「=」 跟「==」所得的結果會出乎 意料之外,請小心使用。 三、結構化--敘述的組合: LPC 是一種結構化的語言,它可以將一堆敘述組合起來,將這些組合起來的 敘述看成一個敘述,而這種結構化的敘述最常見的型式是由「{」及「}」所? 堸_來的敘述方塊。我們同樣以實例來說明。 int add (int a, int b) { return (a + b); } 上面定義了一個函數,add, 在使用時需兩個變數,暫時稱為 a, b, 這兩 個變數都是整數變數,此函數「add」 代表「return (a + b);」 這個敘述。 函數是 LPC 這種結構化程式語言的命脈,往後用到的機會是 100%。 不知讀 者是否有注意到那一對大括號?讓我們先看看函數的定義方式。首先,函數可 概分為兩部份:函數宣告、函數主體。函數宣告即第一行 int add (int a, int b) 除了上面說明的部份,它還宣告函數 add 會傳回一個整數值,這點以後再詳 細討論,另外,函數主體即由一對大括號所組合起來的部份。其傳回值部份跟 變數的宣告是一樣的,等討論變數的時候再看。 四、運算子: 我們以簡單的觀念來看運算子,在我們的求學時代不就有四則運算嗎?加法 是用符號「+」 來表示,「+」 稱為運算子,而運算子「+」 左右兩邊的數字 則稱為「運算元」,就算在方程式裡也是透過運算子結合運算元來表示。在 LPC 中的運算子當然不止「+」、「-」、「*」、「/」,還有專門用在位元運 算的位元運算子:「&」、「|」、「^」, 及用在邏輯運算的邏輯運算子: 「&&」、「||」、「!」 等等,詳細的內容請看任何 C 語言入門的書。 在此我們看看 LPC 將指定運算子「=」與其他運算子結合的特殊情形: a += 3; 上面那一行敘述其實等於 a = a + 3; 在此不多說,再舉幾個例子,底下分左右兩邊,它們是相等的: a -= b; <--> a = a - b; a &= b; <--> a = a & b; 五、型態: 在 LPC 程式語言裡,變數有如我們在國中數學的觀念,然而在電腦的世界 中,我們為了節省記憶體,會將整數、實數、字串等等型態的變數區分開來, 因為變數有型態的區別,在 LPC 中使用變數前要先宣告其型態。在後面的章 節中會詳細介紹各種不同的變數型態如何使用,在此我們只介紹型態種類: int 整數,可以說是目前電腦最常用的型態,處理速度也快。 float 實數,在電腦中,通常稱為浮點數,處理速度上慢了點。 string 字串,這是 LPC 跟 C 不一樣的型態,以後會詳細說明。 mapping 對應,這是 LPC 特有的型態,有點類似陣列,我們可以稱 為關聯陣列,一樣,以後會詳細說明。 object 物件是最特別的型態,而物件在泥巴的世界中也有著非常重 要的角色,我們會常常提到。 function 這個型態非常特殊,會另篇介紹。 void 這個型態只有函數用得到,它說明函數不必傳回值。 mixed 這也是特殊型態,它說明不用檢查型態,也可以說是這種型 態的變數可能是任何型態。 陣列 陣列是用星號來表示,可以上面所有型態的陣列,宣告方式 如: int *a; object *obj; mixed *m; 等等 六、修飾子: 有一些特殊的型態可以加在前一節基本型態的前面,底下先列出簡單的說明 後再舉例來看。 varargs 用在函數,用來說明該函數的參數個數是可變動的,或者說 告訴驅動程式,不用因該函數的參數個數變動而傳回錯誤。 private 指示變數、函數是「私有的」,也就是不能有其他地方使用 如:在物件 a 定義 private int add();則不能用下列呼叫 call_other (a, "add", ...); 或 a->add(...); public MudOS v21 版(含以前)使用,用在函數上,說明該函甚至以 private 繼承也可以呼叫。 MudOS v22 版(含)以後已取消,改為預設值,不需特別指定 static MudOS v21 版(含以前)使用,此修飾子用在變數與函數效果 不同,作用在函數上同 private, 作用在變數上的時候,該 變數不能用「save_object()」或「restore_object()」 來 「儲存」與「取回」該變數存在檔案的值,也就是說,該變 數的值離不開定義它的物件。 MudOS v22 版(含)以後可在 driver 的 options.h 中取消 SENSIBLE_MODIFIERS 的定義,或用下列新修飾子: nosave 用在變數上,用來達成上面的目的:該變數的值離不開定義 它的物件。 protected 用在函數上,同 private。 nomask 用來指示驅動程式,該變數或函數不能再被定義,善用它可 避免被不肖巫師利用重新定義函數來做違法的事。 七、繼承: 在 LPC 中有物件導向程式設計的觀念,其中繼承是用關鍵字「inherit」 來達成,底下是語法與說明: 語法: inherit 檔案完整路徑名; 說明: 檔案完整路徑名 要用雙引號括起來. 如: inherit "/std/user.c" 通常, 常用的"被繼承"物件會由系統用 #define 定義常數, 如 #define USER_OB "/std/user.c" 那麼, 就可以用 inherit USER_OB; 在稍後「前端處理」一節中我們會對「#define」 作解釋。 inherit 敘述提供 LPC 繼承的能力,沒錯,就是 OOP 的概念,繼承讓物件 可以很安全跟很方便的使用其他物件所定義的函數跟變數。由於 MudOS 驅動 程式內部會儲存整體變數,然後把整體變數跟檔案分開編譯,因此各個物件可 以彼此分享已經編譯好的物件,當然,這些物件彼此有自己的整體跟局部變數 可以使用,例如: 物件 A, B, 都繼承 C, 編譯 A 或 B 的時候不會讓編譯器 重新編譯 C,不過,如果 A 或 B 有重新定義整體變數的話,不會引起重複定 義的錯, 但是在 A 或 B 中會讓 C 的整體變數失效,也就是 A 或 B 的宣告 會「蓋過」 C 中的宣告,如果沒有在 A 或 B 中重複宣告的話,當然會使用 C 的宣告囉。還有,上述的重複宣告並不影響 C 使用自己的變數的運作。 現在再假設 A 繼承了 B,物件 A 可以定義跟物件 B 中相同名稱的函數或 變數,如前所述,A 的引用會自動「蓋過」B 的,不過如果只是這樣的話, 繼 承就不會那麼特殊了。如果你因為變數同名,又想引用 B 的函數或變數,可 以使用「::」來引用。例如物件 A 繼承了 B, 兩個各自定義 query_long(); 在物件 A 中使用 query_long() 的時候,是使用到 A 定義的函數,如果要 在 A 使用 B 的 query_long(), 那就可以用 B::query_long(); 不過,如果 是同名變數的引用,則只能透過 B 中定義的函數來存取,這是為了物件的概 念產生的。 此外,B 可以限定是否要讓別的物件繼承函數或整體變數,限定的方式是以 static 來修飾函數或變數。 另外一點,如果 B 重新編譯的話,A 也只會用到舊的 B,除非 A 在 B 之 後也重新編譯過。 LPC 允許多重繼承,也就是說,在一個物件內可以寫多行的 inherit 敘述 假設 special.c 繼承 weapon.c 跟 armor.c,而 weapon.c 跟 armor.c 也 都提供了自己的 query_long(),假設要讓 special.c 有時像 weapon, 有時 像 armor,要像 weapon, 就用 weapon::query_long(),要讓它像 armor 就 用 armor::query_long(),要用自己的 long,就直接定義 query_long() 並 直接呼叫來用。「::」又稱為「引用子」, 或是「視域子」, 也就是讓你引 用祖先的函數,或者把函數加上適當的視域範圍,這樣就不會「用錯」,也就 是不會讓編譯程式「看錯」,所以稱為「視域子」。 請參考上一節跟「修飾子」有關的說明。那邊會有如何隱藏函數跟變數宣告 的說明(在前面有提到 static 這個修飾子)。 八、前端處理: 前端處理是在 LPC 編譯程式編譯之前的處理工作。通常做下列工作: 。 分享「定義」及程式碼(用 #include)。 。 巨集定義(用 #define,#undef)。 。 條件編譯(用 #if,#ifdef,#ifndef,#else,#elif,#endif)。  --- 前三項是跟 C 語言一樣的。 。 除錯(用 #echo)。  。 編譯指示(#pragma)。  。 格式化文字字串(用 @, @@)。 前端處理指示子是用「#」 當開頭的,而且一定要在該行的第一個字。 1、分享「定義」及程式碼 ^^^^^^^^^^^^^^^^^^^^ 語法一:#include <FILE.h> 語法二:#include "FILE.h" 說明: 語法一與語法二的不同在於搜尋 FILE.h 時的順序,語法一是系統內 定的,該順序定義在啟動驅動程式時組態檔中「include directories」 而語法二則從出現「#include "FILE.h"」 的檔的目錄下找。 檔案A若出現 #include 前端處理指示,則驅動程式會把 FILE.h 讀 進檔案A中 #include 出現的地方。這種方式的分享程式碼會在編譯物件 時重新編譯分享程式碼,因此跟繼承是完全不一樣的。再者,如果分享程 式碼中定義的變數或函數名稱與檔案A定義的變數或函數名稱一樣的話, 驅動程式會出現「duplicate-name error」的重複定義錯誤訊息。若用繼 承的話就不會有此重複定義錯誤出現,如繼承那節所述。 2、巨集定義 ^^^^^^^^ 語法一:#define 識別字 巨集定義字串 語法二:#define 識別字(參數) 巨集定義字串 說明: 巨集定義可以用簡單的「識別字」取代較長的「巨集定義字串」,事 實上巨集定義字串可以是好幾行。不用前一段的分享而改用巨集定義的原 因是巨集定義有識別字可資識別,在引用時容易記憶。再說,巨集定義通 常都定義在一個專門的檔中,以分享的方式含括進來,以利往後引用。 習慣上我們會把巨集定義的識別字用大寫字母,而且巨集定義會放在 檔案的前面,如果有程式碼分享,會放在那之後。底下舉例說明: #define STACKSIZE 40 上例以 STACKSIZE 代替 40, 因為 STACKSIZE 比 40 更具識別與記憶性 #define INITCELL(x) 2*x 語法二的宣告方式在引用時參數會隨引用而改變。如 INITCELL(2) 等於 2*2, 而 INITCELL(10) 等於 2*10。 值得注意的是在定義巨集時,識別 字與參數間不可有空白字元,而呼叫時卻可以。即 #define INITCELL (x) 2*x 事實上是跟我們上一例不一樣,它等於 把 "(x) 2*x" 用 INITCELL 取代,所以 INITCELL(2) 等於 (x) 2*x(2) 若我們要取消巨集定義,可以用「#undef 識別字」來達成。 3、條件編譯    ^^^^^^^^ 語法: #ifdef <識別字> #ifndef <識別字> #if <運算式> #elif <運算式> #else #endif 說明: 條件編譯指示子可以給你的程式碼增加彈性,當「識別字」有(或沒 有)定義時,編譯程式會作出不同反應。條件編譯可將該物件應用在不同 版本的驅動程式上,或提供管理者一些彈性限制功能,如是否開放玩家連 線,開放上線人數等。 <識別字>的定義方式如上一段所說。不過也可以引用驅動程式的定義 而<運算式>則是一個常數運算式,也就是在編譯階段就可以決定值。不過 其效果只有布林值,也就是0或非0。<運算式>可包括下列任何運算子: ||, &&, >>, <<, <-- 邏輯運算 +, -, *, /, %, <-- 四則運算 ==, !=, <, >, <=, >=, <-- 比較運算 ?: <-- 條件運算 () <-- 組合程式碼 #ifdef 識別字 <-- 等於 --> #if defined(識別字) #ifndef 識別字 <-- 等於 --> #if !defined(識別字) #elif 運算式 <-- 等於 --> #else # if 運算式 <-- # 跟 if 有空白是合法的,但 # 一 # endif 定要在該行的第一個字的位置。 ☆ #if 0 可以用來將一段程式碼視為註解。 例一: #if 0 // 因 <運算式> 的值是 0,所以 // 本例中的這段程式碼不會被編譯,效果就像註解一樣 // write(user_name + " has " + total_coins + " coins\n"); #else // // 本段程式碼則永遠會被編譯。 // printf("%s has %d coins\n", user_name, total_coins); #endif 例二: // 本段程式碼可以在 TMI's /adm/simul_efun/system.c 找到, // 請自行研究 #ifdef __VERSION string version() { return __VERSION__; } #elif defined(MUDOS_VERSION) string version() { return MUDOS_VERSION; } #else # if defined(VERSION) string version() { return VERSION; } # else string version() { return -1; } # endif #endif 4、除錯    ^^^^ 語法:#echo 這一段訊息會在驅動程式啟動時印出來。 我們可以用 #echo 來告訴驅動程式印出一段訊息,如此有利於除錯。 通常我們都會配合條件編譯來除錯。 5、編譯指示    ^^^^^^^^ 語法:#pragma 關鍵字 說明: 關鍵字的種類有: 。 strict_types 嚴格的型態檢查 。 save_binary 儲存編譯好的二進位檔,利下次快速載入 。 save_types 儲存函數的參數型態,繼承時可用來檢查 。 warnings 對各種危險的程式碼提出警告 。 optimize 作第二回編譯,以利最佳化處理 。 error_context 當出現編譯錯誤時,印出某些程式內容 6、格式化文字字串 ^^^^^^^^^^^^^^ 語法一: @標記 <-- 開始 . . 敘述區塊 . 標記 <-- 結束 語法二:@@標記 <-- 開始 . . 敘述區塊 . 標記 <-- 結束 說明: 語法一是產生字串,而語法二是產生字串陣列,也就是語法二可以方 便用來換頁。底下舉例來說明: int help() { write( @ENDHELP This is the help text. It's hopelessly inadequate. ENDHELP ); return 1; } 上一段跟下一段相同 int help() { this_player()->more( @@ENDHELP This is the help text. It's hopelessly inadequate. ENDHELP , 1); return 1; } 也跟下一段相同 int help() { write ("This is the help text.\n"); write ("It's hopelessly inadequate.\n"); return 1; } 也跟下一段相同 int help() { this_player()->more( ({ "This is the help text.", "It's hopelessly inadequate." }), 1); return 1; } 九、函數的原型: LPC 的函數的原型宣告非常非常類似 ANSI C。 如果你在還沒定義函數前就 想引用到某函數,那麼,沒有函數的原型宣告的話很可能造成錯誤,甚至連參 數格式都不對了。 一般的函數的原型宣告如下: 函數傳回型態 函數名稱(資料型態 參數1, 資料型態 參數2, ...); 也可以不給參數名稱,宣告方式如下: 函數傳回型態 函數名稱(資料型態1, 資料型態2, ....); 如:string query_long(); int smile(string name); int smile (string); 十、重複使用程式碼: 除了前面提的前端處理「#include」、「#define」 可提供重複使用程式碼 外,LPC 還提供,前面提到的繼承「inherit」、「函數」 及下列方便的關鍵 字可讓我們重複使用程式碼。 。 for 。 while/do 。 if 。 switch/case 1、for 迴圈 ^^^^^^^^ 語法: for (表示式-1; 表示式-2; 表示式-3) { 敘述; ...; } 說明: 其中的「表示式-1」是用來初始化 for 迴圈。也就是在迴圈執行之 前就先執行「表示式-1」,此後每做完一次迴圈就先判斷「表示式-2」, 如果「表示式-2」為真(成立)就繼續下一次的迴圈,當然,第二次以後 的每個迴圈在執行前會先執行 表示式-3。 再說一次,「表示式-1」是第一次迴圈執行前做的,「表示式-2」是 在每次迴圈結束前用來判斷是否繼續用的,「表示式-3」是除第一次外在 迴圈執行前執行的。其中「表示式-2」用來判斷是否要繼續迴圈,如果在 該次迴圈結束後計算「表示式-2」的值非0就繼續執行。 如果在迴圈內執行到 break 敘述,則強迫停止該迴圈,並跳出該迴 圈,如果執行到 continue 敘述,則停止「該次」迴圈,並繼續下一次迴 圈。 底下有一個典型的 for 迴圈: int i; for (i = 0; i < 10; i++) { write("i == " + i + "\n"); write("10 - i == " + (10 - i) + "\n"); } 2、while 迴圈 ^^^^^^^^^^ LPC 的 while 迴圈跟一般的 C 一模一樣, 語法如下: while (表示式) 敘述; 或 while (表示式) { 敘述; 敘述; . . } 在 while 迴圈內的敘述會在表示式的值不為 0 的情況下執行, 也就是 該表示式為"真". 如果 while 的敘述有 break 敘述, 則會無條件跳出迴 圈, 如果碰到 continue 敘述, 則會跳過該次迴圈, 繼續下一次的迴圈, 這種用法就像在 for 迴圈裡說的一樣. 值得注意的是, while 比 for 迴圈更容易造成無窮迴圈, 如果這樣的 話, 會造成系統莫大的負擔, 甚至是當機. 底下看一個實際的例子. int test(int limit) { int total = 0; int j = 0; while (j < limit) { if ((j % 2) != 0) continue; total += j; j++; } return total; } 上面的結果會跳過奇數(在 if ((j % 2) != 0) continue; 這兒跳過), 而把 0 到 limit-1 之間所有的偶數加起來並傳回去. 底下的 for 迴圈可以做到相同的功能 int test(int limit) { int total, j; for (j=total=0; j<limit; j = j+2) total += j; return total; } 或 int test(int limit) { int total=0, j=0; for (; j<limit; j += 2) total += j; return total; } 3、if 條件敘述 ^^^^^^^^^^^ LPC 的 if/else 跟 C 一模一樣. 語法如下: 單一敘述: if (表示式) 敘述; 多重敘述: if (表示式) { 敘述; 敘述; ....; } if/else 的方式: if (表示式) { 敘述; 敘述; ....; } else { 敘述; 敘述; ....; } 多重 if/else, 或稱為巢狀的 if/else if (表示式) { 敘述; 敘述; ....; } else if (表示式-2) { 敘述; 敘述; ....; } 注意: if/else 的層數沒有明顯的限制. - - - - - 另外一種受歡迎的 if/else 敘述, ?: 配對, 這也跟 C 的用法一樣. 表示式 ? 敘述-1 : 敘述-2; 如果表示式為真執行 敘述-1, 否則執行 敘述-2 底下列出實際例子. if (表示式) 敘述-1; else 敘述-2; 跟下面的方式一樣: var = 表示式 ? 敘述-1 : 敘述-2; 4、switch 多重條件敘述 ^^^^^^^^^^^^^^^^^^^ LPC 的 switch 敘述跟 C 幾乎一模一樣. 唯一的差別在於 LPC 允許使用 字串跟整數. 一般的語法如下: switch (表示式) { case 條件-1: 敘述; ..... break; case 條件-2: 敘述; ..... break; . . . default : 敘述; ..... break; } 一般來說, switch 能做到的都可以用 if/else 來達成, 如果有很多種 狀況的話, 用 switch 會比較容易閱讀跟除錯. 再說, if/else 可能配對 錯誤而造成意想不到的狀況. 每個條件都要用 break 隔開, 如果沒有 break 的話, 不會結束該條件內的敘述. 這種現象可以讓你很容易就做到讓多種 狀況都執行相同的敘述. 上面的 switch 敘述約等於下面的程式碼. tmp = 表示式; if (tmp == 條件-1) { 敘述; ...; } else if (tmp == 條件-2) { 敘述; ...; } . . . else { // 等於 default 部份 敘述; ...; }