Zh cn:Introduction to SourcePawn 1.7

From AlliedModders Wiki
Revision as of 04:42, 29 July 2016 by Xiaooloong (talk | contribs) (Created page with "本指南将介绍最基础的 SourcePawn 语言编写方法。 Pawn 是一门“脚本”语言,被设计用来将功能嵌入到其它程序中。它不是一门独立...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

本指南将介绍最基础的 SourcePawn 语言编写方法。 Pawn 是一门“脚本”语言,被设计用来将功能嵌入到其它程序中。它不是一门独立的语言(如 C++ 或 Java ),并且它的细节会由所在应用的不同而不同。SourcePawn 是 SourceMod 模组中使用的 Pawn 版本。

本指南并不教你如何去写 SourceMod 插件,而是介绍这门语言的语法和语义的概览。想要了解 SourceMod API 的细节,请参考Introduction to SourceMod Plugins

零基础教程

本章节针对没有编程经验的初学者。如果阅读完后仍有疑问,你可能需要学习另一门编程语言,如 PHP, Python 或者 Java,以便更好的理解编写程序的方法。

标识符/关键字

标识符是一串字母、数字、下划线的组合,用于唯一标识某些东西。标识符是大小写敏感的,通常以字母开头。 (译者注: c 语言中,标识符不能以数字开头。)

SourcePawn 中有一些保留的关键字,它们有特殊的含义。例如 ifforreturn 。这些关键字是特殊的结构,会在下文介绍。他们不能用于标识符的命名。

变量

在你开始编码前,有一些重要的概念你需要了解。首先就是变量。变量是一个用来保存数据的“标识符”或“名称”。例如,变量 "a" 能够保存数字 "2" 或 "16" 或 "0" 等。变量会申请存储数据所需的内存空间。

变量除了有“名字”,还有类型。变量的类型告诉程序如何处理变量存储的数据,以及决定变量使用的内存空间大小。Pawn 有三种最常使用的数据类型:

  • 整形,使用 int 关键字。整形可以存储 -2147483648 ~ 2147483647 (- 2^31 ~ 2^31 - 1) 的整数,类似于 c 语言中的 32 位有符号整数。
  • 浮点,使用 float 关键字。浮点型可以表示很大范围内的小数,但浮点类型是不精确的,不像整形是精确的。
  • 字符,使用 char 关键字。字符型存储一字节的数据,通常是一个 ASCII 字符。
  • 布尔,使用 bool 关键字。布尔存储一个真值:真(true) 或者 假(false)。

声明变量和给变量赋值的例子:

int money = 5400;
float percent = 67.3;
bool enabled = false;

函数

另一个重要的概念是函数。函数是能执行一系列动作的“标识符”或“名称”。当调用函数时,函数会执行一系列的动作,并返回一个值。SourceMod 中有几种类型的函数,不过他们都使用同样的方法来使用。例如:

show(56);     // 调用 "show" 函数,并传入一个整形常量 56。
enable();     // 调用 "enable" 函数,不传入参数。
bool visible = show(a);    //调用 "show" 函数,并将函数的返回值赋给一个变量。

传入给函数的数据被称为 参数。函数支持任意数量的参数(由于某种原因,SourceMod 函数最大支持 32 个参数)。关于参数将会在后文说明。

注释

所有以 "//" 开头的文本都被称作“注释”,他们不是程序的代码,不会被执行。有两种注释的方法:

  • // 注释内容 - 单行注释,这一行内所有双斜线后的部分都被忽略
  • /* 注释内容 */ - 块注释,所有星号之前的部分都被忽略

代码块

下一个概念是代码块。你可以把代码组织在“块”中,通过 { 和 } 来包裹。这能有效的使得一大块的代码运行起来就像一句语句一样。比如:

{
   here;
   is;
   some;
   code;
}

使用花括号包裹起来的代码块被用于编程的各个地方。块代码可以相互嵌套。编程时应尽早形成统一的、可读的编码习惯。

语言泛型

Pawn 可能和其他语言相似(例如 c),但是它们有本质的区别。一开始就明白这些区别并不重要,但是如果你有其他语言的变成基础,了解这些区别会很有帮助。

  • Pawn 是无类型的 在 SourceMod 1.7 之前,Pawn 是没有类型的。在旧版本的代码和内置方法中,使用了 “tag” 的方法和关键字 new 来表达类型。而现在,我们推荐使用类型来书写代码。关于旧版的信息请查阅 SourcePawn Transitional Syntax
  • Pawn 没有垃圾回收 Pawn 语言没有内置的内存分配功能,因此也没有垃圾回收功能。如果一个函数申请过内存,你需要自行释放。
  • Pawn 不是面向对象的 Pawn 没有结构体或者对象。SourceMod 1.7 以后,通过有限的语法糖实现了将特定的数据类型视为对象,但是用户不能自行定义对象和类。
  • Pawn 是单线程的 Pawn 运行在一个线程,因此没有线程安全检查。
  • Pawn 是编译的 Pawn 被编译为平台无关的中间代码,保存在 “.smx” 文件中。运行时,由 SourceMod 加载 “.smx” 文件,并解释为对应平台的机器码而执行。

早期语言设计由 ITB CompuPhase 完成。语言是为底层嵌入式设备设计,因此极为精简和迅速。

变量

Pawn 现在支持以下几种基本类型:

  • bool - 布尔,true 或者 false。
  • char - 字符,八位(一字节) ASCII 字符。
  • int - 整形,32 位有符号整数。
  • float - 浮点,32位 IEEE-754 浮点数。
  • Handle - 基本类型的 SourceMod 对象

其他的类型定义在一些头文件中。例如, enums (枚举)定义了一个新的“命名整形”的类型,以及许多从 Handle 派生的类型。

Strings (字符串)现在是以字符 '\0' 结尾的字符数组。细节将在后文说明。

声明

下面给出了一些正确的,以及错误的变量声明的例子。SourcePawn 最近增加了如下文描述的新语法。一些旧版本的代码可能使用了旧的语法,这些旧版本的语法不再支持。

int a = 5;
float b = 5.0;
bool c = true;
bool d = false;

错误的变量使用:

int a = 5.0;         // 类型错误。常量 5.0 是浮点型
float b = 5;         // 类型错误。常量 5 是整形

未明确赋值的变量声明将被赋以以下的值

int a;        // 默认赋值为 0
float b;      // 默认赋值为 0.0
bool c;       // 默认赋值为 false

赋值

变量创建后,可以被重新赋值。例如:

int a;
float b;
bool c;
 
a = 5;
b = 5.0;
c = true;

数组

数组是一个连续的数据序列。数组能在一个变量中存储多个数据,或者将一种类型的数据映射为另一种。

声明

数组使用方括号声明,例如:

int players[32];     // 存储了 32 个整形数
float origin[3];     // 存储了 3 个浮点数

数组默认会被初始化为 0。你可以显示的初始化为其他的值:

int numbers[5] = {1, 2, 3, 4, 5};       // 存储了 1, 2, 3, 4, 5。
float origin[3] = {1.0, 2.0, 3.0};      // 存储了 1.0, 2.0, 3.0。

如果显示的初始化数组,你可以留空数组的大小。例如:

int numbers[] = {1, 3, 5, 7, 9};

编译器会在编译时自动将这个数组的大小设为 5

定义数组时,如果方括号在变量名后,Pawn 将其认为是 定长 的。定长数组的大小在任意时刻都可以明确的知道。 另一些数组是 变长 的,声明时将方括号放在变量名前。例如:

int[] numbers = new int[MaxClients]

这段代码声明了一个 MaxClients 大小的数组,长度由 MaxClients 决定。变长数组直到数组被分配内存时,才能确定数组的大小。

使用

数组和普通变量使用方法一样。唯一区别是使用数组时需要一个 索引 。索引定义了从数组中需要取出的元素。

这是使用数组和索引的例子:

int numbers[5];
float origin[3];
 
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
numbers[3] = 4;
numbers[4] = 5;
origin[0] = 1.0;
origin[1] = 2.0;
origin[2] = 3.0;

以上示例中,索引是在方括号间的数字。数组的索引从 0 开始。如果数组有 N 个元素,那么数组的合法索引是从 0 到 N-1 。用索引存取的数据和普通变量的使用方法相同。

使用非法的索引会造成错误。例如:

int numbers[5];
 
numbers[5] = 20;

5 不是一个正确的索引。这个数组最大的索引是 4。

你可以使用任意的表达式作为索引。例如:

int a, numbers[5];
 
a = 1;                   // 赋 a 为 1
numbers[a] = 4;          // 赋 numbers[1] 为 4
numbers[numbers[a]] = 2; // 赋 numbers[4] 为 2

表达式的概念将在下文说明。

字符串

字符串是可以存储文本(或者二进制数据)的结构。字符串即为字符数组,与之不同的是,字符串必须以 '\0' (空终止符) 结束。Pawn 以 '\0' 判断字符串的结尾。字符串在 SourcePawn 中均为 UTF-8 编码。

通常,在存储字符串前,你必须明确字符串的长度。 SourcePawn 现在并没有自动确定字符串大小的功能。

使用

字符串通常被声明为数组。例如:

char message[] = "Hello!";
char clams[6] = "Clams";

以下代码作用和上述相同:

char message[7];
char clams[6];
 
message[0] = 'H';
message[1] = 'e';
message[2] = 'l';
message[3] = 'l';
message[4] = 'o';
message[5] = '!';
message[6] = 0;
clams[0] = 'C';
clams[1] = 'l';
clams[2] = 'a';
clams[3] = 'm';
clams[4] = 's';
clams[5] = 0;

虽然字符串极少使用这种方式初始化,但是非常重要的是要记住空终止符,也就是字符串终结的标志。编译器以及大多数 SourceMod 函数将会自动为你加上空终止符,所以当你直接处理字符串时这个概念是不可忽略的。

字符串使用半角双引号定义,字符使用半角单引号定义。

字符

字符文本既可以被当做字符串也可以被当做 cell (注:cell 类型是旧版本中的概念,译时此处文档未更新。)使用。例如:

char text[] = "Crab";
char clam;
 
clam = 'D';         //赋 clam 为 'D'
text[0] = 'A';      //赋 'C' 为 'A'。字符串现在为 'Arab'
clam = text[0];     //赋 clam 为 'A'
text[1] = clam;     //赋 'r' 为 'A'。字符串现在为 'AAab'

你所不能做的就是把字符数组与字符串混合起来。它们的内部存储并不一样。例如:

int clams[] = "Clams";                       // 无效。
int clams[] = {'C', 'l', 'a', 'm', 's', 0};  // 有效,但不是一个 String

函数

函数,如上文所述,是一种独立的代码块来实现某种功能。它们能被触发,或者说被调用,并通过参数来给定选项。

有2种调用函数的方式:

  • 直接调用 - 在你的代码中明确的调用一个函数
  • 回调(callback) - 由应用程序来调用你代码中的函数,例如事件触发器。

有 6 中类型的函数:

  • native(原生、本地):由应用程序提供的直接的,内部的函数。
  • public(公共):对于其他应用程序或者其他脚本可见的回调函数。
  • normal(普通):只有你能调用的普通函数。
  • static(静态):作用域仅在当前文件内,可以与 stock 一起使用。
  • stock(共享、仓库):由 include 头文件引入的普通函数。如果不使用,它不会被编译。
  • forward(导出、转发):由应用程序提供的全局事件函数。如果你实现了这个函数,它将被回调。

所有Pawn中的代码都必须存在于函数中,这有别于PHP,Perl和Python允许你全局代码。这是因为Pawn是一种基于回调的语言:从父应用程序产生一种动作,那么Pawn的函数就必须被编写来回应这种动作。我们的例子都是一些独立的代码,没有上下文关系,这样是为了纯粹的示范性目的。例子中的这些独立代码可能是某些函数的一部分。

声明

和变量不同,函数不需要你在使用前声明。函数有两个要素,签名(signature)和函数体(body)。签名包含了你函数的名字它所要接受的参数。函数体包含着代码的内容。

这是一个函数的例子:

int AddTwoNumbers(int first, int second)
{
  int sum = first + second;
  return sum;
}

这是一个简单的函数。签名是这一行:

int AddTwoNumbers(int first, int second)

解释一下,它的意思是:

  • int - 函数返回值的类型(整形)
  • AddTwoNumbers - 函数的名字
  • int first - 第一个参数(整形)
  • int second - 第二个参数(整形)

函数体是一个简单的代码块。它创建一个新的变量,叫做 sum,并给它赋值为 first 和 second 的和值(表达式在后文说明)。主要需要注意的是 return 语句,它表明了函数的结束,并且把值返回给函数的调用者。函数运行结束后会返回一个数据,否则返回 void

函数可以接收任意类型的输入,返回除数组外的任意类型。

传参数给函数:

int numbers[3] = {1, 2, 0};
 
numbers[2] = AddTwoNumbers(numbers[0], numbers[1]);

注意,cell类型通过值传递。就是说,它们的值不会被函数变化。例如: (译注:区别于值传递的一种传递是引用传递,具体参考其他的编程语言) (译注:旧版 SourceMod 中,类型通过对 cell 的 tag 来实现。原文中这里依然为 cell,未修正。)

int a = 5;
 
ChangeValue(a);
 
ChangeValue(b)
{
   b = 5;
}

这段代码不会改变 a 的值。这是因为 a 的值被拷贝了一份,这份拷贝值代替了 a 本身被传递过去。

更多的函数的例子将会贯穿本文。

公共函数

public 函数被用于实现回调。你不需要创建 public 函数除非需要实现特定的回调。比如,这里有两个回调函数,他们在 sourcemod.inc 中:

forward void OnPluginStart();
forward void OnClientDisconnected(int client);

为了实现和接收这两个事件,你需要写函数如下:

public void OnPluginStart()
{
   /* 代码 */
}
 
public void OnClientDisconnected(int client)
{
   /* 代码 */
}

public 关键字把函数暴露为公共的,允许父应用程序直接调用这个函数。

原生函数

Native 是由 SourceMod 提供的内置函数。你能像普通函数一样调用它们。例如,SourceMod 有如下函数:

native float FloatRound(float num);

它能像如下调用:

int rounded = FloatRound(5.2);     // rounded 被赋值为 5

数组参数

你可以传递数组或者字符串作为参数。非常关键的是,这种传递属于 引用传递 。就是说,相对于产生一份数据的拷贝(值传递),引用传递则是把它们本身传递了过去。

int example[] = {1, 2, 3, 4, 5};
 
ChangeArray(example, 2, 29);
 
void ChangeArray(int[] array, int index, int value)
{
   array[index] = value;
}

这个函数是通过给定的索引来给数组赋值。当它运行如下语句时,它会改变索引2所在的数组元素的值,从3变成29。结果如下:

example[2] = 29;

这里要注意两点。首先,在传递给函数时,并没有复制数组。他们通过 引用 被传递,保证了数组的一致性。其次,方括号在形参中被改变了位置,因为我们的函数可以接受任意长度的数组,所以必须使用变长语法。

为了预防数组被直接修改,你可以给它加上 const 关键字。这会导致一个代码错误当它尝试修数组的时候。例如:

void CantChangeArray(const array[], int index, int value)
{
   array[index] = value;    //不会被编译
}

如果你知道这个数组不会被修改,在数组前添加 const 是一个好习惯。这可以避免写代码时的错误。

表达式

表达式确切的说等同于数学运算。它们是一组用于求值的操作符或标识符。它们经常被包含在括号中。它们遵循严格的“运算顺序”。它们能包含变量,函数,数字以及表达式本身可以被嵌套在其它的表达式中,或者乃至被当做参数传递。

最简单的表达式可以是单独的一个数字,例如:

0;   //表达数字 0
(0); //也表达了数字 0

表达式可以返回任意值,它们同样能返回 0 或 非 0 值。在这层意义上,0 代表 false,非 0 代表 true 。例如,-1 是 true ,因为它是 非0。负数不是 false。

操作符的运算顺序类似于 C 语言:圆括号,乘法,除法,加法,减法。这里是一些简单的例子:

5 + 6;                   //表达了 11
5 * 6 + 3;               //表达了 33
5 * (6 + 3);             //表达了 45
5.0 + 2.3;               //表达了 7.3
(5 * 6) % 7;             //取余操作符,表达了 2
(5 + 3) / 2 * 4 - 9;     //表达了 -8

如前所述,表达式可以包含变量,乃至是函数:

int a = 5 * 6;
int b = a * 3;      //表达了 90
int c = AddTwoNumbers(a, b) + (a * b);

注意:字符串处理的例程可以在 string.inc 中找到,位置在 include 的子目录中。它们也可以通过 API Reference 浏览。

运算符

在Pawn中有一些额外的有用的运算符。第一种便是自聚表达式。 例如:

int a = 5;
 
a = a + 5;

可以写成:

int a = 5;
a += 5;

这种自聚集对于以下操作符都适用:

  • 1. 四则运算: *, /, -, +
  • 2. 位运算: |, &, ^, ~, <<, >>

另外,有一种自增,自减运算符:

a = a + 1;
a = a - 1;

可以写成:

a++;
a--;

注意:运算符 ++ 和 -- 可以写在变量的前面(先自增,先自减)或者后面(后自增,后自减)。区别在于在表达式中,剩下的部分取到此变量的值有所不同。

  • 先自增、先自减 变量在参与表达式的运算前自增/自减,表达式的剩余部分取自增/自减后变量的值。
  • 后自增、后自减 变量在参与表达式的运算后自增/自减,表达式的剩余部分取自增/自减前变量的值。

换句话说, a++ 参与表达式运算的是 a++a 参与表达式运算的是 (a + 1)。两种情况下最后 a 都自增了 1。

例如:

int a = 5;
int b = a++;   // b = 5, a = 6  (1)
int c = ++a;   // a = 7, c = 7  (2)

(1) 中 ba 自增前的值 6 (2) 中 ca 自增后的值 7.

比较运算符

有6个比较操作符用于比较两个数字的值,并且结果是 true(0) 和 false(非 0) 。

  • a == b - a、b 相同表达为 true,否则表达为 false
  • a != b - a、b 不同表达为 true,否表达为 false
  • a > b - a 大于 b 表达为 true,否则为 false
  • a >= b - a 大于或者等于 b 表达为 true,否则为 false
  • a < b - a 小于 b 表达为 true,否则为 false
  • a <= b - a 小于或者等于 b 表达为 true,否则为 false

例如:

(1 != 3);         //表达为 true
(3 + 3 == 6);     //表达为 true
(5 - 2 >= 4);     //表达为 false

注意这些运算符不能用在数组或者字符串上。

真值运算符

这些真值可以用3个布尔运算符结合。

  • a && b - a 和 b 同为 true ,结果为 true ,否则为 false (与运算符)
&& 0 1
0 0 0
1 0 1
  • a || b - a 和 b 同为 false ,结果为 false ,否则为 true (或运算符)
|| 0 1
0 0 1
1 1 1
  • !a - a 为 true ,!a 为 false , a 为 false , !a 为 true (非运算符)
! 0 1
1 0

For example:

(1 || 0);         //表达了 true
(1 && 0);         //表达了 false
(!1 || 0);        //表达了 false

左值与右值

两个重要的概念是左手边的值和右手边的值,或简称为左值和右值。左值在赋值语句的左边,右值在赋值语句的右边。例如:

int a = 5;

这个例子中 a 是一个左值 5 是一个右值

规则:

  • 表达式不可能是左值
  • 变量可以是左值也可以是右值

条件语句

条件语句允许你在符合特定条件的情况运行代码。

If 语句

if语句测试一个或多个条件。例如:

if (a == 5)
{
   /* 表达式为 true 时代码才会执行 */
}

当然也可以写成多个匹配:

if (a == 5)
{
   /* 代码 */
}
else if (a == 6)
{
   /* 代码  */
}
else if (a == 7)
{
   /* 代码 */
}

你也可以处理没有条件匹配的。例如:

if (a == 5)
{
   /* 代码 */
}
else
{
   /* 没有表达式为 true 时这里的代码才会运行 */
}

Switch 语句

Switch 语句是限制的 if 语句。Switch 用一系列可能的值来测试一个表达式。例如:

switch (a)
{
   case 5:
   {
      /* 代码 */
   }
   case 6:
   {
      /* 代码 */
   }
   case 7:
   {
      /* 代码 */
   }
   case 8, 9, 10:
   {
      /* 代码 */
   }
   default:
   {
      /* 以上 case 皆不匹配,这里的代码才会运行 */
   }
}

不像其他语言,switch 匹配后不会一直运行到最后。也就是说,多个 cases 并不会运行。当一个 case 匹配了,这个 case 的代码块运行完就会跳出 switch。

循环

循环允许在条件为真时重复执行一段代码。

for 循环

for 循环由 4 个部分组成:

  • 初始化 语句 - 在第一次循环之前运行
  • 条件 语句 - 每次循环前运行,结果为 false 则终止循环
  • 迭代 语句 - 每次虚幻后运行
  • 循环体 - 每次循环中 条件 为 true 时运行
for ( /* 初始化 */ ; /* 条件 */ ; /* 迭代 */ )
{
   /* 循环体 */
}

一个简单的例子是写一个函数计算数组的总合

int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sum = SumArray(array, 10);
 
int SumArray(const int array[], int count)
{
   int total;
 
   for (int i = 0; i < count; i++)
   {
      total += array[i];
   }
 
   return total;
}

解释:

  • int i = 0 - 新建一个变量 i 并赋值为 0
  • i < count - 仅在 i 小于 count 时执行循环。这确保了循环在适当的时机停止读取数组。在这种情况下,我们不要读取这个循环无效的索引。
  • i++ - 每次循环后 i 自增。这样保证了循环不会永远运行下去,最终会由于 i 变得太大使得循环停止。

因此,函数 SumArray 会遍历数组所有有效的索引,每次都会把值加到总合里。for 循环对于处理数组是非常常用的。

while 循环

while 循环比起 for 循环用的少一些,但它的确是最简单的循环。它有两个部分:

  • 条件 语句 - 每次循环前执行,如果为 false 终止循环。
  • 循环体 - 每次循环执行的部分。
while ( /* 条件语句 */ )
{
   /* 循环体 */
}

只要条件表达式保持为真,这个循环就会继续执行。所有的 for 循环都可以改写成 while 循环:

/* 初始化 */
while ( /* 条件语句 */ )
{
   /* 循环体 */
   /* 迭代 */
}

这是之前 for 循环重写为 while 循环的例子:

int SumArray(const int array[], int count)
{
   int total, i;
 
   while (i < count)
   {
      total += array[i];
      i++;
   }
 
   return total;
}

当然还有 do...while 循环,这是更少见得循环。它和 while 循环基本相同,除了条件的检查之外。 do...while 的条件检查是在每次循环之后,这意味着,循环体至少要被执行一次。例如:

do
{
   /* 循环体 */
}
while ( /* 条件语句 */ );

循环控制

有两种方法可以让你有选择的控制循环:

  • 跳过 一次循环,然后继续
  • 终止 循环,不再继续

比如你有一个函数,获取一个数组并且在数组内寻找匹配的数字。当你找到了这个数字,就想立刻停止循环:

/**
 * 返回匹配的数字所在的索引,没找到时返回 -1
 */
int SearchInArray(const int array[], int count, int value)
{
   int index = -1;
 
   for (int i = 0; i < count; i++)
   {
      if (array[i] == value)
      {
         index = i;
         break;
      }
   }
 
   return index;
}

的确,这个函数只是简单的 return i ,但是这个例子展示了如何用 break 来终止一个循环。

相似的,关键字 continue 会跳过循环的迭代。例如,假设我们想要计算所有素数的和:

int SumEvenNumbers(const int array[], int count)
{
   int sum;
 
   for (int i = 0; i < count; i++)
   {
      /* 如果被 2 整除余 1 ,那就是奇数 */
      if (array[i] % 2 == 1)
      {
         /* 跳过剩余的部分 */
         continue;
      }
      sum += array[i];
   }
 
   return sum;
}

作用域

作用域指的是代码的 可视性。那就是,在某个层级的代码对于另一个层级的代码是“不可见的”。例如:

int A, B, C;
 
void Function1()
{
   int B;
 
   Function2();
}
 
void Function2()
{
   int C;
}

在这个例子中, ABC 存在于 全局作用域 。他们可以被任何函数看见。但是,在 Function1 中的变量 B 不同于全局层级的 B 。它被叫做 局部作用域 ,因此也叫做 局部变量

相似的, Function1Function2 互相并不知道各自的变量。

不仅是 Function1 的私有变量,每次重新创建的时候也是如此。像这样:

void Function1()
{
   int B;
 
   Function1();
}

在这个例子中, Function1 调用它自己。当然,这是一个无限递归(不好的东西),但是重要的是每次这个函数运行,就会有产生一个新的 B 。当函数结束时, B 被销毁,相应的值也消失了。

作用域的这个特性简单来说就是变量的作用域等于它所在的代码块。同时,在全局作用域的变量对于所有函数是可见的。一个变量在局部作用域的对所有在这个代码块之下的代码可见。例如:

void Function1()
{
   int A;
 
   if (A)
   {
      A = 5;
   }
}

以上代码是合法的,因为 A 的作用域延伸到整个函数。下面的代码就是不合法的:

void Function1()
{
   int A;
 
   if (A)
   {
      int B = 5;
   }
 
   B = 5;
}

注意, B 的声明在一个新的代码块里。这意味着 B 只能在这个代码块被存取(以及在这个代码块中的子代码块)。一旦这个代码块运行结束了, B 也就不存在了。

动态数组

动态数组不需要硬编码他的大小。例如:

void Function1(int size)
{
   int array[size];
 
   /* 代码 */
}

动态数组可以用任意的表达式作为他们的大小,只要这个表达式的值大于0。就像普通数组,SourcePawn 在其创建后,并不知道它的大小,你必须保存它的大小以备不时之需。

动态数组只能在局部作用域中使用,它不能存在于全局。

扩展的变量声明

变量可以有更多种声明方式,除了用intfloatchar

静态变量

关键字 static 可以在全局作用域和局部作用域使用,不同的作用域有不同的意义。

全局静态变量

全局作用域下的静态变量只能在同一个文件中访问。例如:

//file1.inc
static float g_value1 = 0.15f;
 
//file2.inc
static float g_value2 = 0.15f;

某个文件同时 include 了这两个头文件,会无法访问 g_value1 或者 g_value2 。这可以简单的隐藏某些信息,类似于其他变成语言中的 private 属性。

局部静态变量

局部静态变量其实也是一个全局的变量,只不过它只对所在的代码块可见。例如:

int MyFunction(int inc)
{
   static int counter = -1;
 
   counter += inc;
 
   return counter;
}

在这个例子中, counter 其实是一个全局的变量(其生命周期是全局的)————它只在第一次被初始化为 -1 ,之后不再初始化,不存在函数的堆栈中。每次函数 MyFunction 执行时,变量 counter 和它在内存中存储的位置是同一个。

再看这个例子:

MyFunction(5);
MyFunction(6);
MyFunction(10);

在这个例子中, counter-1 + 5 + 6 + 1020)。它存在于函数的生命周期之外。注意,如果你在递归函数中使用静态变量可能会造成一些问题。如果你使用了递归函数,不要将静态变量作为每次递归的新变量使用。

使用局部静态变量的好处是避免过多的定义全局变量。如果你的变量不会被其他的函数使用,你可以放心的使用静态变量持久的存储在函数“内部”。

静态变量可以在任意的代码块中:

int MyFunction(int inc)
{
   if (inc > 0)
   {
      static int counter;
      return (counter += inc);
   }
   return -1;
}

翻译 Translated By: STF.Iscariot STF.xiaooloong

Warning: This template (and by extension, language format) should not be used, any pages using it should be switched to Template:Languages

View this page in:  English  Russian  简体中文(Simplified Chinese)