作者 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 部份
敘述;
...;
}