Zh cn:Introduction to SourcePawn 1.7
本指南将介绍最基础的 SourcePawn 语言编写方法。 Pawn 是一门“脚本”语言,被设计用来将功能嵌入到其它程序中。它不是一门独立的语言(如 C++ 或 Java ),并且它的细节会由所在应用的不同而不同。SourcePawn 是 SourceMod 模组中使用的 Pawn 版本。
本指南并不教你如何去写 SourceMod 插件,而是介绍这门语言的语法和语义的概览。想要了解 SourceMod API 的细节,请参考Introduction to SourceMod Plugins。
零基础教程
本章节针对没有编程经验的初学者。如果阅读完后仍有疑问,你可能需要学习另一门编程语言,如 PHP, Python 或者 Java,以便更好的理解编写程序的方法。
标识符/关键字
标识符是一串字母、数字、下划线的组合,用于唯一标识某些东西。标识符是大小写敏感的,通常以字母开头。 (译者注: c 语言中,标识符不能以数字开头。)
SourcePawn 中有一些保留的关键字,它们有特殊的含义。例如 if,for,return 。这些关键字是特殊的结构,会在下文介绍。他们不能用于标识符的命名。
变量
在你开始编码前,有一些重要的概念你需要了解。首先就是变量。变量是一个用来保存数据的“标识符”或“名称”。例如,变量 "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) 中 b 取 a 自增前的值 6 (2) 中 c 取 a 自增后的值 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; }
在这个例子中, A , B 和 C 存在于 全局作用域 。他们可以被任何函数看见。但是,在 Function1 中的变量 B 不同于全局层级的 B 。它被叫做 局部作用域 ,因此也叫做 局部变量 。
相似的, Function1 和 Function2 互相并不知道各自的变量。
不仅是 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 在其创建后,并不知道它的大小,你必须保存它的大小以备不时之需。
动态数组只能在局部作用域中使用,它不能存在于全局。
扩展的变量声明
变量可以有更多种声明方式,除了用int、float或char。
静态变量
关键字 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 + 10(20)。它存在于函数的生命周期之外。注意,如果你在递归函数中使用静态变量可能会造成一些问题。如果你使用了递归函数,不要将静态变量作为每次递归的新变量使用。
使用局部静态变量的好处是避免过多的定义全局变量。如果你的变量不会被其他的函数使用,你可以放心的使用静态变量持久的存储在函数“内部”。
静态变量可以在任意的代码块中:
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) |