作者laechan (小太保)
看板mud_sanc
标题[转载] LPC 入门
时间Mon Mar 9 16:18:04 2009
作者
[email protected] (jjchen), 看板 MUD
标题 LPC 入门-1
时间 交大资工凤凰城资讯站 (Wed Aug 19 01:22:51 1998)
来源 SimFarm!news.ntu!news.mcu!news.cs.nthu!news.csie.nctu!phoenix
───────────────────────────────────────
野人献xx一下,底下的文章欢迎流传,修改,但请勿作为商业用途,
[email protected] 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 部份
叙述;
...;
}
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 218.170.228.153