您现在的位置是:首页 >技术教程 >FreePascal 备忘录网站首页技术教程
FreePascal 备忘录
FreePascal 备忘录
语言特点
代码不区分大小写。
名词解释
RTL:Run-Time Library
,是 Free Pascal 的运行时库,就是一些单元文件,可以在程序中使用它们。
这些单元文件的介绍:https://wiki.freepascal.org/RTL
这些单元文件的手册:https://www.freepascal.org/docs-html/current/rtl/index.html
FCL:Free Component Library
,是 Free Pascal 的组件库,也是一些单元文件,可以在程序中使用它们。
这些单元文件的介绍:https://wiki.freepascal.org/FCL
这些单元文件的手册:https://www.freepascal.org/docs-html/current/fcl/index.html
LCL:Lazarus component library
,是 Lazarus 的组件库,可以在 Lazarus 中使用它们。
这些库文件的介绍:https://wiki.freepascal.org/LCL
编译器兼容模式
{$Mode xxx}
设置编译器的兼容模式,可用的模式如下(一般使用 ObjFPC
模式):
{$MODE FPC} = fpc -MFpc // String = ShortString。p := @proc。
{$MODE ObjFPC} = fpc -S2 -MObjFpc // String = ShortString。p := @proc。
{$MODE Delphi} = fpc -Sd -MDelphi // String = AnsiString。 p := proc。
{$MODE DelphiUnicode} = fpc -MDelphiUnicode // String = UnicodeString。p := proc。
{$MODE MACPAS} = fpc -MMacPas // 更兼容 Mac OS 上常用的 Pascal 方言。
{$MODE TP} = fpc -So -MTp // String = ShortString。
{$MODE ISO} = fpc -MIso // 符合 ISO/IEC 7185 的 0 级和 1 级要求。
{$MODE ExtendedPascal} = fpc -MExtPas // 仅 3.2 及更高版本支持。参考 ISO 10206 规范。
{$MODE Default} // 恢复使用命令行中指定的模式。
lazarus IDE 默认选择 {$MODE ObjFPC}
模式,如果使用 Lazarus IDE 编译源代码,则不必在源代码中添加 {$MODE ObjFPC}
指令。但是为了使源代码在脱离 Lazarus IDE 的情况下依然能够正确编译,可以在每个单元文件的 uses
语句之前添加 {$MODE ObjFPC}
指令。
程序
program project1;
uses ...
type ...
function ...
procedure ...
resourcestring ...
const ...
var ...
begin
...
end.
program
关键字是为了向后兼容,编译器会忽略它。
单元
单元包含一组声明、过程和函数,可供程序或其他单元使用。
unit unit1;
interface
uses ...
implementation
uses ...
initialization // 可选
...
finalization // 可选
...
interface
部分声明可被其它单元访问的标识符,只能声明,不能有可执行代码(包括函数的实现)。
implementation
部分实现具体的函数。
initialization
部分在该单元被 uses
时候执行,finalization
的执行顺序与之相反。
在程序正常终止的情况下,总是会执行 finalization
部分,无论是因为程序到达终点,还是因为执行了 Halt 指令。
如果程序在执行 initialization
期间停止,则只会 finalization
已初始化的单元。
如果只有 initialization
而没有 finalization
,则可以简写为 begin...end.
。
命名空间
单元名称中可以包含点:
unit a;
interface
var
b: record
c: Integer;
end;
implementation
initialization
b.c := 3;
end.
unit a.b;
interface
var c: Integer = 1;
implementation
end.
unit b;
interface
var c: Integer = 2;
implementation
end.
program project1;
uses
a, a.b, b;
begin
WriteLn(c); // 2 b
WriteLn(b.c); // 2 b
WriteLn(a.b.c); // 1 a.b
end.
program project2;
uses
b, a.b, a;
begin
WriteLn(c); // 1 a.b
WriteLn(b.c); // 2 b
WriteLn(a.b.c); // 1 a.b
end.
program project1;
uses
a;
begin
WriteLn(b.c); // 3
WriteLn(a.b.c); // 3
end.
库
library test;
function Max(A, B: Integer): Integer; cdecl;
begin
if A > B then Exit(A) else Exit(B);
end;
procedure Print(S: String); cdecl;
begin
WriteLn(S);
end;
exports
Max, Print;
end.
// 编译:fpc -Mobjfpc -Scghi -CX -O3 -XX -olibtest.so test.pas
// 将生成的 libtest.so 拷贝到 /usr/lib 目录中
program app1;
function Max(A, B: Integer): Integer; cdecl; external 'test';
procedure Print(P: PChar); cdecl; external 'test';
procedure printf(Fmt: PChar); cdecl; varargs; external 'c';
begin
WriteLn(Max(1, 2));
Print('Hello');
printf('%d, %d'#10, 1, 2);
end.
// 编译:fpc -Mobjfpc -Scghi -CX -O3 -XX -k-lc -k-ltest -oapp1 app1.pas
关于 undefined symbol: calloc, version GLIBC_2.2.5
问题,是因为链接选项的顺序错误(-ltest
必须在最后)。重新排序可以解决问题。可以使用 -k
参数自定义链接顺序。
或者在源文件中使用 {$LinkLib xxx}
并将 {$LinkLib libc.so}
放在 {$LinkLib libtest.so}
之前:
program app2;
{$LinkLib libc.so}
{$LinkLib libtest.so}
function Max(A, B: Integer): Integer; cdecl; external;
procedure Print(P: PChar); cdecl; external;
procedure printf(Fmt: PChar); cdecl; varargs; external;
begin
WriteLn(Max(1, 2));
Print('Hello');
printf('%d, %d'#10, 1, 2);
end.
// 编译:fpc -Mobjfpc -Scghi -CX -O3 -XX -oapp2 app2.pas
另外一个指令 {$L xxx}
或 {$Link xxx}
用于链接 fpc 生成的对象文件:
// 链接 .o 文件(fpc 生成的对象文件)
{$L xxx.o}
{$Link xxx.o}
uses
uses
子句用于标识程序所需的单元。这些被 uses
的单元,它们的 interface
中声明的所有标识符都将被添加到该程序中成为已知的标识符。System
单元始终由编译器加载,因此不必 uses
。
单元出现的顺序很重要,它决定了它们的初始化顺序。单元的初始化顺序与它们在 uses
子句中的顺序相同。标识符以相反的顺序搜索,如果两个单元使用相同的标识符声明不同的类型,则优先时候最后被 uses
的标识符。
编译器将在单元搜索路径中查找 uses
子句中所有单元的编译版本或源代码版本。可以使用 in
关键字明确指定单元文件名:
uses a in '..a.pas';
还可以使用 {$ UnitPath ..}
指令指定单元搜索路径。
当编译器查找单元文件时,它会将扩展名 .ppu
添加到单元的名称中。当在 Linux 和文件名区分大小写的操作系统中查找单元时,使用以下机制:
1、以原始大小写查找
2、以全小写字母查找
3、以全大写字母查找
此外,如果单元名称的长度超过 8 个字符,编译器将首先查找具有此长度的单元名称,然后将名称截断为 8 个字符并再次查找。出于兼容性原因,在支持长文件名的平台上也是如此。
注意,上述搜索是在搜索路径中的每个目录中执行的。
program
块包含程序启动时将执行的语句。注意,这些语句不一定是程序最先执行的语句,在程序 uses
的单元中 initialization
部分的代码将在程序代码之前执行。
循环依赖
单元可以相互依赖,但至少有一个依赖项位于单元的 implementation
部分。
注释
// 行注释
{ 块注释 }
(* 块注释 *)
说明:{$mode fpc}
和 {$mode objfpc}
模式支持嵌套块注释。
标识符
标识符由字母、数字和下划线组成,不能以数字开头,最大 127 个字符。
大多数标识符(常量、变量、函数、方法、属性)都可以在其定义中附加一个提示指令:
// deprecated 表示一个标识符被弃用,信息字符串可省略
procedure F1(); deprecated '请使用 FA() 代替';
// experimental 表示此标识符是实验性的
procedure F2(); experimental;
// platform 表示此标识符是依赖于平台的
procedure F3(); platform;
// unimplemented 表示尚未实现特定功能
procedure F4(); unimplemented;
当用户使用这些标识符时,会在编译器输出中给出相应的提示信息。
可以通过使用 &
符号将保留字作为标识符使用,比如 var &if: Integer
。主要用于版本升级时保留字更改导致的旧代码兼容性。
特殊字符
以下字符具有特殊含义:
' + - * / = < > [ ] . , ( ) : ^ @ { } $ # & %
以及以下成对的字符:
<< >> ** <> >< <= >= := += -= = /= ( *) (. .) //
在范围说明符中使用时,字符对 (.
等价于左方括号 [
。同样,字符对 .)
等价于右方括号 ]
。当用于注释分隔符时,字符对 (*
相当于左大括号 {
,字符对 *)
相当于右大括号 }
。这些字符对在字符串表达式中保留其正常含义。
运算符
// 算数运算
+ - * / div mod
// 位运算
and or xor not shl shr << >>
// 逻辑运算
and or xor not = <> > >= < <=
// 赋值
:= += -= *= /=
// 集合(并、差、交、对称差、右超左、左超右、添加、排除、是否包含)
+ - * >< <= >= include exclude in += -= *=
// 指针(引用、解引用)
@ ^
// 类运算符(类型判断、类型转换)
is as
is
运算符可用于检查类是否实现接口。
as
运算符可用于将接口类型转换回类。
保留字
Turbo Pascal 保留字:
program unit interface implementation procedure function begin end
and or xor not shl shr div mod
string array set record file object
packed inline absolute
if then else case of for in to downto while repeat until with do label goto
uses type var const asm operator
constructor destructor self nil inherited
reintroduce
Object Pascal 附加保留字:
as is on
class resourcestring property
library exports out
try except finally raise
initialization finalization
dispinterface
threadvar
inline packed
修饰符(并不完全是保留字,可以用作标识符,但在特定的地方,它们对编译器具有特殊的含义,即编译器将它们视为 Pascal 语言的一部分):
private protected public published export
absolute abstract default deprecated dynamic external generic helper overload override message register virtual static winapi
alias forward
read write
name result
break continue
cdecl cppdecl stdcall safecall cvar
near far far16
assembler
bitpacked
enumerator
experimental
implements
index
interrupt
iocheck
local
nodefault
noreturn
nostackframe
oldfpccall
otherwise
pascal
platform
reintroduce
saveregisters
softfloat
specialize
stored
strict
unaligned
unimplemented
varargs
变量存储
变量是显式命名的具有特定类型的内存位置。为变量赋值时,Free Pascal 编译器会生成机器代码,将值移动到为此变量保留的内存位置。此变量的存储位置取决于声明它的位置:
1、全局变量存储在固定的内存位置,并且在程序的整个执行时间内可用。
2、局部变量存储在程序栈上,用到时入栈,用完后出栈,即不存储在固定位置。
变量的声明
普通变量:
var
// 声明变量,编译器自行管理所有内容
A: Int32;
// 声明变量,并指定初始值
B: Int32 = 1;
// absolute 指示该变量与另一个变量(D)存储在相同的位置(共用内存)
C: Int32 absolute D;
导出变量(生成库文件):
var
// cvar 指示该变量的汇编程序名称等于源码中的变量名(区分大小写)
A: Int32; cvar;
// export 指示该变量必须公开(即外部代码可以从库文件中引用该变量)
B: Int32; cvar; export;
// name 指示该变量的汇编程序名称为 DDD,如果未使用 cvar,则必须指定名称
D: Int32; export name 'DDD';
// public 是 export 的别名
E: Int32; cvar; public;
F: Int32; public name 'FFF';
导入变量(引用库文件):
var
// external 指示该变量位于外部
A: Int32; cvar; external;
// xxx 指示该变量位于外部的 libxxx.so 中
B: Int32; cvar; external 'xxx';
// 如果未使用 cvar,则必须指定名称
C: Int32; external 'xxx' name 'CCC';
注意,汇编程序名称必须是唯一的。无法声明或导出汇编程序名称相同的两个变量。特别是,不要尝试导出以 FPC_
开头的公共名称的变量;编译器的一些内部系统函数会使用此名称。
变量的初始化
全局变量仅在程序启动时初始化一次。没有初始值的全局变量,会被初始化为等效的零值。
指定了初始值的局部变量,会在每次进入过程时都进行初始化。没有指定初始值的局部变量,如果是托管类型,则使用默认值进行初始化,否则不进行初始化。
Result
标识符可用作函数的返回值,因此类似于变量。但它不是变量,而是被视为按引用传递的参数。因此不会被初始化。
Default
函数可以返回指定类型的默认初始值(这对于范型比较有用):
type
TRecord = record
I: Int32;
S: String;
end;
var
A: Int32;
B: TObject;
C: TRecord;
begin
A := Default(Int32); // 0
B := Default(TObject); // nil
C := Default(TRecord); // ( I:0; S:'' )
end.
线程变量
全局变量会被多个线程共享。
局部变量会被各个线程专用。
通过 threadvar
关键字可以使全局变量也被各个线程专用。在线程启动时候,会给每个线程创建一份 threadvar
变量的副本,并使用变量的初始值进行初始化。
如果未使用线程,则 threadvar
变量为与普通变量相同。
应谨慎使用 threadvar
,检索或设置变量值会产生开销。如果可能的话,考虑使用局部变量,它们总是比 threadvar
快。
常量声明
常量分为“指定了类型的常量”和“未指定类型的常量”:
const
A = 1;
A: Integer = 1;
未指定类型的常量相当于宏,在编译时会进行简单的宏替换,我喜欢把它叫做宏常量。
指定了类型的常量(包括局部常量)叫类型化常量,它相当于全局变量,如果指定了 {$J+}
,则该常量可修改(默认),如果指定了 {$J-}
,则该常量不可修改。
只能声明这些类型的常量:序数类型、集合类型、指针类型(只能是 nil
)、实型、字符、字符串。
常量声明中可以使用以下运算符:
+ - * / not and or div mod ord chr sizeof pi int trunc round frac odd
宏常量
下面两段代码是完全等价的:
Const
One = 1;
begin
Writeln(One);
end.
begin
Writeln(1);
end.
宏常量的类型是不固定的,使用能容纳常量值的最小有符号类型:
begin
WriteLn(SizeOf(1 )); // 1
WriteLn(SizeOf(1000 )); // 2
WriteLn(SizeOf(3.0 )); // 4
WriteLn(SizeOf(3.2 )); // 10
WriteLn(Sizeof(True )); // 1
WriteLn(Sizeof('A' )); // 1
WriteLn(Sizeof('AB' )); // 2
WriteLn(Sizeof('ABCD')); // 4
WriteLn(Low(127)); // -128
WriteLn(Low(128)); // 0
end.
类型化常量
“类型化常量”可以实现函数的闭包效果:
procedure Count();
const
{$J+}
N: Integer = 0; // 局部常量,只初始化一次
{$J-}
begin
N += 1;
WriteLn(n);
end;
begin
Count(); // 1
Count(); // 2
Count(); // 3
end.
全局属性
全局块中可以声明属性,就像声明类属性一样。
全局属性的概念特定于 Free Pascal,在 Delphi 中不存在。需要 {$mode objfpc}
模式才能使用。
unit SomeUnit;
Interface
Function GetData; Int32;
Procedure SetData(Value: Int32);
property Data: Int32 read GetData write SetData;
implementation
var
FData: Int32;
function GetData: Int32;
begin
Result := FData;
end;
Procedure SetData(Value: Int32);
begin
// 可以在这里对 Value 做一些检查 ...
FData := Value;
end;
end.
可以将 Get
和 Set
方法隐藏在另一个单元中:
前端:
unit SomeUnit;
Interface
uses
BackUnit;
property Data: Int32 read GetData write SetData;
implementation
end.
后端:
unit BackUnit;
Interface
Function GetData; Int32;
Procedure SetData(Value: Int32);
implementation
var
FData: Int32;
function GetData: Int32;
begin
Result := FData;
end;
Procedure SetData(Value: Int32);
begin
FData := Value;
end;
end.
序数类型
除浮点类型外,所有基本类型都是序数类型(整数、布尔、枚举、子界、字符)。序数类型是可数的、有序的、有限的,接受范围检查。
Int64
和 QWord
在 64 位 CPU 上被视为序数类型。在 32 位 CPU 上,它们具有序数的一些特征,但它们不能用于例如 for 循环中。
布尔字面量
true
false
布尔类型
Boolean // 1 字节,Ord(True) = 1
Boolean16 // 2 字节,Ord(True) = 1
Boolean32 // 4 字节,Ord(True) = 1
Boolean64 // 8 字节,Ord(True) = 1
ByteBool // 1 字节,Ord(True) = not(0)
WordBool // 2 字节,Ord(True) = not(0)
LongBool // 4 字节,Ord(True) = not(0)
QWordBool // 8 字节,Ord(True) = not(0)
类型转换
整数转布尔时,0 为 False,非 0 为 True。
布尔转整数时,False 为 0,True 根据布尔类型而定:
Boolean、Boolean16、Boolean32、Boolean64
会将 True 转换为 1
ByteBool、WordBool、LongBool、QWordBool
会将 True 转换为 not(0)
not(0)
就是对 0 取反,有符号结果为 -1,无符号结果为该类型最大值。
begin
// Boolean 系列
WriteLn(Int32 (Boolean (True))); // 1
WriteLn(UInt32(Boolean (True))); // 1
// ByteBool 系列
WriteLn(Int32 (ByteBool(True))); // -1
WriteLn(UInt32(ByteBool(True))); // 4294967295
end.
布尔表达式的逻辑运算符 and、or、xor、not
遵循短路原则,当结果可以确定时,不再继续向后求值。此行为可由 {$B}
编译器指令更改。
枚举类型
枚举类型就是给整数赋予了名字。
type
// := 可以换成 =
TWeekDay = (Mon := 1, Tue, Wed, Thu, Fri, Sat, Sun);
// 可以使用 {$ScopedEnums on} 开启枚举的作用域
{$ScopedEnums On}
Sign = (Negative = -1, None = 0, Positive = 1);
begin
WriteLn(Mon); // Mon
WriteLn(Sign.Negative); // Negative
end.
如果对多个元素使用赋值语法,则元素之间必须按升序排列,不能颠倒顺序。
可以使用 Ord()
获取枚举值所对应的整数。
可以使用 Pred()
和 Succ()
跳转到前一个或后一个枚举值,但是对于枚举定义中使用了赋值语法的情况,则不能使用这两个函数。
枚举大小
可以使用 {$PackEnum n}
修改枚举类型所占用的字节空间,如果设置为 Default
则恢复默认值。在 {$mode TP}
中,默认值为 1,其他模式下为 4。如此“大”的最小尺寸有助于快速读写访问:
type
TWeekDay1 = (Mon1, Tues1, Wed1, Thu1, Fri1, Sat1, Sun1);
{$PackEnum 1}
TWeekDay2 = (Mon2, Tues2, Wed2, Thu2, Fri2, Sat2, Sun2);
TWeekDay3 = (Mon3, Tues3, Wed3, Thu3, Fri3, Sat3, Sun3=32767);
{$PackEnum Default}
TWeekDay4 = (Mon4, Tues4, Wed4, Thu4, Fri4, Sat4, Sun4);
begin
WriteLn(SizeOf(TWeekDay1)); // 4
WriteLn(SizeOf(TWeekDay2)); // 1
WriteLn(SizeOf(TWeekDay3)); // 2
WriteLn(SizeOf(TWeekDay4)); // 4
end.
格式说明
右侧成员都小于左侧成员的枚举称为升序枚举。非升序枚举在编译时会给出提醒。
相邻成员之间的值相差为 1 的枚举称为连续枚举。对非连续枚举使用集合构造函数可能会产生意外结果。
升序且连续的枚举,且首元素为零,或者在广义上(由 FPC 实现)小于等于零,则枚举类型为正常。编译器内在类型信息仅适用于这些正常的枚举类型。
将枚举值转换为字符串
type
TWeekDay = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
var
S: String;
begin
WriteStr(S, Mon);
WriteLn(S); // Mon
end,
整数字面量
09999 // 十进制,允许以 0 开头
$FFFF // 十六进制
&7777 // 八进制
%1111 // 二进制
{$mode TP}
或 {$mode Delphi}
模式不支持八进制和二进制表示法。
整数类型
ShortInt // 有符号,1 字节,8 位
Byte // 无符号,1 字节,8 位
SmallInt // 有符号,2 字节,16 位
Word // 无符号,2 字节,16 位
LongInt // 有符号,4 字节,32 位
LongWord // 无符号,4 字节,32 位
Int64 // 有符号,8 字节,64 位
QWord // 无符号,8 字节,64 位
Integer // SmallInt 或 LongInt
Cardinal // LongWord
Integer
类型在默认的 FPC
模式中是 SmallInt
的别名。在 Delphi
或 ObjFpc
模式中是 LongInt
的别名。
类型别名
在 System 单元中有一些更好记的类型别名:
type
Int8 = ShortInt;
Int16 = SmallInt;
Int32 = Longint;
//Int64 = Int64;
IntPtr = PtrInt; // 本机整数大小
UInt8 = Byte;
UInt16 = Word;
UInt32 = Cardinal;
UInt64 = QWord;
UIntPtr = PtrUInt; // 本机整数大小
取值范围
begin
WriteLn(Low(Int8 ), '..', High(Int8 ));
WriteLn(Low(UInt8 ), '..', High(UInt8 ));
WriteLn(Low(Int16 ), '..', High(Int16 ));
WriteLn(Low(UInt16 ), '..', High(UInt16 ));
WriteLn(Low(Int32 ), '..', High(Int32 ));
WriteLn(Low(UInt32 ), '..', High(UInt32 ));
WriteLn(Low(Int64 ), '..', High(Int64 ));
WriteLn(Low(UInt64 ), '..', High(UInt64 ));
WriteLn(Low(IntPtr ), '..', High(IntPtr ));
WriteLn(Low(UIntPtr), '..', High(UIntPtr));
end.
子界类型
子界类型是对序数类型的范围限定,超出范围会引发异常。一些预定义的整数类型就是子界类型,比如:
type
Byte = 0..255;
ShortInt = -128..127;
Word = 0..65535;
SmallInt = -32768..32767;
可以通过枚举类型生成子界类型:
type
TWeekDay = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
TWorkDay = Mon .. Fri;
var
Day: TWorkDay;
begin
for Day in TWorkDay do
WriteLn(Day); // Mon Tue Wed Thu Fri
end.
字符字面量
字符和字符串都使用单引号包裹,不同之处在于内容的长度。
'A'
'''' // 引号内连续的两个单引号被解释为一个单引号字符
#10 // 转义字符
字符类型
字符类型也是序数类型。有 2 种字符类型:
AnsiChar // 单字节字符。字符的解释取决于代码页。
WideChar // 双字节字符。字符的解释取决于代码页。
类型别名
Char // AnsiChar 的别名
UnicodeChar // WideChar 的别名
宽字符赋值
给宽字符赋值时,如果值超出 ASCII 范围,则不能使用单引号方式,必须使用转义字符:
var
A: WideChar = 'A';
B: WideChar = #22909; // 字符“好”的 Unicode 编码(十进制)
C: WideChar = #$597D; // 字符“好”的 Unicode 编码(十六进制)
浮点字面量
3.2 // 浮点数(精度根据需求而定,3.0 和 3.2 有不同的精度)
3.2e3 // 科学计数法(精度根据需求而定)
浮点类型
Single // 单精度 ,1.5E-45 .. 3.4E38 ,4 字节
Double // 双精度 ,5.0E-324 .. 1.7E308 ,8 字节
Extended // 扩展精度,1.9E-4932 .. 1.1E4932,10 字节
Comp // 实际上是一个 64 位整数,-2E64+1 .. 2E63-1,8 字节
Currency // 定点实数,-922337203685477.5808 .. 922337203685477.5807,8 字节
Real // 平台相关类型
Free Pascal 使用数学协处理器(或仿真)进行所有浮点计算。
Real
类型取决于处理器,它可以是 Single
或 Double
,仅支持 IEEE 浮点类型,这些类型取决于目标处理器和仿真选项。并非所有浮点类型都适用于所有平台。
Single
类型是唯一保证在所有支持浮点数的平台上都可用的类型。
Double
类型适用于所有带协处理器的平台,
Extended
类型适用于所有 Intel x86 处理器,Windows 64 位平台除外。
Comp
类型实际上是一个 64 位整数,并非在所有目标平台上都可用。
Currency
货币类型是定点实数数据类型,内部使用 64 位整数类型(自动缩放系数 10000),这样可以最大限度地减少舍入误差。应谨慎使用此类型:在使用例如乘法的表达式中使用时,如果中间结果超出货币范围,则表达式的计算可能会出错(丢失精度)。
获取浮点数取值范围:
var
L: LongWord;
S: Single absolute L; // 与 L 共享内存
Q: QWord;
D: Double absolute Q; // 与 Q 共享内存
begin
L := %10000000000000000000000000000001; // 最小 Single
WriteLn(S);
L := %01111111011111111111111111111111; // 最大 Single
WriteLn(S);
Q := %1000000000000000000000000000000000000000000000000000000000000001; // 最小 Double
WriteLn(D);
Q := %0111111111101111111111111111111111111111111111111111111111111111; // 最大 Double
WriteLn(D);
end.
字符串字面量
'' // 空字符串
'abc' // 字符串
'abc'#9'def' // 字符串和转义字符可直接连接
'abc' + 'def' // 字符串之间用加号连接
'abc''def' // 字符串中的连续两个单引号被解析为一个单引号
字符串类型
字符串有很多种,但平时常用的只有 AnsiString
一种,其它可以简单了解。
// 短字符串,最长 255 个字节(索引 0 存储长度值),始终使用系统代码页。
ShortString
// 长字符串,长度无限制,可指定代码页,有引用计数,保证以 #0 结尾(方便外部 C 函数调用)。
// 如果未指定代码页,则使用 CP_ACP 作为其代码页,该代码页会根据不同的系统使用不同的编码方式解析字符串。
// 不同代码页的字符串之间相互赋值时,会自动进行编码转换,这种自动转换会严重降低代码速度,注意限制在最低限度。
// 如果指定了无效的代码页,则不会自动进行编码转换。
AnsiString
// 指定代码页的方法(定义类型)
type
U8Str = type AnsiString(CP_UTF8);
WinStr = type AnsiString(CP_936 );
CmdStr = type AnsiString(CP_OEM ); // Windows 系统中 cmd 控制台使用的代码页
// 当指定 {$H-} 时 String 为 ShortString
// 当指定 {$H+} 时 String 为 AnsiString
String
// 如果指定了长度信息,则 String 为 ShortString,不受 {$H} 影响
String[32]
// 如果指定了代码页,则 String 为 AnsiString ,不受 {$H} 影响
type
U8Str = type String(CP_UTF8);
如果连接两个 ShortString
,则生成的字符串也是 ShortString
。因此,可能会发生截断:不会自动升级到 AnsiString
。
一些有用的函数:
// 获取和设置字符串长度
Length()
SetLength()
// 获取字符串的引用计数,常量的引用计数始终为 -1。
StringRefCount()
// 确保字符串引用计数为 1。也就是说,如果有两个字符串变量指向同一块内存区域,
// 则可以使用该函数将内存区域拷贝出一个副本给某个字符串变量单独使用。
UniqueString()
// 修改字符串的代码页标记,同时会将字符串的数据格式转换为目标代码页。
SetCodePage()
// 获取字符串的代码页
StringCodePage()
SetLength()
会将字符串的引用计数设置为 1,相当于调用了 UniqueString()
,无论字符串的长度实际有没有发生变化。
引用计数测试:
const
S: String = 'Hello World!';
var
S1, S2, S3: String;
begin
S1 := S; S2 := S; S3 := S;
WriteLn(StringRefCount(S1)); // -1
WriteLn(StringRefCount(S2)); // -1
WriteLn(StringRefCount(S3)); // -1
UniqueString(S3);
WriteLn(StringRefCount(S1)); // -1
WriteLn(StringRefCount(S2)); // -1
WriteLn(StringRefCount(S3)); // 1
S1 := S3; S2 := S3;
WriteLn(StringRefCount(S1)); // 3
WriteLn(StringRefCount(S2)); // 3
WriteLn(StringRefCount(S3)); // 3
SetLength(S3, Length(S3));
WriteLn(StringRefCount(S1)); // 2
WriteLn(StringRefCount(S2)); // 2
WriteLn(StringRefCount(S3)); // 1
end.
AnsiString
使用了“写时拷贝”技术,当把一个字符串的内容赋值给另一个字符串时(在不产生编码转换的情况下),只是复制了指针,没有复制内存数据,当修改其中任何一个字符串时,便会生成副本,不会影响另一个字符串的内容。但通过 PChar
指针修改字符串内容时,不会生成副本。
字符串的索引是从 1 开始的,AnsiString
按字节索引,UnicodeString
按双字节索引。除此以外,其它数据类型(比如数组和指针)的索引都是从 0 开始的,所以人们一般不使用字符串索引,因为它会造成混乱。一般都使用 PChar
或 PUnicodeString
指针来索引字符串。
UnicodeString
只支持双字节字符,必须为程序添加“宽字符串管理器”才能正确使用 UnicodeString
,对于 Linux 而言,只需要 uses cwstring
单元即可,cwstring
单元会链接到 C 库并利用 C 库实现宽字符转换支持。或者使用 FPWideString
单元中包含的一个由 Object Pascal 本地实现的 UnicodeString
管理器。由于 UnicodeString
支持的字符集有限,一般不使用该类型,在 Windows 中可能会用到,因为 Windows API 的宽字符版本会用它作为参数类型。
RawByteString
是 AnsiString(CP_NONE)
类型,即没有与之关联的代码页信息,所以任何字符串与 RawByteString
进行相互赋值,都不会产生编码转换。
UTF8String
是 AnsiString(CP_UTF8)
类型。如果要遍历 UTF8 字符串中的字符,可以使用 lazUTF8
单元中提供的各种函数。
字符串的代码页与字符串实际存储的内容不一定匹配,使用时要注意,避免出现乱码。
Windows API 的宽字符版本支持 Unicode。通常将 UnicodeString
与该版本的 Windows API 一起使用。
WideString
仅在 COM/OLE
编程中需要,没有引用计数,其中操作系统负责内存管理。在 Windows 中,WideString
是使用特殊的 Windows 函数分配的,所以内存布局与 UnicodeString
不同。例如,长度以字节为单位而不是双字节。
在非 Windows 系统中,WideString
就是 UnicodeString
。
关于字符串乱码
如果指定了 {$codepage xxx}
,则源文件中的字符串字面量将以 xxx
编码进行解析,并将解析结果以 UTF16
格式存储在程序的二进制文件中,并将其标记为 CP_UTF16
代码页。
如果未指定 {$codepage xxx}
,则源文件中的字符串字面量将以字节流的形式原封不动的存储在程序的二进制文件中,并将其标记为 CP_ACP
代码页(系统代码页)。
Lazarus(不是 Free Pascal)在所有平台上都将源代码文件存储为 UTF8 格式,并且要求不要使用 {$codepage xxx}
,这样源文件中的字符串字面量就会以 UTF8 格式原封不动的存储在程序的二进制文件中,并被标记为 CP_ACP
代码页(我猜测编译器不会对 CP_ACP
代码页的源文件进行任何编码解析处理,所以会原封不动的存储其中的字符串数据)。
对于使用非 ASCII 字符的用户(而且系统默认代码页不是 UTF8 的情况,比如 Windows),需要在 uses
部分添加 lazUTF8
单元(它必须在接近开头的位置,就在关键的内存管理器和线程内容之后,例如 cmem、heaptrc、cthreads
之后),该单元会在编译好的程序启动初期,修改系统的默认代码页为 UTF8
(即使在 Windows 系统中),其实就是修改了 CP_ACP
的解释方式,本来 CP_ACP
是根据不同的操作系统选择不同的编码方式来解析字符串,被修改为 UTF8
后,所有系统中的 CP_ACP
字符串都会以 UTF8
编码方式进行解析,所以存储在二进制文件中的 UTF8
字符串就会被正确解析(因为该字符串被标记为 CP_ACP
)。由于 AnsiString
默认就是 CP_ACP
代码页,所以所有 AnsiString
都会以 UTF8
编码进行数据交换。也就是说,在整个程序的内部,都是以 UTF8
编码在处理数据。当需要将数据传递给宽字符版本的 Windows API 函数时,由于 Windows API 的参数类型是 UnicodeString
,不同类型的字符串之间会自动进行编码转换,所以 AnsiString
会被正确的转换为 UnicodeString
传递给 Windows API 使用,不会产生乱码。
Free Pascal 在程序运行过程中,会根据字符串的代码页动态解析其数据。在字符串之间相互赋值时,是否会发生编码转换,主要取决于字符串所指定的代码页是否相同,比如上面说的 CP_ACP
代码页实际存储的是 UTF8
字符串,但是如果将该字符串赋值给 UTF8String
时,就会发生编码转换,因为 CP_ACP
与 CP_UTF8
不是同一个值,这就导致原本正确的 UTF8
字符串,被再一次进行 UTF8 编码,从而产生了乱码。所以,如果你要在程序中使用 UTF8
字符串,那么直接使用 String
类型配合 lazUTF8
单元就可以了,因为在这种情况下,String
类型始终是以 UTF8
编码解释的,不要使用 UTF8String
类型,它会导致乱码。也不要使用 {$codepage xxx}
,它会让字符串字面量不再是 CP_CAP
代码页,而是 CP_UTF16
代码页,从而出现解析错误。
对于在 Lazarus 中使用非 ASCII 字符的情况,遵循以下原则就不会出现乱码:
1、永远使用 String
类型,不要使用 UTF8String
或 UnicodeString
。
2、将字符串字面量赋值给指定了 String
类型的常量或变量。
在 Free Pascal 中,未指定类型的 const
相当于宏值,会在编译期间进行宏替换,而指定了类型的 const
才是真正意义上的常量,会有自己的存储空间。
下面是避免乱码的例子:
program project1;
{$mode objfpc}{$H+} // 开启 {$H+}
uses
// 在所有系统中,如果要使用 UnicodeString,则需要加载 lazUTF8 单元。
// 在 Windows 系统中,如果要使用非 ASCII 字符,则必须加载该单元,
// 因为它会修改系统的默认代码页为 UTF8。
lazUTF8;
const
S1 = '你好'; // 没有指定类型,可能会乱码
S2: String = '你好'; // 指定了 String 类型,不会乱码
var
S3: String;
begin
S3 := '你好'; // S3 在定义时指定了 String 类型,不会乱码
WriteLn(S1); // S1 没有指定类型,可能会乱码(在 Windows 中会)
WriteLn(S2); // S2 指定了 String 类型,不会乱码
WriteLn(S3); // S3 指定了 String 类型,不会乱码
WriteLn('你好'); // 字面量没有指定 String 类型,可能会乱码(在 Windows 中会)
end.
直接使用 WriteLn('你好')
的话,字符串字面量是被当作 ShortString
处理的,这种字符串在转换为某些代码页时会出错。
字符指针
PChar
相当于 C 语言中的字符串指针,可以执行加减操作来移动指针位置,或者求两个指针之间的距离。
String
变量本身也是一个指针,PChar
变量所指向的地址就是 String
变量所指向的地址,但是它们的索引方式不同,String
是从 1 开始索引的,而 PChar
是从 0 开始索引的:
var
A: String;
B: PChar;
begin
A := 'Hello World!';
B := PChar(A);
WriteLn(UIntPtr(A)); // 4358168
WriteLn(UIntPtr(B)); // 4358168
WriteLn(A[1]); // H
WriteLn(B[0]); // H
end.
空的 AnsiString
变量会被设置为 nil
,当转换为 PChar
时,会分配一个固定的常量地址,但转换为 Pointer
时不会分配地址:
var
S: String; // String 是托管类型,当未赋值时,保证为 nil 值
begin
WriteLn(PtrUInt(S)); // 0
WriteLn(PtrUInt(PChar(S))); // 4403536 常量地址,所有空字符串常量都指向该地址
WriteLn(PtrUInt(Pointer(S))); // 0
end.
要连接两个 PChar
字符串,需要使用 Strings
单元中的函数,或者借助于空字符串:
var
A: String;
B: PChar;
begin
A := 'Hello World!';
B := PChar(A);
WriteLn(B); // Hello World!
WriteLn(A + B); // Hello World!Hello World!
WriteLn(B + A); // Hello World!Hello World!
//WriteLn(B + B); // 错误
WriteLn(B + '' + B); // Hello World!Hello World!
end.
将字符串转换为 PChar
类型时,不会修改引用计数,所以 PChar
并不持有字符串。
字符串内存占用
字符串类型 栈大小 堆大小
Shortstring Length + 1 0
Ansistring SizeOf(Pointer) Length + 1 + HS
Widestring SizeOf(Pointer) (Length + 1) * 2 + WHS
UnicodeString SizeOf(Pointer) (Length + 1) * 2 + UHS
PChar SizeOf(Pointer) Length + 1
HS (AnsiString 头部大小)从 Free Pascal 2.7.1 开始为 16 字节。
UHS(UnicodeString 头部大小)为 8 字节。
WHS(WideString 头部大小)在 Windows 中是 4 字节。其他平台等于 UHS。
这些字符串都是以 #0 结尾,所以长度要加 1。
资源字符串
resourcestring
FileMenu = '&File...';
EditMenu = '&Edit...';
自动生成 po 文件
在“工程选项”中找到“国际化”,勾选“启用 i18n(国际化)”,将“PO 输出目录”设置为当前目录 ./
,根据需要勾选更新 PO 文件的方式,然后在源文件中使用 resourcestring
,正常编译即可生成相应的 .po 文件。
手动生成 po 文件
在编译包含 resourcestring
的单元时,编译器会为其生成一个 .rsj
文件(与编译器生成的 .o
文件在同一个目录中)。此文件是一个 UTF8 编码的 JSON
文件。
收集所有 .rsj
文件并将它们合并为单个 .rsj
文件。
调用 Free Pascal 提供的 rstconv
程序将合并后的 .rsj
文件转换为 .po
文件:
rstconv -i project1.rsj -o lang.zh_CN.po
转换为 mo 文件
将 .po
文件翻译成需要的语言(可以直接打开编辑,也可以使用 poedit
进行编辑)
调用 msgfmt
程序将 .po
文件格式化为 .mo
文件:
msgfmg lang.zh_CN.po -o lang.zh_CN.mo
说明:msgfmt
属于 gettext
软件包,如果没有 msgfmt
程序,则需要安装 gettext
软件包。
使用 mo 文件
在源代码中,需要在程序启动时(或在需要翻译资源字符串时),调用 gettext
单元中的 TranslateResourceStrings
方法,为其指定 .mo
文件的位置,例如:
// 参数中的 %s 将被替换为 LANG 环境变量的内容,也可以不使用 %s。
TranslateResourceStrings('lang/res.%s.mo');
然后就可以像使用普通字符串常量一样使用翻译后的资源字符串。
示例
lang.pas
uses
gettext;
resourcestring
S = 'Hello';
begin
WriteLn(S); // 翻译前
// 会忽略不存在的 mo 文件
TranslateResourceStrings('./lang.mo');
WriteLn(S); // 翻译后
end.
编译后,生成的 po 文件如下(中文“你好”是手动输入的内容):
lang.po
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: lang.s
msgid "Hello"
msgstr "你好"
执行下面的命令,然后再执行自己的程序即可看到效果:
msgfmg lang.po -o lang.mo
窗体中的字符串
如果应用程序含有窗体,则可以在窗体的单元文件中 uses LCLTranslator
,这样窗体中的字符串也会被收集到 .po 文件中。
如果要排除某个控件,使其不被翻译,可以在“工程选项”的“国际化”中,在“排除标识符”里面填写要排除的标识符,比如 form1.edit1.text
或之前示例中的 S
,如果要排除某一类原文,可以在“排除原文”中填写,比如 Hello
,则所有原文为 Hello
的资源字符串都会被排除。
切换窗体语言也很简单,将 .mo
文件命名为 程序名称.zh_CN.mo
格式,与程序文件放在一起,然后在代码中执行 SetDefaultLang('zh_CN');
即可。
SetDefaultLang
的原型如下:
function SetDefaultLang(Lang: String; Dir: String = ''; LocaleFileName: String = ''; ForceUpdate: Boolean = True): String;
参数:
Lang:要设置的语言(如 zh、zh_CN、zh_CN.UTF8),一般使用标准语言代码,也可以使用任意字符串,只要和 .mo
文件名中的语言代码部分匹配就可以了。如果留空则表示使用环境变量 LANG 所指定的语言。
Dir:指定翻译文件所在的目录(例如 ‘lang’ 表示程序所在目录中的 lang 目录),如果留空则表示先在程序目录中搜索,如果找不到,则在系统预定义的目录(比如 /usr/share/locale
)中搜索。
LocaleFileName:自定义翻译文件名,留空则表示与可执行文件同名。
ForceUpdate:True 表示强制立即更新接口。仅当从单元 initialization
部分调用时,才应设置为 False。用户代码通常不应该设置该参数。
可以将下面的代码放到窗体单元(或其它任意单元)的 initialization
部分,这样就可以在程序启动时根据系统 LANG
环境变量自动选择语言,也可以通过 --lang zh_CN
或 -l zh_CN
命令行参数设置语言,还可以在程序中根据需要随时调用 SetDefaultLang
手动设置语言。
SetDefaultLang('', '', '', False);
更多信息
在 objpas
单元中提供了操作资源文件的相关函数,可以对资源字符串进行更精细的控制。对于 {$mode delphi}
和 {$mode objfpc}
模式而言,该单元会自动加载。相关函数如下:
// 回调函数类型,用户根据此类型编写回调函数
TResourceIterator = Function (Name, Value: AnsiString; Hash: Longint; Arg: Pointer) : AnsiString;
// 传入回调函数,使用回调函数依次处理每个资源字符串,将回调函数的返回值作为资源字符串的内容
Procedure SetResourceStrings (SetFunction: TResourceIterator; Arg: Pointer);
// 传入回调函数,使用回调函数依次处理 UnitName 单元中的每个资源字符串,将回调函数的返回值作为资源字符串的内容
Procedure SetUnitResourceStrings (UnitName: String; SetFunction: TResourceIterator; Arg: Pointer);
// 计算字符串的 Hash 值
Function Hash(S: AnsiString): longint;
// 重置资源字符串,恢复翻译之前的默认内容
Procedure ResetResourceTables;
// 清空资源字符串(将它们设为空字符串)
Procedure FinalizeResourceTables;
原则上,可以在程序运行时随时调用 TranslateResourceStrings
翻译所有资源字符串。但是,如果将资源字符串赋值给变量(或数组元素),则该变量的内容不会随着资源字符串的更新而更新,只有赋值给常量才会自动更新:
program project1;
uses
gettext;
resourcestring
Help = 'Help Message'; // 资源字符串
var
A: String = Help; // 字符串变量
const
B: String = Help; // 字符串常量
begin
WriteLn(Help); // 未翻译
WriteLn(A); // 未翻译
WriteLn(B); // 未翻译
TranslateResourceStrings('lang/lang.%s.mo'); // 开始翻译
// 此后需要重新执行 A := Help; 才能使 A 与 Help 保持一致
WriteLn(Help); // 已翻译
WriteLn(A); // 未翻译
WriteLn(B); // 已翻译
end.
如果在“资源字符串”中使用加号运算符连接之前声明的“资源字符串”,则连接之后的“资源字符串”也不会被更新。
program project1;
uses
gettext;
resourcestring
Part1 = 'Hello';
Part2 = 'World';
Sentence = Part1 + ' ' + Part2;
begin
TranslateResourceStrings('lang/lang.%s.mo'); // 开始翻译
WriteLn(Part1 ); // 已翻译
WriteLn(Part2 ); // 已翻译
WriteLn(Sentence); // 未翻译
end.
区域设置
区域设置用来设置日期和时间的格式、月份和日期名称、用作小数点或千位分隔符的字符等。
在 Lazarus 中,TFormatSettings 结构体收集所有可能的数据。默认设置由“标准格式转换函数”使用,例如 StrToDate、DateToStr、StrToFloat 或 FloatToSt 等。
对于 Windows,在 SysUtils 单元中存在一个 GetLocaleFormatSettings 函数,它根据给定 Locale 设置默认的 TFormatSettings。
GetLocaleFormatSettings(2520, DefaultFormatSettings);
参数 LCID 指定 Windows 中的语言代码,可以在后面的“语言代码表”中查找语言代码(该表来自:https://wiki.freepascal.org/Language_Codes)。
语言代码表
区域 语言代码 LCID LCID 代码页
南非荷兰语 af af 1078 1252
阿尔巴尼亚语 sq sq 1052 1250
阿姆哈拉语 am am 1118
阿拉伯语 - 阿尔及利亚 ar ar-dz 5121 1256
阿拉伯语 - 巴林 ar ar-bh 15361 1256
阿拉伯语 - 埃及 ar ar-eg 3073 1256
阿拉伯语 - 伊拉克 ar ar-iq 2049 1256
阿拉伯语 - 约旦 ar ar-jo 11265 1256
阿拉伯语 - 科威特 ar ar-kw 13313 1256
阿拉伯语 - 黎巴嫩 ar ar-lb 12289 1256
阿拉伯语 - 利比亚 ar ar-ly 4097 1256
阿拉伯语 - 摩洛哥 ar ar-ma 6145 1256
阿拉伯语 - 阿曼 ar ar-om 8193 1256
阿拉伯语 - 卡塔尔 ar ar-qa 16385 1256
阿拉伯语 - 沙特阿拉伯 ar ar-sa 1025 1256
阿拉伯语 - 叙利亚 ar ar-sy 10241 1256
阿拉伯语 - 突尼斯 ar ar-tn 7169 1256
阿拉伯语 - 阿拉伯联合酋长国 ar ar-ae 14337 1256
阿拉伯语 - 也门 ar ar-ye 9217 1256
亚美尼亚语 hy hy 1067
阿萨姆语 as as 1101
阿塞拜疆语 - 西里尔文 az az-az 2092 1251
阿塞拜疆语 - 拉丁语 az az-az 1068 1254
巴士克语 eu eu 1069 1252
比利时语 be be 1059 1251
孟加拉语 - 孟加拉国 bn bn 2117
孟加拉语 - 印度 bn bn 1093
波斯尼亚语 bs bs 5146
保加利亚语 bg bg 1026 1251
缅甸语 my my 1109
加泰隆语 ca ca 1027 1252
中文 - 中国 zh zh-cn 2052
中文 - 香港特别行政区 zh zh-hk 3076
中文 - 澳门特别行政区 zh zh-mo 5124
中文 - 新加坡 zh zh-sg 4100
中文 - 台湾 zh zh-tw 1028
克罗地亚语 hr hr 1050 1250
捷克语 cs cs 1029 1250
丹麦语 da da 1030 1252
迪维希;迪维希;马尔代夫 dv dv 1125
荷兰语 - 比利时 nl nl-be 2067 1252
荷兰语 - 荷兰 nl nl-nl 1043 1252
Edo 1126
英语 - 澳大利亚 en en-au 3081 1252
英语 - 伯利兹 en en-bz 10249 1252
英语 - 加拿大 en en-ca 4105 1252
英语 - 加勒比海 en en-cb 9225 1252
英语 - 英国 en en-gb 2057 1252
英语 - 印度 en en-in 16393
英语 - 爱尔兰 en en-ie 6153 1252
英语 - 牙买加 en en-jm 8201 1252
英语 - 新西兰 en en-nz 5129 1252
英语 - 菲律宾 en en-ph 13321 1252
英语 - 南部非洲 en en-za 7177 1252
英语 - 特立尼达 en en-tt 11273 1252
英语 - 美国 en en-us 1033 1252
英语 - 津巴布韦 en 12297 1252
爱沙尼亚语 et et 1061 1257
法罗语 fo fo 1080 1252
波斯语 - 波斯语 fa fa 1065 1256
菲律宾语 1124
芬兰语 fi fi 1035 1252
法语 - 比利时 fr fr-be 2060 1252
法语 - 喀麦隆 fr 11276
法语 - 加拿大 fr fr-ca 3084 1252
法语 - 刚果 fr 9228
法语 - 科特迪瓦 fr 12300
法语 - 法国 fr fr-fr 1036 1252
法语 - 卢森堡 fr fr-lu 5132 1252
法语 - 马里 fr 13324
法语 - 摩纳哥 fr 6156 1252
法语 - 摩洛哥 fr 14348
法语 - 塞内加尔 fr 10252
法语 - 瑞士 fr fr-ch 4108 1252
法语 - 西印度群岛 fr 7180
弗里斯兰语 - 荷兰 1122
马其顿共和国 mk mk 1071 1251
盖尔语 - 爱尔兰 gd gd-ie 2108
盖尔语 - 苏格兰 gd gd 1084
加利西亚语 gl 1110 1252
乔治亚语 ka 1079
德语 - 奥地利 de de-at 3079 1252
德语 - 德国 de de-de 1031 1252
德语 - 列支敦士登 de de-li 5127 1252
德语 - 卢森堡 de de-lu 4103 1252
德语 - 瑞士 de de-ch 2055 1252
希腊语 el el 1032 1253
瓜拉尼 - 巴拉圭 gn gn 1140
古吉拉特语 gu gu 1095
希伯来语 he he 1037 1255
HID(人机接口设备) 1279
印地语 hi hi 1081
匈牙利语 hu hu 1038 1250
冰岛语 is is 1039 1252
伊博语 - 尼日利亚 1136
印度尼西亚语 id id 1057 1252
意大利语 - 意大利 it it-it 1040 1252
意大利语 - 瑞士 it it-ch 2064 1252
日语 ja ja 1041
卡纳拉语 kn kn 1099
克什米尔 ks ks 1120
哈萨克语 kk kk 1087 1251
高棉语 km km 1107
孔卡尼语 1111
朝鲜语 ko ko 1042
吉尔吉斯语 - 西里尔文 1088 1251
老挝语 lo lo 1108
拉丁语 la la 1142
拉脱维亚语 lv lv 1062 1257
立陶宛语 lt lt 1063 1257
马来语 - 文莱 ms ms-bn 2110
马来语 - 马来西亚 ms ms-my 1086
马拉雅拉姆语 ml ml 1100
马耳他语 mt mt 1082
曼尼普尔语 1112
毛利语 mi mi 1153
马拉地语 mr mr 1102
蒙古语 mn mn 2128
蒙古语 mn mn 1104 1251
尼泊尔语 ne ne 1121
挪威语 - 博克马尔语 nb no-no 1044 1252
挪威语 - 尼诺斯克语 nn no-no 2068 1252
奥里亚语 or or 1096
波兰语 pl pl 1045 1250
葡萄牙语 - 巴西 pt pt-br 1046 1252
葡萄牙语 - 葡萄牙 pt pt-pt 2070 1252
旁遮普语 pa pa 1094
雷托浪漫 rm rm 1047
罗马尼亚语 - 摩尔多瓦 ro ro-mo 2072
罗马尼亚语 - 罗马尼亚 ro ro 1048 1250
俄语 ru ru 1049 1251
俄语 - 摩尔多瓦 ru ru-mo 2073
萨米拉普兰语 1083
梵文 sa sa 1103
塞尔维亚语 - 西里尔文 sr sr-sp 3098 1251
塞尔维亚语 - 拉丁语 sr sr-sp 2074 1250
塞索托语(苏图) 1072
塞苏阿纳 tn tn 1074
信德语 sd sd 1113
僧伽罗语;僧伽罗语 si si 1115
斯洛伐克语 sk sk 1051 1250
斯洛文尼亚语 sl sl 1060 1250
索马里语 so so 1143
文德语 sb sb 1070
西班牙语 - 阿根廷 es es-ar 11274 1252
西班牙语 - 玻利维亚 es es-bo 16394 1252
西班牙语 - 智利 es es-cl 13322 1252
西班牙语 - 哥伦比亚 es es-co 9226 1252
西班牙语 - 哥斯达黎加 es es-cr 5130 1252
西班牙语 - 多米尼加共和国 es es-do 7178 1252
西班牙语 - 厄瓜多尔 es es-ec 12298 1252
西班牙语 - 萨尔瓦多 es es-sv 17418 1252
西班牙语 - 危地马拉 es es-gt 4106 1252
西班牙语 - 洪都拉斯 es es-hn 18442 1252
西班牙语 - 墨西哥 es es-mx 2058 1252
西班牙语 - 尼加拉瓜 es es-ni 19466 1252
西班牙语 - 巴拿马 es es-pa 6154 1252
西班牙语 - 巴拉圭 es es-py 15370 1252
西班牙语 - 秘鲁 es es-pe 10250 1252
西班牙语 - 波多黎各 es es-pr 20490 1252
西班牙语 - 西班牙(繁体) es es-es 1034 1252
西班牙语 - 乌拉圭 es es-uy 14346 1252
西班牙语 - 委内瑞拉 es es-ve 8202 1252
斯瓦希里语 sw sw 1089 1252
瑞典语 - 芬兰 sv sv-fi 2077 1252
瑞典语 - 瑞典 sv sv-se 1053 1252
古叙利亚语 1114
塔吉克斯坦 tg tg 1064
泰米尔语 ta ta 1097
鞑靼语 tt tt 1092 1251
泰卢固语 te te 1098
泰语 th th 1054
藏语 bo bo 1105
特松加 ts ts 1073
土耳其语 tr tr 1055 1254
土库曼语 tk tk 1090
乌克兰语 uk uk 1058 1251
统一码 UTF-8
乌都语 ur ur 1056 1256
乌兹别克语 - 西里尔文 uz uz-uz 2115 1251
乌兹别克语 - 拉丁语 uz uz-uz 1091 1254
文达 1075
越南语 vi vi 1066 1258
威尔士语 cy cy 1106
班图语 xh xh 1076
意第绪语 yi yi 1085
祖鲁语 zu zu 1077
参考链接:https://wiki.freepascal.org/Step-by-step_instructions_for_creating_multi-language_applications
结构化类型
结构化类型是可以在一个变量中保存多个值的类型。结构化类型可以无限嵌套。
打包
声明结构化类型时,不应假设类型中元素的字节位置。编译器将按照它认为最合适的位置来布置结构中的元素。也就是说,元素的顺序将被保留,但元素的字节位置不能保证,并且部分受 $PackRecords
指令的约束。
但是,Free Pascal 允许使用 Packed
和 BitPacked
关键字控制布局。这些词的含义取决于上下文:
BitPacked
让编译器尝试在位边界上对齐序数类型。
Packed
的含义取决于以下情况:
- 在
MacPas
模式下,它等效于BitPacked
关键字。 - 在其他模式下,如果将
$BitPacking
指令设置为ON
,它也等效于BitPack
关键字。 - 在其他模式下,如果将
$BitPacking
指令设置为OFF
,它表示在字节边界上对齐序数类型。
按位打包
使用 BitPacked
机制时,非序数类型(包括但不限于集合、浮点数、字符串、(Packed)记录、(Packed)数组、指针、类、对象和过程变量)存储在第一个可用字节边界上。
请注意,BitPacked
的内部是不透明的(不对用户暴露细节):它们将来可以随时更改。更重要的是,内部打包方式取决于进行编译的平台的字节序,并且平台之间无法转换。这使得 BitPacked
结构不适合存储在磁盘上或通过网络传输。然而,该格式与 GNU Pascal 编译器使用的格式相同,Free Pascal 团队的目标是在未来保持这种兼容性。
对 BitPacked
结构的元素还有更多限制:
- 无法检索地址,除非元素刚好也是字节对齐的。
BitPacked
结构的元素不能用作var
参数,除非元素刚好也是字节对齐的。
要确定 BitPacked
结构中元素的大小,可以使用 BitSizeOf
函数。它返回元素的位大小。对于其他非 BitPacked
结构的类型或元素,它将简单地返回 8 * SizeOf
。
BitPacked
记录和数组的大小是有限的:
- 在 32 位系统上,最大大小为 2^29 字节(512 MB)。
- 在 64 位系统上,最大大小为 2^61 字节。
原因是元素的偏移量必须使用系统的最大整数大小来计算。
集合字面量
集合字面量和数组字面量的语法相同,只有将它们赋值给不同类型的变量时,才会有所区分。如果直接使用该字面量(比如在 for v in [1, 2]
语句中),则会被解析为数组字面量。
// 集合类型(元素取值范围在 0-255 之间)
var a: set of byte = [1, 2];
集合类型
集合类型的元素可以是 0 到 255 之间的任何序数类型。一个集合最多可以包含 256 个元素。集合类型的大小是 32 字节,也就是 256 个 bit,集合通过 bit 来存储数据。
type
Days = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
var
WorkDays: set of Days; // 集合类型
begin
WorkDays := [Mon..Fri];
end.
集合运算符
+ // 并集
- // 差集
* // 交集
>< // 对称差集(不相交的元素的集合)
= // 相等判断
<> // 不等判断
<= // 子集判断
>= // 子集判断
Include // 添加元素
Exclude // 排除元素
in // 检查元素是否存在
位掩码
集合可用于创建位掩码,例如:
type
TFlag = (FlagA, FlagB, FlagC);
TFlags = set of TFlag;
var
Flags: TFlags;
begin
Flags := [FlagA, FlagC];
if FlagA in Flags then WriteLn('FlagA is enabled.');
end.
区分数组字面量
var
T: set of Byte;
A: array of String;
I: Integer;
S: String;
begin
T := [1, 2];
for I in T do
WriteLn(I); // 1 2
WriteLn(Low(T), '..', High(T)); // 0..255
A := [1, 2];
for I in A do
WriteLn(I); // 1 2
WriteLn(Low(A), '..', High(A)); // 0..1
// 未指定类型的字面量被识别为数组
WriteLn(High([1, 2])); // 1
end.
记录类型
记录类型只能包含字段,不能包含方法,但可以通过类型助手为其附加方法。
type
T = record
X, Y: Int32;
Z : UInt32;
end;
var
A: T = (X:0; Y:0; Z:0);
B: T = (X:0; Y:0);
C: T = ();
变体记录
type
T = record
case Boolean of
True : (X, Y, Z: Int32);
False: (A, B, C: Int64);
end;
case Boolean of
在这里只是为了实现分支语法,没有实际意义,可以换成任何数据类型。
圆括号相当于 begin
和 end
。
各个分支共用一组内存,记录的内存大小以最大的分支为准。
变体标记
可以给 `case` 语句指定一个标记,方便程序员标记当前使用的是哪个分支:
type
T = record
case Flag: Boolean of
True : (X, Y, Z: Int32);
False: (A, B, C: Int64);
end;
这个 `Flag` 只是一个普通字段,没有任何特殊含义,完全由程序员决定它的作用。它和下面的记录类型相同:
type
T = record
Flag: Boolean;
case Boolean of
True : (X, Y, Z: Int32);
False: (A, B, C: Int64);
end;
变体嵌套
变体和记录一样可以嵌套,但是变体部分只能放在记录的最后位置。
T = record
X: Int32;
case Byte of
10: (
Y: Int32;
case byte of
10: (Z: Int32);
);
end;
示例
type
T1 = record
X, Y, Z: Int8;
end;
T2 = record
case Boolean of
False: (X, Y: Int8);
True : (Z: Int64);
end;
T3 = record
case Flag: Boolean of
False: (X, Y: Int8);
True : (Z: Int64);
end;
var
A: T1;
B: T2;
C: T3;
I: Integer;
begin
WriteLn('T1 Size: ', SizeOf(T1)); // 3 (Int8 + Int8 + Int8)
A.X := 1;
A.Y := -1;
WriteLn(A.X, ' ', A.Y); // 1 -1
A.X := 10;
A.Y := 20;
for I := 0 to SizeOf(A) -1 do
Write(PByte(@A)[I], ' ');
WriteLn(); // 10 20 0
WriteLn('------------------------------');
WriteLn('T2 Size: ', SizeOf(T2)); // 8 (Int64)
B.X := 0;
B.Y := 1;
WriteLn(B.X, ' ', B.Y, ' ', B.Z); // 0 1 256
B.X := 10;
B.Y := 20;
for I := 0 to SizeOf(B) -1 do
Write(PByte(@B)[I], ' ');
WriteLn(); // 10 20 0 0 0 0 0 0
WriteLn('------------------------------');
WriteLn('T3 Size: ', SizeOf(T3)); // 16 (Boolean + Int64)
C.Z := 255;
C.Flag := True;
WriteLn(C.X, ' ', C.Y, ' ', C.Z, ' ', C.Flag); // -1 0 255 TRUE
C.X := 10;
C.Y := 20;
for I := 0 to SizeOf(C) - 1 do
Write(PByte(@C)[I], ' ');
WriteLn(); // 1 0 0 0 0 0 0 0 10 20 0 0 0 0 0 0
end.
布局和大小
记录的布局和大小受五个方面的影响:
-
字段的大小。
-
字段的对齐要求,这些要求取决于平台。注意,记录内部各个类型的对齐要求可能与该类型单独使用时的对齐要求不同。此外,字段在记录中的位置也可能影响字段的对齐要求。
-
当前的
{$Align N}
或{$PackRecords N}
设置(这些设置相互覆盖,最后一个指定的是有效设置。注意,这些指令不接受完全相同的参数)。 -
当前的
{$CodeAlign RecordMin=X}
设置。 -
当前的
{$CodeAlign RecordMax=X}
设置。
字段的对齐方式
记录中字段的偏移量等于前一字段的偏移量加上前一字段的大小之和,然后根据该字段所需的对齐值的倍数进行舍入。计算方法如下:
-
所需的对齐方式设置为字段类型的默认对齐方式,可能会根据此类型出现在记录中的位置以及字段在记录中的位置进行调整。
-
如果所需的对齐方式小于当前的
{$CodeAlign RecordMin=X}
设置,则会将其更改为此 X 值。 -
如果当前的
{$Align N}
或{$PackRecords N}
设置为-
一个数值:如果要求的对齐方式大于 N,则改为 N。
-
Reset
或Default
:所需的对齐方式取决于目标。 -
C
:根据当前平台官方 ABI 中指定的规则调整所需的对齐方式。 -
Power/PowerPC
,Mac68K
:对齐值的调整是通过遵循官方 ABI 规则来确定的,适用于(经典)Macintosh PowerPC 或 Macintosh 680x0 平台。
-
记录的对齐方式
记录的大小等于记录的最后一个字段偏移量与该字段的大小之和,然后根据记录所需的对齐值的倍数进行舍入。记录所需的对齐方式计算如下:
-
使用记录中字段的最大对齐方式。
-
如果当前
{$Align N}
或{$PackRecords N}
设置不是C
,并且所需的对齐方式大于当前的{$CodeAlign RecordMax=X}
,则所需的对齐方式将更改为 X。 -
如果当前的
{$Align N}
或{$PackRecords N}
设置是C
,则遵循官方 ABI 规则确定所需的对齐方式。
打包
Free Pascal 支持“打包记录”,即所有元素都与字节对齐。以下两个声明是等效的:
type
{$PackRecords 1}
T = record
A : Byte;
B : Word;
end;
{$PackRecords default}
type
T = packed record
A : Byte;
B : Word;
end;
请注意第一个声明后的 {$PackRecords default}
用于恢复默认设置。
鉴于记录在内存中的布局方式与平台相关的性质,确保跨平台兼容布局的唯一方法是使用 {$PackRecords 1}
(假设所有字段都使用这些“在相同平台上具有相同含义”的类型声明)。
特别是,如果必须读取由 Turbo Pascal 程序生成的带有记录的类型文件,则尝试正确读取该文件可能会失败。原因是 Free Pascal 的默认 {$PackRecords N}
设置不一定与 Turbo Pascal 兼容。它可以更改为 {$PackRecords 1}
或 {$PackRecords 2}
,具体取决于创建文件的 Turbo Pascal 程序中使用的设置(尽管由于 16 位 MSDOS 和当前平台之间的类型对齐要求不同,它仍可能因 {$PackRecords 2}
而失败)。
同样的情况也适用于 Delphi:只有当生产方和使用方都使用打包记录,或者他们在同一平台上并使用相同的 {$PackRecords X}
设置时,才能保证正确的数据交换。
数组字面量
['aa', 'bbbb'] // 数组类型(元素类型相同即可)
在 FPC 3.2.2 中,数组字面量还存在一个 Bug,会用第一个元素的长度截断所有其它元素的长度:
for S in ['aa', 'bbbb'] do
WriteLn(S); // aa bb
数组类型
静态数组(定长)是按值传递(在栈中分配空间),具有“写入时复制”特性。
动态数组(不定长)是按引用传递(在堆中分配空间,有引用计数),所有引用共享底层数据,没有“写入时复制”特性。
SetLength()
调用将确保返回数组的引用计数为 1,即使设置的新长度与原长度相同(同样适用于字符串)。
var
I: Integer;
A: array[2..8] of Integer; // 静态数组,可自定义索引范围,在栈中分配内存
B: array of Integer; // 动态数组,索引从 0 开始,尚未分配内存
begin
for I := Low(A) to High(A) do
A[I] := I;
SetLength(B, 10); // 在堆中分配内存
for I := Low(B) to High(B) do
B[I] := I;
WriteLn(A[Low(A)], ' ', A[High(A)]);
WriteLn(B[Low(B)], ' ', B[High(B)]);
end.
多维数组
var
A: array of array[1..5] of Integer; // 栈大小:1 * SizeOf(Pointer);
B: array[1..5] of array of Integer; // 栈大小:5 * SizeOf(Pointer);
I, J: Integer;
begin
// 初始化
// 在堆中申请 10 个大小为 5 * Sizeof(Integer) 的内空间
SetLength(A, 10);
for I := Low(A) to High(A) do begin
for J := Low(A[I]) to High(A[I]) do
A[I][J] := I * J;
end;
// 显示内容
for I := Low(A) to High(A) do begin
for J := Low(A[I]) to High(A[I]) do
Write(A[I][J], ' ');
WriteLn();
end;
// 初始化
for I := Low(B) to High(B) do begin
// 在堆中申请 1 个长度为 I * SizeOf(Integer) 的内存空间
SetLength(B[I], I);
for J := Low(B[I]) to High(B[I]) do
B[I][J] := I * J;
end;
// 显示内容
for I := Low(B) to High(B) do begin
for J := Low(B[I]) to High(B[I]) do
Write(B[I][J], ' ');
WriteLn();
end;
end.
var
// 栈大小:25 * SizeOf(Pointer);
A: array[1..5] of array[1..5] of Integer;
// 栈大小:25 * SizeOf(Pointer);
B: array[1..5, 1..5] of Integer;
var
A : Array of array of Integer;
begin
SetLength(A, 10, 100); // 创建多维动态数组
end;
High()
和 Low()
函数返回第一维索引的上下限。
Copy()
函数可用于截取数组或字符串。
动态数组初始化
type
TIntArray = array of Integer;
TIntArray2 = array of array of Integer;
var
A: TIntArray;
B: TIntArray2;
C: TIntArray2 = ((1,2,3), (4,5,6), (7,8,9));
begin
A := TIntArray.Create(1, 2, 3); // 或 [1, 2, 3]
B := [
[1, 2, 3],
[4, 5, 7],
[7, 8, 9]
];
end.
连接数组
启用 {$ModeSwitch ArrayOperators}
后,可以使用 +
连接两个相同类型的数组。
{$ModeSwitch ArrayOperators}
type
IntArray = array of Integer;
var
A: IntArray = (1, 2, 3);
B: IntArray = (2, 3, 4);
I: Integer;
begin
for I in A + B do
WriteLn(I); // 1 2 3 2 3 4
end.
打包
数组可以 packed
和 bitpacked
。具有相同索引类型和元素类型但打包方式不同的两种数组类型不能相互赋值。
但是,可以使用 pack()
函数将普通数组转换为 bitpacked
数组。反向操作也是允许的。可以使用 unpack()
函数将 bitpacked
数组转换为普通数组,例如:
var
I: Integer;
J: Char;
A: array ['a'..'f'] of Boolean
= (False, False, True, False, False, False);
B: bitpacked array [42..47] of Boolean;
C: array ['0'..'5'] of Boolean;
begin
WriteLn(BitSizeOf(A)); // 48
WriteLn(BitSizeOf(B)); // 8
WriteLn(BitSizeOf(C)); // 48
Pack(A, 'a', B); // 将 A 中从索引 'a' 开始的元素打包到 B 中
for I := Low(B) to High(B) do Write(B[I], ' ');
WriteLn();
UnPack(B, C, '0'); // 将 B 中的元素解包到 C 中索引 '0' 开始的位置
for J := Low(C) to High(C) do Write(C[J], ' ');
end.
文件类型
文件类型是存储某些“基类型”序列的类型,该“基类型”可以是除其他文件类型之外的任何类型。它可以包含无限数量的元素。文件类型通常用于在磁盘上存储数据。
type
TPoint = record
X, Y, Z: real;
end;
PointFile = File of TPoint;
如果未给出类型标识符,则该文件是非类型化的文件。它可以被等效视为字节文件。非类型化的文件需要特殊函数(BlockRead
、BlockWrite
)才能对其进行操作。
在内部,文件由 FileRec
记录表示,该记录在 Dos
或 SysUtils
单元中声明。
文本文件
一种特殊的文件类型是 Text
类型,由 TextRec
记录表示。该类型的文件使用特殊的输入输出函数。默认的 Input
、Output
和 StdErr
文件(标准输入、标准输出、标准错误)在 System
单元中定义:它们都是 Text
类型,并由 System
单元的 initialization
代码将其打开。
指针字面量
nil // 空指针
指针类型
type
PInt = ^Integer;
var
I: Integer;
P: PInt;
begin
I := 0;
P := @I; // @ 用于获取变量 I 的地址
P^ := 100; // ^ 用于解引用指针,访问其所指向的数据
WriteLn(I); // 100
WriteLn(PtrUInt(P)); // 4398496
WriteLn(PtrUInt(P+1)); // 4398500 加 1 是增加一个元素的宽度
end.
如果 {$T}
开关打开,@
运算符将返回类型化的指针。如果 {$T}
开关关闭,则 @
运算符返回一个非类型化的指针,非类型化的指针与所有指针类型赋值兼容。默认情况下,@
运算符返回一个非类型化指针。
类和对象
类(Class)和对象(Object)的用法差不多,不过对象是在栈上分配内存(按值传递),类是在堆上分配内存(按引用传递)。在静态方法的使用上也有差别。对象基本上不再被使用,只需要关注类就可以了。
type
TObj = object
private
Data: Int32;
public
constructor Init;
destructor Final;
procedure SetData(Value: Int32);
function GetData(): Int32;
end;
TCla = class
private
Data: Int32;
public
constructor Create;
destructor Destroy;
procedure SetData(Value: Int32);
function GetData(): Int32;
end;
`constructor` 指定构造函数,`destructor` 指定析构函数。
类和对象的方法中可以使用 Self
代表当前实例,也可以省略 Self
。
在 MacPas 模式下,object
关键字被替换为 class
关键字,以便与 Mac 上可用的其他 pascal 编译器兼容。这意味着 object
不能在 MacPas 模式下使用。
类和对象也支持 packed
。就像 packed
记录一样。
字段也可以使用 var
的方式声明:
type
TCla = class
private
var Data: Int32;
end;
编译器从 3.0 版开始,可以对内存中的字段重新排序,如果这会导致更好的对齐和更小的实例。这意味着在实例中,字段的显示顺序不一定与声明中的顺序相同。为类生成的 RTTI(运行时类型信息)将反映此更改。
可见性
可见性分为 public
、protected
、public
,默认为 public
。这些关键字可多次使用。
private protected public
单元内 可见 可见 可见
子类 不可见 可见 可见
单元外 不可见 不可见 可见
严格可见性
另外还有 strict private
和 strict protected
不允许单元内访问。
type
TCla = class
private
A: Int32;
protected
B: Int32;
public
C: Int32;
strict private
D: Int32;
strict protected
E: Int32;
public
PublicAgain: Int32;
end;
发布可见性
在 {$M+}
状态下编译的类可以具有 published
部分。对于 published
部分中的方法、字段和属性,编译器会为其生成 RTTI(运行时类型信息),这些信息可用于查询 published
部分中定义的方法、字段和属性。typinfo
单元包含查询这些信息所需的函数,该单元用于 Classes
单元中的流系统。
只能发布类类型的字段。对于属性,任何大小小于等于指针的简单属性都可以声明 published
:浮点数、整数、集合(元素数量不超过 32)、枚举、类或动态数组(不是静态数组)。
尽管其它类型也有 RTTI 信息,但这些类型不能用于 published
部分中的属性或字段定义。
封闭类
可以使用 sealed
关键字将类和对象设置为封闭的,这样的类和对象无法被继承。
type
TCla = class sealed
end;
抽象类
可以使用 abstract
关键字将类和对象设置为抽象的,这样的类和对象无法被实例化。但是,为了与 Delphi 兼容,编译器会忽略该关键字,仅在实例化该类时给出警告。
type
TCla = class abstract
end;
定义字段
在类和对象中定义字段的方法和记录体相同:
type
TObj = object
Data: Int32;
end;
TCla = class
Data: Int32;
end;
类和对象还支持另一种定义字段的方法:
type
TObj = object
var Data: Int32;
end;
TCla = class
var Data: Int32;
end;
静态字段
静态字段的特点是该字段归类本身所有,其数据由所有实例和子类共享。该字段可以通过“类型名.字段名”或“实例名.字段名”的方式进行访问。
类和对象都可以通过 static
关键字声明静态字段:
type
TObj = object
Data: Int32; static;
end;
var
A: TObj;
begin
A.Data := 100;
WriteLn(TObj.Data)
end.
另一种声明静态字段的方法是使用 class
关键字:
type
TObj = object
class var Data: Int32;
end;
这两种方法等效,只不过后一种方法仅在 {$mode Delphi}
或 {$mode ObjFpc}
模式下有效。
静态字段在 {$mode Delphi}
模式下可以被子类覆盖。
调用父类方法
可以使用 inherited
关键字访问父类的方法,inherited
后面可以指定要调用的父类方法的名称,如果省略,则表示调用与当前方法相同签名的方法,并传入相同的参数。
type
TBase = class
constructor Create();
end;
TSub = class(TBase)
constructor Create();
end;
constructor TBase.Create;
begin
WriteLn('TBase');
end;
constructor TSub.Create;
begin
inherited; // 调用父类的 Create 方法
WriteLn('TSub');
end;
var
A: TSub;
begin
A := TSub.Create(); // TBase TSub
A.Free();
end.
构造和析构
构造函数的名称可以自定义,构造函数可以有多个。
对象的析构函数名称可以自定义,数量只能有一个。
类的析构函数名称必须使用 Destroy,必须重写 TObject 中声明的 Destroy,不能有参数,并且必须始终调用继承的析构函数(inherited)。
type
TCla = class
private
Data: Pointer;
public
constructor Create;
constructor New;
destructor Destroy; override;
end;
var
A: TCla;
constructor TCla.Create;
begin
inherited;
GetMem(Data, 100);
end;
constructor TCla.New;
begin
inherited Create;
GetMem(Data, 200);
end;
destructor TCla.Destroy;
begin
WriteLn('Destroy');
FreeMem(Data);
inherited Destroy;
end;
begin
A := TCla.Create();
A.Free(); // Destroy
A := TCla.New();
A.Free(); // Destroy
end.
Destroy
将调用 FreeInstance
,在默认实现中,FreeInstance
将调用 FreeMem
以释放实例占用的内存。
为了避免在 nil
实例上调用析构函数,最好调用 TObject
的 Free
方法。此方法将检查 Self
是否为 nil
,如果不是,则调用 Destroy
。如果 Self
等于 Nil
,它将退出。(疑问:如果 Self
为 nil
,还能调用 Free
方法吗?怎么检查?)
建议使用 SysUtils
单元的 FreeAndNil(Obj: TObject)
方法,它会检查要释放的对象是否为 nil
,如果不是则调用其 Destroy
方法,并将该对象设置为 nil
,该方法可以反复调用,不会出问题。
如果在执行构造函数期间发生异常,将自动调用析构函数。
在堆上创建对象
对象可以使用 New
和 Dispose
在堆上实例化,这使其更像是一个类了:
type
TObj = object
constructor Init;
destructor Final;
end;
PObj = ^TObj;
constructor TObj.Init;
begin
WriteLn('Init');
end;
destructor TObj.Final;
begin
WriteLn('Final');
end;
var
A: PObj;
begin
A := new(PObj, Init);
dispose(A, Final);
new(A, init);
dispose(A, Final);
new(A); // 编译器警告,指出应该指定构造函数
A^.Init();
A^.Final();
dispose(A); // 编译器警告,指出应该指定析构函数
end.
虚拟方法
“虚拟方法”是和“静态方法”相对而言的,“静态方法”是在编译期就确定的方法,而“虚拟方法”是在执行期才确定的方法。
普通的方法属于“静态方法”,使用 virtual
关键字声明的方法属于“虚拟方法”。
对于类而言,需要使用 override
关键字重写父类的虚拟方法。
对于对象而言,需要使用 virtual
关键字重写父类的虚拟方法。
重写后的方法仍然属于虚拟方法,可以被后续子类再次重写。
如果不使用 override
或 virtual
关键字,而是直接声明与父类相同的方法,则属于覆盖父类的方法。这样会使父类的“虚拟方法”变成普通的“静态方法”,编译器会给出警告。如果确实需要这么做,可以使用 reintroduce
关键字避免编译器警告。
另一个关键字是 dynamic
,它的作用与 virtual
一样,没有区别。
“静态方法”是根据调用者“声明”的类型来确定方法地址,“虚拟方法”是根据调用者“构造”的类型来确定方法地址:
type
// 每个子类都与父类有相同的方法
TBase = class
procedure F1();
procedure F2(); virtual;
end;
TSub1 = class(TBase)
procedure F1();
procedure F2(); override;
end;
TSub2 = class(TBase)
procedure F1();
procedure F2(); override;
end;
procedure TBase.F1;
begin
WriteLn('TBase');
end;
procedure TBase.F2;
begin
WriteLn('TBase');
end;
procedure TSub1.F1;
begin
WriteLn('TSub1');
end;
procedure TSub1.F2;
begin
WriteLn('TSub1');
end;
procedure TSub2.F1;
begin
WriteLn('TSub2');
end;
procedure TSub2.F2;
begin
WriteLn('TSub2');
end;
// 这里需要根据不同的子类调用不同的方法
procedure Test(A: TBase);
begin
A.F1(); // F1 是静态方法,永远根据其声明类型 TBase 来选择方法
A.F2(); // F2 是动态方法,根据参数 A 构造时的类型来选择方法
WriteLn('----------');
end;
var
A: TBase;
B: TSub1;
C: TSub2;
begin
A := TBase.Create;
B := TSub1.Create;
C := TSub2.Create;
Test(A); // TBase, TBase
Test(B); // TBase, TSub1
Test(C); // TBase, TSub2
end.
如果使用 new
创建对象,则对象的普通方法也是静态的,除非将其声明为 virtual
方法:
type
TBase = object
constructor init();
procedure F1();
procedure F2(); virtual;
end;
PBase = ^TBase;
TSub = object(TBase)
procedure F1();
procedure F2(); virtual;
end;
PSub = ^TSub;
constructor TBase.init;
begin
end;
procedure TBase.F1;
begin
WriteLn('TBase');
end;
procedure TBase.F2;
begin
WriteLn('TBase');
end;
procedure TSub.F1;
begin
WriteLn('TSub');
end;
procedure TSub.F2;
begin
WriteLn('TSub');
end;
procedure Test(A: PBase);
begin
A^.F1();
A^.F2();
end;
var
B1, B2: PBase;
S: PSub;
begin
B1 := new(PBase, init);
B2 := new(PSub, init);
S := new(PSub, init);
Test(B1); // TBase TBase
Test(B2); // TBase TSub
Test(S); // TBase TSub
end.
抽象方法
抽象方法的特点是只有声明部分,没有实现部分,含有抽象方法的类属于抽象类。
可以使用 abstract
关键字声明抽象方法,abstract
关键字必须与 virtual
关键字一起使用。
type
TCla = class
procedure F(); virtual; abstract;
end;
消息方法
可以使用 message
关键字声明消息方法。消息方法只能接受一个 var
参数(有类型或无类型):
type
TIntMsg = record
MsgID : Cardinal;
Data : String;
end;
TStrMsg = record
MsgID : String[16];
Data1 : String;
Data2 : String;
end;
{ TCla }
TCla = class
procedure F1(var Msg: TIntMsg); message 0;
procedure F2(var Msg: TStrMsg); message 'Hello';
procedure F3(var Msg); message 1;
end;
procedure TCla.F1(var Msg: TIntMsg);
begin
WriteLn(Msg.Data);
end;
procedure TCla.F2(var Msg: TStrMsg);
begin
WriteLn(Msg.Data1, ' ', Msg.Data2);
end;
procedure TCla.F3(var Msg);
begin
WriteLn('Message');
end;
var
A: TCla;
IntMsg: TIntMsg;
StrMsg: TStrMsg;
I: Integer;
begin
A := TCla.Create();
IntMsg.MsgID := 0;
IntMsg.Data := 'IntMsg';
A.Dispatch(IntMsg);
StrMsg.MsgID := 'Hello';
StrMsg.Data1 := 'Hello';
StrMsg.Data2 := 'World';
A.DispatchStr(StrMsg);
I := 1;
A.Dispatch(I);
A.Free();
end.
Dispatch
或 DispatchStr
将查看对象是否声明了(或继承了)具有指定消息的方法。如果有,则调用该方法并传入 Msg
参数。
如果未找到此类方法,则调用 DefaultHandler
方法。DefaultHandler
是 TObject
的虚拟方法,它不执行任何操作,但可以重写它以提供可能需要的任何处理。
消息类型可以自定义,只要第一个字段符合消息要求就可以了,对于整数消息,第一个字段必须时 `Cardinal` 类型,对于字符串消息,第一个字段必须是 `ShortString` 类型。
消息方法的方法实现与普通方法没有什么不同。也可以直接调用消息方法,但不应这样做。
消息方法自动是虚拟的,它们可以在后代类中被覆盖。
类方法
类方法的特点是可以通过“类型名.方法名”进行调用,不需要类实例。在类方法中,只能访问静态字段和类方法,不能访问非静态字段和普通方法(因为非静态字段只有在创建类实例后才会生成,普通方法需要类实例作为其 Self 参数)。
对于类而言,可以使用 class
关键字声明类方法,
对于对象而言,必须使用 static
关键字声明类方法:
type
TCla = class
Data: Int32; static;
class procedure F(); // 类方法
end;
TObj = object
Data: Int32; static;
procedure F(); static; // 类方法
end;
class procedure TCla.F;
begin
WriteLn(Self.Data);
end;
procedure TObj.F;
begin
WriteLn(Self.Data);
end;
begin
TCla.Data := 100;
TCla.F(); // 100 // 通过类型名调用
TObj.Data := 200;
TObj.F(); // 200 // 通过类型名调用
end.
类方法中的 Self
指向类的虚拟方法表(VMT),也就是说类本身就是一个虚拟方法表。
type
TCla = class
class procedure F;
end;
class procedure TCla.F();
begin
WriteLn(PtrInt(TCla));
WriteLn(PtrInt(Self));
WriteLn(PShortString((Pointer(Self) + vmtClassName)^)^);
WriteLn(Self.ClassName());
end;
begin
TCla.F(); // 4358256 4358256 TCla TCla
end.
可以使用 virtual
声明虚拟类方法,可以使用 override
重写虚拟类方法。
静态类方法
静态类方法是另外一个概念,其特点是它不会调用派生类中的虚拟类方法。
可以在类方法中使用 static
关键字声明静态类方法:
type
TBase = class
class procedure F(); virtual;
class procedure StaticCall(); static;
class procedure NormalCall();
end;
TSub = class(TBase)
class procedure F(); override;
end;
class procedure TBase.F;
begin
WriteLn('Base');
end;
class procedure TBase.StaticCall;
begin
F(); // 不会调用派生类中的 F();
end;
class procedure TBase.NormalCall;
begin
F(); // 会调用派生类中的 F();
end;
class procedure TSub.F;
begin
WriteLn('Sub');
end;
begin
TBase.F(); // Base
TBase.StaticCall(); // Base
TBase.NormalCall(); // Base
TSub.F(); // Sun
TSub.StaticCall(); // Base // 不调用派生类中的 F()
TSub.NormalCall(); // Sub // 调用派生类中的 F();
end.
静态类方法不能单独使用 static
关键字定义,必须配合 class
关键字一起使用。
静态类方法可以直接赋值给普通函数类型,而普通类方法只能赋值给对象函数类型(of object
):
type
TCls = class
private
Data: Int32; static;
public
class procedure F1();
class procedure F2(); static;
end;
class procedure TCls.F1;
begin
WriteLn(Self.Data);
end;
class procedure TCls.F2;
begin
WriteLn(Data);
end;
var
Func1: procedure of object;
Func2: procedure;
begin
TCls.Data := 100;
Func1 := @TCls.F1; // 对象函数类型
Func2 := @TCls.F2; // 普通函数类型
Func1(); // 100
Func2(); // 100
end.
类构造和析构
类构造函数在声明它的单元的 initialization
部分之前调用,类析构函数在声明它的单元的 finalizaiton
部分之后调用。
类构造函数和类析构函数遵循以下规则:
1、最多各有一个,名称是任意的,但不能有参数。
2、不能是虚拟的。
3、始终会被调用,即使从未使用过该类。
4、没有调用顺序保证。对于嵌套类,唯一保证的顺序是先构造外部,再构造内部,先析构内部,再析构外部。
type
TCls = class
class constructor Init();
class destructor Final();
end;
class constructor TCls.Init;
begin
WriteLn('Init');
end;
class destructor TCls.Final;
begin
WriteLn('Final');
end;
begin
WriteLn('program');
end.
// Init
// program
// Final
定义属性
可以通过 property
关键字定义属性,通过 read
和 write
关键字定义属性要读写的数据或方法。如果省略 write
则属性是只读的,如果省略 read
则属性是只写的。
属性可以像普通字段一样访问。属性的作用是对私有数据进行有限的访问。
type
TCla = class
private
FData: Int32;
public
property Data: Int32 read FData write FData;
end;
type
TObj = object
private
FData: Int32;
function GetData: Int32;
procedure SetData(Value: Int32);
public
property Data: Int32 read GetData write SetData;
end;
function TObj.GetData: Int32;
begin
Result := FData;
end;
procedure TObj.SetData(Value: Int32);
begin
if Value > 0 then
FData := Value;
end;
var
Obj: TObj;
begin
Obj.Data := 100;
WriteLn(Obj.Data); // 100
Obj.Data := -100;
WriteLn(Obj.Data); // 100
end.
属性不能作为 var
参数传递给函数或过程,因为属性没有已知的地址(至少并非总能得到地址)。
索引属性
type
TPoint = class
private
FX, FY : Longint;
function GetCoord (I: Integer): Longint;
procedure SetCoord (I: Integer; Value: longint);
public
property X: Longint index 1 read GetCoord write SetCoord;
property Y: Longint index 2 read GetCoord write SetCoord;
property Coords[Index : Integer]:Longint read GetCoord;
end;
procedure TPoint.SetCoord(I: Integer; Value: Longint);
begin
case I of
1: FX := Value;
2: FY := Value;
end;
end;
function TPoint.GetCoord(I: Integer): Longint;
begin
case I of
1: Result := FX;
2: Result := FY;
end;
end;
var
P: TPoint;
begin
P := TPoint.Create;
P.X := 2;
P.Y := 3;
WriteLn('X=',P.X,' Y=',P.Y);
end.
数组属性
type
TCla = class
private
...
function GetInt(I: LongInt): LongInt;
procedure SetInt(I: LongInt; Value: LongInt);
function GetStr(A: String): String;
procedure SetStr(A: String; Value: String);
public
property Ints[I: LongInt]: LongInt read GetInt write SetInt;
property Strs[S: String]: String read GetStr write SetStr;
end;
数组属性可以是多维的:
type
TCla = class
private
function GetInt(I,J: Int32): Int32;
procedure SetInt(I,J: Int32; Value: Int32);
public
property Ints[Row,Col: Int32]: Int32 read GetInt write SetInt;
end;
如果有 N
个维度,则 Get
和 Set
的前 N
个参数的类型必须与数组属性定义中 N
个索引说明符的类型相对应。
默认属性
可以使用 default
关键字将数组属性声明为默认属性。这意味着在分配或读取该属性时无需指定属性名:
type
TCla = class
private
...
function GetInt(I: Int32): Int32;
procedure SetInt(I: Int32; Value: Int32);
public
property Ints[I: Int32]: Int32 read GetInt write SetInt; default;
end;
var
A: TCla;
begin
...
A[0] := 0; // 相当于 A.Ints[0] := 0;
end;
每个类只允许一个默认属性,但后代类可以重新声明默认属性。
属性默认值
以 Lazarus 的窗体文件为例,当用户在“属性编辑器”中修改了窗体的某个属性后,Lazarus 会将修改后的属性值存入窗体的 .lfm
文件中,如果之后用户又将该属性改回了原来的默认值,此时这个默认值是没必要保存到 .lfm
中的。这就需要有一个数据来告诉 Lazarus 默认值是多少,以便 Lazarus 在处理属性值时与之对比来决定是否保存该属性的值。并不是所有属性有需要有默认值,有些属性无论如何都不需要被保存,比如一些数据库控件的连接属性。
将属性值转换为 .lfm
文件内容的过程称为“流式处理”(或称为“序列化”),
stored
和 default
修饰符就可以用来实现这个需求。
stored
修饰符后面可以跟随一个布尔常量、类的布尔字段或返回布尔值的函数,它标识一个属性的值是否需要被保存,如果省略 stored
修饰符,则默认为 True
。
如果 stored
指定为 False
,则该属性值不会被保存,如果指定为 True
,则根据 default
修饰符而定。
可以为序数或集合类型的属性指定 default
修饰符,default
后面可以跟随一个值,用于标识这个属性的默认值是多少。如果指定了 default
值,那么在处理属性时,如果属性值与 default
值相同,则不保存该属性值,否则保该存属性值。
另一个关键字 nodefault
用来取消父类中指定的 default
属性,取消后,相应属性的任何值都需要被保存。
字符串、浮点数和指针属性分别具有空字符串、0、nil 的隐式 default
值。序数和集合属性没有隐式 default
值。
type
TCla = class
private
FData: Int32;
published
property Data1: Int32 read FData write FData default 100;
property Data2: Int32 read FData write FData nodefault;
property Data3: Int32 read FData write FData stored False;
property Data4: Int32 read FData write FData stored False default 100;
end;
stored
和 default
的相关数据存储在类的 RTTI 中。
当类实例化时,default
值不会自动应用于属性,程序员需要在类的构造函数中自行初始化该属性。
值 -2147483648
不能用作默认值,因为它在内部用于表示 nodefault
。
无法为数组属性指定 stored
或 default
修饰符。
上面描述的流式处理机制是在 RTL 的 Classes
单元中实现的机制。用户可以实现其它流机制,并且它们可以以不同的方式使用 RTTI 信息。
属性改写
属性可以在后代类中改写和重声明。
子类在声明与父类同名的属性时,如果指定了属性的类型,则属于属性重声明,否则为属性改写。
属性改写使用新修饰符替换或扩展继承的修饰符。
属性重声明隐藏所有继承修饰符。重声明的属性类型不必与父属性类型相同。
type
TBase = class
private
FData: Int32;
public
property Data: Int32 Read FData write FData;
end;
TSub1 = class(TBase)
public
property Data default 100; // 属性重写(注意在构造函数中设置初始值)
end;
TSub2 = class(TBase)
private
FData: Int64;
public
property Data: Int64 Read FData; // 属性重声明
end;
关键字 inherited
可用于引用属性的父定义。
type
TBase = class
private
FData : Int32;
public
property Data: Int32 read FData write FData;
end;
TSub = class(TBase)
private
procedure SetData(const Value: Char);
function GetData: Char;
public
constructor Create;
property Data: Char read GetData write SetData;
end;
procedure TSub.SetData(const Value: Char);
begin
inherited Data := Ord(Value);
end;
function TSub.GetData: Char;
begin
Result := Char((inherited Data) and $FF);
end;
在属性改写和属性重声明的情况下,如果属性中使用了 Get
或 Set
方法,则对这些方法的访问是静态的。即对属性的改写仅作用于对象的 RTTI,不要与方法覆写混淆。
type
TBase = class
private
function GetData: Int32;
public
property Data1: Int32 read GetData;
property Data2: Int32 read GetData;
end;
TSub = class(TBase)
private
function GetData: Int32;
public
property Data1 nodefault; // 属性改写
property Data2: Int32 read GetData; // 属性重声明
end;
function TBase.GetData: Int32;
begin
Result := 100;
end;
function TSub.GetData: Int32;
begin
Result := 200;
end;
var
A: TSub;
begin
A := TSub.Create;
WriteLn(A.Data1); // 100
WriteLn(A.Data2); // 200
A.Free;
end.
在属性改写和属性重声明的情况下,如果属性的 Get
或 Set
方法中使用了 virtual
关键字,则这些方法遵循继承规则。
type
TBase = class
private
function GetData: Int32; virtual; // 虚拟方法
public
property Data1: Int32 read GetData;
property Data2: Int32 read GetData;
end;
TSub = class(TBase)
private
function GetData: Int32; override; // 重写方法
public
property Data1 nodefault;
property Data2: Int32 read GetData;
end;
function TBase.GetData: Int32;
begin
Result := 100;
end;
function TSub.GetData: Int32;
begin
Result := 200;
end;
var
A: TSub;
begin
A := TSub.Create;
WriteLn(A.Data1); // 200
WriteLn(A.Data2); // 200
A.Free;
end.
如果必须调用父类的属性实现,则需要显式调用:
constructor TSub.Create;
begin
inherited SetData(100);
end;
重声明的祖先属性也可以从后代对象的内部和外部使用:
function GetParentData(A: TSub): Int32;
begin
Result := TBase(A).Data;
end;
类属性
类属性与类相关联,而不是与类的实例相关联。
属性值的存储必须是类变量,而不是类的常规字段或变量。
类属性的 Get
和 Set
方法必须是静态类方法。这样要求的原因是因为类属性应该与特定类相关联,而不是与其后代类相关联。由于普通类方法可以是虚拟的,允许后代类重写,这会导致类属性失去与原始类的关联。所以要求 Get
和 Set
方法必须是静态的,不允许重写。
type
TBase = class
private
class function GetData: Int32; static;
public
class property Data: Int32 read GetData;
end;
TSub = class(TBase)
private
class function GetData: Int32; static;
public
class property Data: Int32 read GetData;
end;
class function TBase.GetData: Int32;
begin
Result := 100;
end;
class function TSub.GetData: Int32;
begin
Result := 200;
end;
begin
WriteLn(TBase.Data); // 100
WriteLn(TSub.Data); // 200
end.
类范围定义
类定义中可以包含 type
、const
、var
区段,其中 var
区段中的变量充当普通字段,type
和 const
中定义的标识符可以当作普通类型或常量使用(只能在它们定义的可见性范围内使用):
type
TCla = class
type
I32 = Int32;
var
A: I32;
const
S: String = 'Hello';
procedure F();
end;
procedure TCla.F;
var
I: I32 = 100;
begin
WriteLn(S, ' ', I);
end;
var
I: TCla.I32;
begin
I := 200;
WriteLn(I); // 200
TCla.F(); // Hello 100
end.
对于可写常量,在后代的作用域和覆盖方面,与类变量使用相同的规则:
type
TBase = class
const Data: Int32 = 0;
end;
TSub1 = class(TBase);
TSub2 = class(TBase);
begin
TBase.Data := 10;
TSub1.Data := 20;
TSub2.Data := 30;
Writeln(TBase.Data,' ',TSub1.Data,' ',TSub2.Data); // 30 30 30
end.
{$mode Delphi}
type
TBase = class
const Data: Int32 = 0;
end;
TSub1 = class(TBase)
const Data: Int32 = 0;
end;
TSub2 = class(TBase)
const Data: Int32 = 0;
end;
begin
TBase.Data := 10;
TSub1.Data := 20;
TSub2.Data := 30;
Writeln(TBase.Data,' ',TSub1.Data,' ',TSub2.Data); // 10 20 30
end.
引用类类型
可以使用 class of
引用类类型:
type
TComponentClass = class of TComponent;
function CreateComponent(AClass: TComponentClass;
AOwner: TComponent): TComponent;
begin
Result := AClass.Create(AOwner);
end;
var
C : TComponent;
begin
C := CreateComponent(TEdit, Form1);
end;
类型判断
is
可以用于判断某个对象是否属于某个类或该类的派生类。
as
可以对对象进行类型转换。
type
TCla = class
procedure F();
end;
procedure TCla.F;
begin
WriteLn('Hello');
end;
var
A: TObject;
begin
A := TCla.Create();
if A is TCla then
(A as TCla).F(); // Hello
A.Free();
end.
Object is Class
完全等效于 Object.InheritsFrom(Class)
Object as Class
等效于:
if Object = nil then
result := nil
else if Object is Class then
result := Class(Object)
else
raise Exception.Create(SErrInvalidTypeCast);
函数或过程类型
type
TFunc = function: Integer; // 定义函数类型
TProc = procedure(X: Integer); // 定义过程类型
var
F: TFunc; // 函数类型的变量
P: TProc; // 过程类型的变量
function Func1: Integer; // 一个符合 TFunc 签名的函数
begin
Result := 100;
end;
procedure Proc1(X: Integer); // 一个符合 TProc 签名的过程
begin
WriteLn(X);
end;
begin
F := @Func1; // 将函数赋值给变量
P := @Proc1; // 将过程赋值给变量
P(F()); // 通过变量执行调用操作
end.
传递子过程
// 必须启用此选项,才能传递子过程
{$ModeSwitch NestedProcVars}
type
// 启用上面的选项后,才能使用 is nested 修饰符
TNestedProc = procedure is nested;
// 参数 P 可以是普通过程,也可以是子过程
procedure Test(P: TNestedProc);
begin
P();
end;
// 普通过程
procedure Proc1;
begin
WriteLn('No nested');
end;
procedure Proc2;
// 子过程
procedure NProc2;
begin
WriteLn('Nested');
end;
begin
Test(@Proc1); // 传入普通过程
Test(@NProc2); // 传入子过程
end;
begin
Proc2();
end.
修饰符
有关调用约定的修饰符必须与声明相同。以下代码将给出错误:
type
TProc = procedure(var X: Integer); cdecl;
var
Proc: TProc;
procedure PrintIt(var X: Integer);
begin
WriteLn (x);
end;
begin
Proc := @PrintIt;
end.
方法类型
此机制有时称为委派。
program project1;
type
// 使用 of object 声明一个方法类型
TObjProc = procedure() of object;
var
P: TObjProc; // TObjProc 类型的变量
type
// 创建一个类,并定义一个符合 TObjProc 签名的方法
TMyObject = class
procedure Test();
end;
// 实现该方法
procedure TMyObject.Test();
begin
WriteLn(Self.ClassName);
end;
var
Obj: TMyObject; // 该类的对象
begin
Obj := TMyObject.Create();
P := @Obj.Test; // 将对象的方法赋值给 P
P(); // 通过 P 调用该方法(其中 Self 就是 Obj)
end.
因为 TProc
类型是使用 cdecl
调用约定的过程。
比较方法类型的两个变量时,仅比较方法的地址,而不比较实例第地址。这意味着以下程序将打印 True:
Type
TObjProc = procedure of object;
TSome = class
procedure DoSomething;
end;
procedure TSome.DoSomething;
begin
WriteLn('In DoSomething');
end;
var
X , Y : TSome;
P1, P2 : TObjProc;
begin
X := TSome.Create;
Y := TSome.Create;
P1 := @X.DoSomething;
P2 := @Y.DoSomething;
Writeln('Same method: ', P1 = P2);
end.
如果必须同时比较方法和实例两个指针,则必须转换为 TMethod
类型,并且比较其中的两个指针。该类型在 system
单元中定义如下:
TMethod = record
Code : CodePointer;
Data : Pointer;
end;
以下程序将打印 False:
Type
TObjProc = procedure of object;
TSome = class
procedure DoSomething;
end;
procedure TSome.DoSomething;
begin
WriteLn('In DoSomething');
end;
var
X , Y : TSome;
P1, P2 : TObjProc;
begin
X := TSome.Create;
Y := TSome.Create;
P1 := TMethod(@X.DoSomething);
P2 := TMethod(@Y.DoSomething);
Writeln('Same method: ', (P1.Data = P2.Data) and (P1.Code = P2.Code));
end.
接口
FPC 从 1.1 版开始支持接口。接口是多重继承的替代方法(多重继承是指一个类可以有多个父类)。接口基本上是一组命名的方法和属性,实现接口的类需要提供接口中定义的所有方法。一个类不可能只实现接口的一部分,要么全部实现,要么不实现。
接口也可以继承,就像类一样。
接口可以由 GUID 唯一标识。GUID 是全局唯一标识符的首字母缩写,全局唯一标识符是一个 128 位整数,保证始终是唯一的(理论上)。接口的 GUID 是可选的,但是在 Windows 系统上,使用 COM 时必须使用接口的 GUID。
接口只能在 Delphi 模式或 objfpc 模式下使用。
接口中声明的属性只能通过 Get
和 Set
方式读写。
不能直接创建接口实例,必须创建实现接口的类的实例。
方法定义中只能存在调用约定修饰符。
所有接口都使用引用计数,当接口赋值给变量时,会增加引用计数,当变量超出作用域时,会减少引用计数。当引用计数为零时,会释放实现接口的类实例。
接口定义
type
IUnknown = interface ['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): LongInt;
function _AddRef : LongInt;
function _Release: LongInt;
end;
IInterface = IUnknown;
ISomeInterface = Interface
Function Func1: Int32;
Function Func2: Int32;
end;
GUID
接口可以通过 GUID 标识。这是一个 128 位数字,以文本表示形式如下:
['{HHHHHHHH-HHHH-HHHH-HHHH-HHHHHHHHHHHH}']
每个 H 字符代表一个十六进制数。该格式包含 8+4+4+4+12 个数字。GUID 也可以由以下记录表示,该记录在 objpas
单元中定义(在 Delphi
或 objfpc
模式下自动包含):
type
PGuid = ^TGuid;
TGuid = packed record
case Integer of
1: (
Data1: DWord;
Data2: Word;
Data3: Word;
Data4: array[0..7] of Byte;
);
2: (
D1: DWord;
D2: Word;
D3: Word;
D4: array[0..7] of Byte;
);
3: ( { 符合 RFC4122 的 UUID 字段 }
time_low: DWord;
time_mid: Word;
time_hi_and_version: Word;
clock_seq_hi_and_reserved: Byte;
clock_seq_low: Byte;
node: array[0..5] of Byte;
);
end;
可以使用字符串文本指定 TGUID 类型的常量:
const
SomeGuid: TGuid = '{10101010-1010-0101-1001-110110110110}';
通常,GUID 仅在 Windows 中使用 COM 接口时使用。
接口实现
通常,实现接口的方法的名称必须与接口中定义的名称相同。编译器将在类的继承树中查找相同签名的方法。
但是,可以为实现接口的方法提供别名:
type
ISomeInterface = interface
function Func1: Int32;
end;
TSomeClass = class(TInterfacedObject, ISomeInterface)
function Func2: Int32;
function ISomeInterface.Func1 = Func2;
end;
function TSomeClass.Func2: Int32;
begin
Result := 100;
end;
procedure Test(A: ISomeInterface);
begin
WriteLn(A.Func1());
end;
var
C: TSomeClass;
begin
C := TSomeClass.Create;
Test(C); // 接口有引用计数,对象由接口释放
// 类实例没有引用计数,当赋值给接口时,引用计数从 0 变为 1
// 当接口超出作用域时,引用计数从 1 变成 0,实例被释放。
end.
接口继承
type
IParentInterface = interface
procedure F1;
end;
IChildInterface = interface(IParentInterface)
procedure F2;
end;
TChildClass = class(TInterfacedObject, IChildInterface)
procedure F1;
procedure F2;
end;
TChildClass
的实例可以赋值给 IChildInterface
类型的变量,但不能赋值给 IParentInterface
类型的变量。如果要同时支持两种接口,必须在类声明时明确指定实现两种接口:
TChildClass = class(TInterfacedObject,
IParentInterface,
IChildInterface)
procedure F1;
procedure F2;
end;
接口委托
可以在属性中使用 implements
修饰符指示接口由其它对象委托实现:
type
ISomeInterface = interface
procedure F;
end;
// 实际实现接口
TDelegateClass = class(TInterfacedObject, ISomeInterface)
private
procedure F;
end;
// 委托其它类实现接口
TSomeClass = class(TInterfacedObject, ISomeInterface)
private
FSomeInterface: TDelegateClass;
property SomeInterface: TDelegateClass
read FSomeInterface implements ISomeInterface;
public
constructor Create;
end;
constructor TSomeClass.Create;
begin
FSomeInterface := TDelegateClass.Create;
end;
procedure TDelegateClass.F;
begin
WriteLn('Hello');
end;
procedure Test(A: ISomeInterface);
begin
A.F();
end;
var
C: TSomeClass;
begin
C := TSomeClass.Create;
Test(C);
C.Free;
end.
可以使用单个委托对象实现多个接口:
type
ISomeInterface1 = interface
procedure F;
end;
ISomeInterface2 = interface
procedure F;
end;
// 实际实现接口
TDelegateClass = class(TInterfacedObject, ISomeInterface1, ISomeInterface2)
private
procedure F;
end;
// 委托其它类实现接口
TSomeClass = class(TInterfacedObject, ISomeInterface1, ISomeInterface2)
private
FSomeInterface: TDelegateClass;
property SomeInterface: TDelegateClass
read FSomeInterface implements ISomeInterface1, ISomeInterface2;
public
constructor Create;
end;
constructor TSomeClass.Create;
begin
FSomeInterface := TDelegateClass.Create;
end;
procedure TDelegateClass.F;
begin
WriteLn('Hello');
end;
procedure Test(A: ISomeInterface1; B: ISomeInterface2);
begin
A.F();
B.F();
end;
var
C: TSomeClass;
begin
C := TSomeClass.Create;
Test(C, C);
C.Free;
end.
不能混合使用方法解析和接口委派。这意味着,不可能通过方法解析实现接口的一部分,并通过委托实现接口的另一部分。
接口委派可用于指定类实现父接口:
type
IGetFileName = interface
function GetFileName: String;
property FileName: String read GetFileName;
end;
IGetSetFileName = interface(IGetFileName)
procedure SetFileName(const Value: String);
property FileName: String read GetFileName write SetFileName;
end;
// 实际实现接口(只实现了 IGetSetFileName)
TDelegateClass = class(TInterfacedObject, IGetSetFileName)
private
FFileName: String;
function GetFileName: String;
procedure SetFileName(const Value: String);
end;
// 委托其它类实现接口
TSomeClass = class(TInterfacedObject, IGetFileName, IGetSetFileName)
private
// 这里必须使用 IGetSetFileName 类型而不是 TDelegateClass 类型
FGetSetFileName: IGetSetFileName;
property Implementor: IGetSetFileName
read FGetSetFileName implements IGetFileName, IGetSetFileName;
public
constructor Create;
end;
constructor TSomeClass.Create;
begin
FGetSetFileName := TDelegateClass.Create;
end;
function TDelegateClass.GetFileName: String;
begin
Result := FFileName;
end;
procedure TDelegateClass.SetFileName(const Value: String);
begin
FFileName := Value;
end;
procedure Test(A: IGetFileName; B: IGetSetFileName);
begin
B.SetFileName('Hello');
WriteLn(A.GetFileName());
end;
var
C: TSomeClass;
begin
C := TSomeClass.Create;
Test(C, C);
C.Free;
end.
接口和 COM
在 Windows 上使用 COM 子系统的接口时,调用约定应该是 stdcall
,这不是 Free Pascal 的默认调用约定,因此应该显式指定它。
COM 不认识属性。它只认识方法。因此,当在接口中使用属性时,请注意属性仅在 Free Pascal 编译的程序中是已知的,其他 Windows 程序将不认识属性。
引用计数
所有 COM 接口都使用引用计数,当将接口赋值给变量时,会更新引用计数。当变量超出作用域时,引用计数会自动减少。当引用计数为零时,会释放实现接口的类实例。
变体类型
为了获得最大的变体支持,建议 uses variants
,variants
单元包含对检查和转换变体的支持,而不是 System
或 objpas
单元提供的默认支持。
存储在变体中的值的类型仅在运行时确定:它取决于赋值给变体的内容。几乎任何简单类型都可以赋值给变体。接口和 COM 或 CORBA 对象可以赋值给变体(主要因为它们只是一个指针)。
var
V: Variant;
I: Integer;
begin
V := '100';
I := V; // 字符串可转为数值,如果包含非数字字符,则引发异常
WriteLn(I);
end.
变体数组
变体可以为数组,数组也可以包含变体:
uses
variants;
var
A: array of Variant; // 包含变体的数组
V: Variant; // 存储数组的变体
I: Integer;
begin
SetLength(A, 10);
for I := 1 to 10 do
A[I] := I;
V := VarArrayCreate([1, 10], varInteger);
for I := 1 to 10 do
V[I] := I;
end.
注意,涉及变体的表达式需要更多时间来计算,因此应谨慎使用。如果需要进行大量计算,最好避免使用变体。
变体和接口
编译器中当前中断了对变体的 Dispatch
接口支持。
变体可以包含对接口的引用:普通接口(从 IInterface
派生)或调度接口(从 IDispatch
派生)。调度接口的变体可用于控制其背后的对象,编译器将使用后期绑定来执行对调度接口的调用,不会对函数名称和参数进行运行时检查。也不会检查结果类型。编译器将简单地插入代码进行调度调用并检索结果。
这意味着基本上,你可以在 Windows 上执行以下操作:
var
W: Variant;
V: String;
begin
W := CreateOleObject('Word.Application');
V := W.Application.Version; // 不检查 W 的类型,直接调用
WriteLn('Installed version of MS Word is : ', V);
end;
V := W.Application.Version;
通过插入必要的代码来查询存储在变体 W 中的调度接口来执行,并在找到所需的调度信息时执行调用。
字典
program Project1;
uses
Generics.Collections;
type
TDict = specialize TDictionary<String, Integer>;
var
Dictionary: TDict;
begin
Dictionary := TDict.Create;
Dictionary.Add('abc', 1);
Dictionary.Add('def', 2);
WriteLn(Dictionary['abc']);
WriteLn(Dictionary['def']);
end.
类型别名
定义类型别名的方法如下:
type
Int1 = Integer; // 类型别名
Int2 = type Integer; // 新类型
别名经常用于重新公开类型:
Unit A;
interface
uses
B;
type
MyType = B.MyType;
当重构时,将一些声明从单元 A 移动到单元 B 时,经常会看到这种构造,以保持单元 A 接口的向后兼容性。
类型定义
定义新类型的方法如下:
type
MyInteger = type Integer;
var
A: MyInteger;
B: Integer;
begin
A := B;
end;
从编译器的角度来看,A 和 B 不会具有相同的类型。但是,这两种类型在赋值时是兼容的(就像 Byte
和 Integer
是赋值兼容的一样)。
检查类型信息时可以看出差异:
if TypeInfo(MyInteger) <> TypeInfo(Integer) then
Writeln('MyInteger and Integer are different types');
编译器函数 TypeInfo
返回指向二进制文件中的类型信息的指针。由于 MyInteger
和 Integer
两种类型不同,它们会生成不同类型的信息块,指针也会有所不同。
具有不同的类型有三种后果:
- 它们具有不同的类型信息,因此 RTTI(运行时类型信息)也不同。
- 它们可以用于函数重载,即以下代码会正常工作:
procedure MyProc(A: MyInteger); overload;
procedure MyProc(A: Integer); overload;
- 它们可用于运算符重载,即以下代码也会正常工作:
Operator +(A,B: MyInteger): MyInteger;
类型提升
当在表达式中使用不同类型的整数时,Free Pascal 会自动进行类型转换和提升:
1、每个平台都有一个“本机”整数大小,具体取决于平台是 8 位、16 位、32 位还是 64 位。每个小于“本机”大小的(有符号?)整数都被提升为“本机”大小的有符号版本。等于“本机”大小的整数保持其符号性。
2、二元算术运算符(+、-、* 等)的结果按以下方式确定:
A、如果至少有一个操作数大于本机整数大小,则将结果选择为可以包含两个操作数类型范围的最小类型。这意味着将一个无符号数与一个尺寸小于等于该数的有符号数混合将产生一个大于它们两者的有符号类型。
B、如果两个操作数具有相同的符号,则结果与它们的类型相同。唯一的例外是减法(-):在无符号减无符号的情况下在 FPC 中产生有符号结果(如在 Delphi 中那样,但在 TP7 中没有)。
C、混合“本机”整数大小的有符号和无符号操作数会产生更大的有符号结果。这意味着在 32 位平台上混合 Int32 和 UInt32 将产生 Int64。同样,在 8 位平台 (AVR) 上混合 Int8 和 UInt8 将产生 Int32。
// 在 64 位系统上的结果
var
A: Int8 = 0;
B: UInt8 = 0;
begin
// 类型相加是什么意思?无意中试出来的
WriteLn(Low(A + B )); // -9223372036854775808
WriteLn(Low(Int8 + UInt8 )); // -9223372036854775808
WriteLn(Low(Int16 + UInt16 )); // -9223372036854775808
WriteLn(Low(Int32 + UInt32 )); // -9223372036854775808
WriteLn(Low(Int64 + UInt64 )); // -9223372036854775808
WriteLn(Low(IntPtr + UIntPtr)); // -9223372036854775808
WriteLn(Low(Int64)); // -9223372036854775808
WriteLn(Low(B - B )); // -9223372036854775808
WriteLn(High(B + B )); // 18446744073709551615
WriteLn(High(UInt8 + UInt8 )); // 18446744073709551615
WriteLn(High(UInt64 + UInt64 )); // 18446744073709551615
WriteLn(High(UInt64)); // 18446744073709551615
end.
前向声明
编译器支持延迟解析,但必须在同一个 type
块内部,中间不能被其它块分割,即使是另一个 type
块也不行,因为 type
关键字会导致编译器启动一个新的块范围,与之前的 type
块不属于同一个范围。
type
PList = ^TList; // PList 使用了之后才声明的 TList
TList = Record
Data : Integer;
Next : PList;
end;
前向类型声明仅适用于指针类型和类,不适用于其他类型。
托管类型
默认情况下,Pascal 类型是非托管的。这意味着变量必须显式初始化、终结化、分配内存等。但是,在 Object Pascal 中,托管了几种类型,这意味着编译器将自动初始化和终结化这种类型的变量:这是必需的,例如对于引用计数数据类型。
以下类型是托管类型:
AnsiString 它们被初始化为 nil。
UnicodeString 它们被初始化为 nil。
WideString 它们被初始化为 nil。
接口 它们被初始化为 nil。
动态数组 它们被初始化为 nil。
以及包含托管类型元素的任何记录或数组。
包含托管类型的类实例也会初始化,但类实例指针本身不会初始化。
托管类型的变量也会被终结化:这意味着,一般来说,它们的引用计数最迟会在当前作用域结束时减少。
注意,不应假设此终结化的确切时间。可以保证的是,当它们超出作用域时,它们会被终结化。
类型转换
在进行类型转换时,两个类型的大小必须相同。但对于序数类型(字节、字符、字、布尔、枚举),允许大小不同:
Integer('A');
Char(4875);
boolean(100);
Word(@Buffer);
对于赋值语句,也可以在左值上进行类型转换:
var
A: Int64;
begin
PDouble(@A)^ := 3.2;
end.
IntToStr(10 进制)
var
S: String;
begin
System.Str(100, S);
WriteLn(S);
end.
IntToBin(2 进制)
WriteLn(System.BinStr(15, 8));
IntToOct(8 进制)
WriteLn(System.BinStr(15, 8));
IntToHex(16 进制)
WriteLn(System.HexStr(15, 8));
IntToStr StrToInt(2 - 16 进制)
unit uBaseConvert;
{$Mode Objfpc}{$H+}
interface
const
Digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
function IntToStr(I: Int64; const Base: Integer = 10): String;
function StrToInt(const S: String; const Base: Integer = 10): Int64;
implementation
function IntToStr(I: Int64; const base: Integer = 10): String;
var
R : Int64;
Len : Integer;
PDigits : PChar;
PResult : PChar;
begin
if (Base < 2) or (Base > 16) then Exit('');
SetLength(Result, 64);
PDigits := PChar(Digits);
PResult := PChar(Result) + 64;
repeat
R := I mod Base;
I := I div Base;
PResult -= 1;
PResult^ := PDigits[R];
until I = 0;
Len := 64 - (PResult - PChar(Result));
Move(PResult^, PChar(Result)[0], Len);
SetLength(Result, Len);
end;
function StrToInt(const S: string; const Base: Integer = 10): Int64;
var
C: Char;
I: Integer;
begin
Result := 0;
for C in S do begin
I := Pos(C, Digits) - 1;
if (I < 0) or (I >= Base) then Exit;
Result := Result * Base + I;
end;
end;
procedure Test();
begin
WriteLn(IntToStr(255, 2));
WriteLn(IntToStr(255, 8));
WriteLn(IntToStr(255, 10));
WriteLn(IntToStr(255, 16));
WriteLn(StrToInt('1111', 2));
WriteLn(StrToInt('7777', 8));
WriteLn(StrToInt('9999', 10));
WriteLn(StrToInt('FFFF', 16));
end;
end.
If
if ... then ...
if ... then ... else ...
Case
出现在各个 case
部分中的常量必须在编译时已知,并且可以是序数类型或字符串类型。字符串的情况等效于一系列 if then else
语句,不执行任何优化。
case A of
1 : ...
2, 3: ...
4..6: ...
else ...
end;
case A of
'aaa' .. 'bbb': ...
'ccc' .. 'ddd': ...
end;
For
for I := 0 to 10 do ...
for I := 10 downto 0 do ...
for I in ... do ...
break;
continue;
For 语句中的上下限只计算一次。
不允许在循环内更改循环变量的值。
循环完成后或根本不执行循环时,循环变量的值是未定义的。
如果循环因异常(Exception)、break 或 goto 语句而终时,循环变量将保留其值。
对于子函数(函数内的函数),循环变量必须是子函数的局部变量。循环变量可以是全局变量。
编译器不会明确禁止使用 goto 语句跳转到 for 循环块中,但这样做会导致不可预测的行为。
可枚举表达式可以是以下五种情况之一:
1、枚举。
2、集合。
3、数组。字符串被视为字符数组。
4、可枚举的类、对象或扩展记录。这是支持 IEnumerator 和 IEnumerable 接口的任何结构化类型的实例。
5、定义了枚举器运算符的任何类型。枚举器运算符必须返回实现 IEnumerator 接口的结构化类型。
IEnumerable = interface(IInterface)
function GetEnumerator: IEnumerator;
end;
IEnumerator = interface(IInterface)
function GetCurrent: TObject;
function MoveNext : Boolean;
procedure Reset;
property Current: TObject read GetCurrent;
end;
Current
属性和 MoveNext
方法必须存在于 GetEnumerator
方法返回的类中。Current
属性的实际类型不必是 TObject
。当遇到 for..in
循环时,将类实例作为 in
操作数,编译器将检查以下每个条件:
1、可枚举表达式中的类是否实现了 GetEnumerator
方法。
2、GetEnumerator
的结果是否为具有以下方法的类:
Function MoveNext: Boolean
3、GetEnumerator
的结果是否为具有以下只读属性的类:
property Current: AType;
Current
属性的类型必须与 for...in
的控制变量类型匹配。
实际上不必实现 IEnumerator
和 IEnumerable
接口,编译器将使用上述检查方式来检测这些接口是否存在。定义这些接口只是为了兼容 Delphi,不会在 Free Pascal 内部使用。
type
TCla = class
FData: Integer;
constructor Create(AData: Integer);
function GetEnumerator: TCla; // 必须的方法
function MoveNext: Boolean; // 必须的方法
property Current: Integer read FData; // 必须的属性
end;
constructor TCla.Create(AData: Integer);
begin
FData := AData;
end;
function TCla.MoveNext: Boolean;
begin
FData -= 1;
Result := FData > 0;
end;
function TCla.GetEnumerator: TCla;
begin
Result := Self
end;
var
I: Integer;
A: TCla;
begin
A := TCla.Create(10);
For I in A do
Writeln(I);
end.
type
// 偶数枚举器
TEvenEnumerator = class
FCurrent: Integer; // 自定义字段
FMax: Integer; // 自定义字段
function MoveNext: Boolean; // 必须的方法
property Current: Integer read FCurrent; // 必须的属性
end;
function TEvenEnumerator.MoveNext: Boolean; // 实现必须的方法
begin
FCurrent += 2;
Result := FCurrent <= FMax;
end;
operator enumerator(I: Integer): TEvenEnumerator; // 定义枚举运算符
begin
Result := TEvenEnumerator.Create;
Result.FMax := I;
end;
var
I: Integer;
begin
For I in 10 do
Writeln(I);
end.
定义 enumerator
运算符时必须小心,编译器将查找并使用第一个可用的 enumerator
运算符。对于类而言,这意味着如果有 enumerator
运算符,则不考虑其 GetEnumerator
方法。
Classes
单元包含许多可枚举的类:
TFPList 枚举列表中的所有指针。
TList 枚举列表中的所有指针。
TCollection 枚举集合中的所有项。
TStringList 枚举列表中的所有字符串。
TComponent 枚举组件拥有的所有子组件。
Repeat
repeat
...
until ...
break
continue
While
while ... do ...
break
continue
with
with ... do ...
with A, B do ...
// 等效于
With A do
With B do ...
Goto
为了能够使用 Goto 语句,必须使用 -Sg
编译器开关,或者必须使用 {$ GOTO ON}
。
在 ISO 或 MacPas 模式下,或使用模式开关 NonLocalGoto
,编译器也将允许非本地 Goto。
label
abc, 123;
begin
abc:
...
goto abc;
123:
...
goto 123;
end;
Asm
asm
Movl $1, %ebx
Movl $0, %eax
addl %eax, %ebx
end ['EAX','EBX'];
这将告诉编译器,当它遇到此 asm
语句时,它应该保存和恢复 EAX
和 EBX
寄存器的内容。
Free Pascal 支持各种风格的汇编语法。默认情况下,80386 和兼容平台假定使用 AT&T
语法。可以在代码中使用 {$AsmMode xxx}
开关或 -R
命令行选项更改默认汇编程序样式。
过程
procedure ProcName(...);
begin
...
end;
函数
function FuncName(...): ...;
begin
...
Result := ...
FuncName := ...
Exit(...);
end;
函数的 Result
被视为按引用传递的参数。这对于托管类型尤其重要,函数 Result
在输入时可能为非 nil
值,可能为该类型的有效实例。
参数
为了便于移植,正式参数列表中所有参数的总大小应低于 32K(英特尔版本将其限制为 64K)。
参数可以有默认值。
按引用传递
procedure ProcName(var A: Char; out B: Char; const C: Char);
begin
...
end;
var
参数按引用传递
out
参数按引用传递,忽略初始值,主要用于从函数返回数据
const
参数按引用传递,不允许修改,主要用于减小栈大小,提高性能
const
、out
、var
参数也可以不指定类型。在这种情况下,变量在函数中没有类型,因此与所有其他类型不兼容,编译器只是将变量的地址传递给函数,因此函数中可用的只是一个地址,没有附加任何类型信息:
procedure F(const Data; Len: cint);
等效于以下 C 声明:
void F(void* Data; int Len);
实现方式
对于 const
参数如何传递给底层例程,不应做出任何假设。特别是,通过引用传递大尺寸参数的假设是不正确的。为此,应使用 constref
参数类型。
一个例外是 stdcall
调用约定,为了与 COM 标准兼容,会通过引用传递大型 const
参数。
注意,指定 const
是程序员和编译器之间的约定。是程序员告诉编译器执行例程时 const
参数的内容不会更改,不是编译器告诉程序员参数不会更改(程序员应该保证在使用 const
参数时不会通过某些方法改动其值)。
这在使用引用计数类型时尤其重要且可见。对于这些类型,使用 const
时,将省略任何引用计数的(不可见)递增和递减。这样做通常允许编译器省略这些例程的不可见 try...finally
帧。
作为副作用,以下代码将生成不是预期的输出:
var
S: String = 'AAA';
Procedure DoIt(Const T : String);
begin
S := 'BBB';
Writeln(T);
end;
begin
DoIt(S); // BBB
end.
此行为是设计使然。
变长数组
procedure F(A: array of Ineger);
var
I: Integer;
begin
For I := 0 to High(A) do
WriteLn(A[I]);
end;
部分数组
var
A: array[1..6] of Integer = (1,2,3,4,5,6);
B: array of Integer;
procedure F(A: array of Integer);
var
I: Integer;
begin
For I := 0 to High(A) do
WriteLn(A[I]);
end;
begin
SetLength(B, 6);
B[4] := 10;
B[5] := 20;
B[6] := 30;
F(A); // 1 2 3 4 5 6
F(A[1..3]); // 1 2 3
F(B[4..6]); // 10 20 30
end.
常量数组
procedure F(A: array of const);
begin
...
end;
常量数组的元素被转换为变体类型:
type
PVarRec = ^TVarRec;
TVarRec = record
case VType: Ptrint of
vtInteger : (VInteger : Longint);
vtBoolean : (VBoolean : Boolean);
vtChar : (VChar : Char);
vtWideChar : (VWideChar : WideChar);
vtExtended : (VExtended : PExtended);
vtString : (VString : PShortString);
vtPointer : (VPointer : Pointer);
vtPChar : (VPChar : PChar);
vtObject : (VObject : TObject);
vtClass : (VClass : TClass);
vtPWideChar : (VPWideChar : PWideChar);
vtAnsiString : (VAnsiString: Pointer);
vtCurrency : (VCurrency : PCurrency);
vtVariant : (VVariant : PVariant);
vtInterface : (VInterface : Pointer);
vtWideString : (VWideString: Pointer);
vtInt64 : (VInt64 : PInt64);
vtQWord : (VQWord : PQWord);
end;
因此,在函数体中,常量数组参数等效于变长的 TVarRec 数组:
procedure F(A: array of const);
var
I: LongInt;
begin
if High(A) < 0 then begin
Writeln('无参数');
Exit;
end;
Writeln('共 ', Length(A), ' 个参数:');
For I := 0 to High(A) do begin
Write(' 参数 ', I, ' 的类型为 ');
case A[I].vtype of
vtinteger : Writeln('Integer ,值为:', A[I].vinteger);
vtboolean : Writeln('Boolean ,值为:', A[I].vboolean);
vtchar : Writeln('Char ,值为:', A[I].vchar);
vtextended : Writeln('Extended ,值为:', A[I].VExtended^);
vtString : Writeln('ShortString,值为:', A[I].VString^);
vtAnsiString : Writeln('AnsiString ,值为:', AnsiString(A[I].VAnsiString));
vtPChar : Writeln('PChar ,值为:', A[I].VPChar);
vtPointer : Writeln('Pointer ,值为:', Longint(A[I].VPointer));
vtObject : Writeln('Object ,名称:', A[I].VObject.Classname);
vtClass : Writeln('类引用 ,名称:', A[I].VClass.Classname);
else Writeln('(未知):', A[I].vtype);
end;
end;
end;
var
S: String[6];
P: PChar;
begin
S := 'aaa';
P := PChar('bbb');
F([]);
F([1, True, 'a', 3.2, S, 'ccc', P, @F, nil, TObject]);
end.
注意,Delphi 并非如此,因此依赖于此功能的代码将无法移植。
注意,在常量数组中不支持 DWord
(或 Cardinal
)参数。它们被转换为 vtInteger/vtLongint
。这是为了与 Delphi 兼容,编译器在 Delphi 模式下将忽略任何结果范围检查。
引用计数
函数或过程调用中参数的限定符会影响托管类型的引用计数发生的情况:
1、按值传递:参数的引用计数在进入时增加,在退出时减少。
2、out
:传入值的引用计数减少 1,并且参数初始化为空(通常为 nil,但这是一个不应依赖的实现细节)。
3、var
:引用计数没有任何变化。
4、const
:引用计数没有任何变化。
函数结果在内部被视为函数的 var 参数,并且应用与 var 参数相同的规则。
const
对接口有特殊影响:
type
ITest = Interface
end;
TTest = class(TInterfacedObject, ITest)
end;
procedure F1(A: ITest); // 普通接口参数
begin
// A 的引用计数在进入时加 1,在退出时减 1
// 如果退出时引用计数为 0 则释放 A
end;
procedure F2(const A: ITest); // 常量接口参数
begin
// A 的引用计数不会发生变化,退出时也不会检查 A 的引用计数
end;
var
A: ITest;
B: TTest;
begin
// 创建一个类,类的初始引用计数为 0,在赋值给接口时加 1
A := TTest.Create;
// 创建一个类,类的初始引用计数为 0
B := TTest.Create;
F1(A); // 1 -> (2) -> 1 A 在退出时不被释放
F2(A); // 1 -> (1) -> 1 A 在退出时不被释放
F1(B); // 0 -> (1) -> 0 A 在退出时被释放
F2(B); // 0 -> (0) -> 0 A 在退出时不被释放
end.
函数重载
同名函数,参数的类型或数量不同,以此来区分各个函数调用。
procedure F(A: Integer);
procedure F(A: Boolean);
procedure F(A: String);
procedure F(A: Integer; B: String);
不能重载具有 cdecl
修饰符的函数。(从技术上讲,因为此修饰符可防止编译器修改函数名称)。
如果编译器在一个单元中找不到重载函数的匹配版本,并且存在 overload
关键字,则编译器将继续在其他单元中搜索。
如果不存在 overload
关键字,则所有重载版本必须在同一个单元中,如果是类方法,则必须位于同一个类中(即如果未指定 overload
关键字,编译器将不会在父类中查找重载方法)。
注意,如果重定义(并重载)编译器内部函数,则原始编译器函数将不再可用。例如:
function Inc(A: Integer): Integer;
begin
Result := A + 10;
end;
var
A: Integer;
begin
A := 1;
Inc(A);
WriteLn(A); // 1
end.
前向声明
前向声明一般用于函数相互调用的情况:
procedure F1; forward;
procedure F2();
begin
F1;
end;
procedure F1();
begin
F2;
end;
外部函数
external
修饰符可用于声明驻留在外部对象文件中的函数。它允许在代码中使用该函数,同时,必须在链接时链接包含该函数的目标文件。
procedure F1(); external;
procedure F2();
begin
F1;
end;
如果外部修饰符后跟字符串常量:
external 'name';
这会告诉编译器该函数驻留在名为 name
的库中,编译器会自动链接此库。
还可以指定函数在库中的名称:
external 'name' name 'F';
这告诉编译器该函数驻留在 name
库中,名称为 F
,编译器会自动链接此库,并为函数使用正确的名称。在 Windows 和 OS/2 中,也可以使用以下形式:
external 'name' Index Ind;
这告诉编译器该函数驻留在 name
库中,索引为 Ind
,编译器会自动链接此库,并为函数使用正确的索引。
最后,外部指令可用于指定函数的外部名称:
{$L file.o}
external name 'F';
{$L file.o}
告诉编译器在链接时要链接 file.o
文件。
external
仅指定外部函数的名称为 F
,由链接器自己去查找。
汇编函数
请参阅程序员指南
alias
alias
修饰符允许程序员为函数指定不同的名称。这对于从汇编语言结构引用此函数或从其他对象文件引用此函数非常有用。
指定的别名直接插入到汇编代码中,因此区分大小写。
procedure F1; [public, alias: 'F'];
begin
WriteLn ('Hello');
end;
procedure F2; external name 'F';
begin
asm
call F2
end;
end.
alias
修饰符不会使符号对其他模块公开,除非函数也在单元的 interface
部分声明,或者使用 public
修饰符强制其为公有。
unit unit1;
interface
procedure F1;
implementation
procedure F1; alias:'F';
begin
WriteLn('Hello');
end;
end.
alias
修饰符被视为已弃用。请使用 public name
指令。
cdecl
cdecl
修饰符可用于声明使用 C 类型调用约定的函数。主要用于声明与 C 库进行交互的函数。
{$LinkLib c}
function StrLen(P: PChar): LongInt; cdecl; external name 'strlen';
procedure Test;
var
P: PChar = 'Hello';
begin
WriteLn(P, ' Len: ', StrLen(P));
end;
对于使用 cdecl
声明的非 external
函数,不需要外部链接。这些函数有一些限制,例如不能使用 array of const
(由于它使用栈的方式)。另一方面,cdecl
修饰符允许将这些函数用作用 C 编写的例程的回调,因为后者需要 cdecl
调用约定。
cppdecl
cppdecl
修饰符可用于声明使用 C++ 类型调用约定的函数。主要用于声明与 C++ 库进行交互的函数。
注意,链接到 C++ 支持充其量只是实验性的,因此应格外小心。
export
export
修饰符用于在创建共享库或可执行程序时导出名称。这意味着该符号将公开可用,并且可以从其他程序导入。有关此修饰符的更多信息,请参阅程序员指南中有关“制作库”的部分。
hardfloat
硬浮点修饰符用于指示编译器必须使用在 VFP 寄存器中传递某些浮点参数的调用约定。(仅用于 ARM)
inline
声明了 inline
修饰符的函数,其函数体将被复制到调用它们的位置,这将导致更快的执行速度。很明显,内联大函数没有意义。
默认情况下,不允许内联。必须使用命令行开关 -Si
或 {$ inline on}
指令启用内联。
注意:
内联只是编译器的提示。这并不意味着所有 `inline` 调用都是内联的。有时,编译器可能会决定函数根本无法内联,或者无法内联对该函数的特定调用。如果是这样,编译器将发出警告。
不允许使用递归内联函数。即不允许调用自身的内联函数。
interrupt
interrupt
关键字用于声明将用作中断处理程序的函数。在进入此函数时,所有寄存器将被保存,在退出时,所有寄存器将被恢复,中断或陷阱返回将被执行(而不是子函数指令的正常返回)。
在不存在中断返回的平台上,将改为执行函数的正常退出代码。
iocheck
iocheck
关键字用于声明一个函数,该函数在调用时都会在 {$ IOCHECKS ON}
块中生成 I/O 结果检查代码。
结果是,如果生成对此函数的调用,则编译器将插入 I/O 检查代码(如果调用位于 { $IOCHECKS ON}
块内)。
此修饰符适用于 RTL 内部例程,不适用于应用程序代码。
local
local
修饰符允许编译器优化函数,局部函数不能在单元的接口部分,它总是在单元的实现部分。由此可见,该函数无法从库中导出。
在 Linux 上,local
指令会导致一些优化。在 Windows 上,它不起作用。它是为了与 Kylix 兼容而引入的。
MS_ABI_Default
MS_ABI_Default
修饰符用于指示编译器必须使用 Microsoft 版本的 x86-64 调用约定。这是 Win64 的默认调用约定,仅在 x86-64 上受支持。
MS_ABI_CDecl
MS_ABI_CDecl
修饰符用于指示编译器必须使用 Microsoft 版本的 x86-64 调用约定,但 array of const
被解释为 cdecl varargs
参数,而不是 Pascal 的常规 array of const
数组。
MWPascal
MWPascal
修饰符用于指示编译器必须使用 Metrowerks Pascal
调用约定。此调用约定的行为与 cdecl
相同,不同之处在于 const 记录参数是通过引用而不是按值传递的。在所有平台上受支持。
noreturn
noreturn
修饰符可用于告诉编译器过程不返回。编译器可以使用此信息来避免发出有关未初始化变量或未设置结果的警告。
在以下示例中,编译器不会发出警告,指出结果可能未在函数 f 中设置:
procedure Test; noreturn;
begin
Halt(1);
end;
function F(I : Integer): Integer;
begin
if (I < 0) then
Test
else
Result := I;
end;
begin
Test;
end.
nostackframe
nostackframe
修饰符可用于告诉编译器它不应为此函数生成栈帧。默认情况下,始终为每个过程或函数生成栈帧,但如果可以,编译器将省略它。
对于 asm
函数(纯汇编程序),此指令可用于省略生成栈帧。
使用此修饰符时应非常小心,大多数函数都需要栈帧。特别是对于调试,它们是必需的。
overload
overload
修饰符告诉编译器此函数已重载。它主要是为了与 Delphi 兼容,就像在 Free Pascal 中一样,所有函数都可以在没有这个修饰符的情况下重载。
只有一种情况必需使用 overload
修饰符,如果要重载的函数不在同一个单元中,则被重载的函数都必须使用 overload
修饰符,overload
修饰符告诉编译器它应该继续在其他单元中查找重载版本。
pascal
pascal
修饰符可用于声明使用经典 Pascal 类型调用约定(从左到右传递参数)的函数。有关 Pascal 调用约定的更多信息,请参阅程序员指南。
public
如果函数不应该声明在 interface
部分,但允许从其它目标文件访问,public
修饰符将非常有用。
interface
implementation
procedure F; [public];
begin
WriteLn('Hello');
end;
public
修饰符后面还可以跟一个 name
修饰符来指定汇编程序名称(区分大小写):
procedure F; [public name 'FF'];
register
register
修饰符用于与 Delphi 兼容。对于英特尔 i386 平台,前三个参数在寄存器 EAX、EDX 和 ECX 中传递。对于其他平台,此关键字不起作用,使用默认平台 ABI 调用约定。通常,不建议使用此修饰符,除非您知道自己在做什么。
safecall
safecall
修饰符与 stdcall
修饰符非常相似。它在栈上从右到左发送参数。此外,调用者保存和还原所有寄存器。
有关此修饰符的更多信息可以在程序员指南“调用机制”部分和“链接”章节中找到。
saveregisters
saveregisters
修饰符告诉编译器在调用此函数之前应保存所有 CPU 寄存器。保存哪些 CPU 寄存器完全取决于 CPU。
softfloat
softfloat
修饰符仅在 ARM 体系结构上有意义。它告诉编译器在软件模拟浮点支持处于活动状态时,使用(特定于平台的)调用约定来传递浮点值。(并非所有平台都支持,目前仅 ARM)。
stdcall
stdcall
修饰符在栈上从右向左传递参数,它还将所有参数对齐到默认对齐方式。
有关此修饰符的更多信息可以在程序员指南“调用机制”部分和“链接”章节中找到。
SYSV_ABI_Default
SYS_ABI_Default
修饰符用于指示编译器必须使用符合 System V AMD64 ABI
的特定于 x86-64 的调用约定,这是除 Win64 之外的所有 x86-64 平台的默认调用约定,仅在 x86-64 上受支持。
SYSV_ABI_CDecl
SYSV_ABI_CDecl
修饰符用于指示编译器必须使用符合 System V AMD64 ABI
的特定于 x86-64 的调用约定,但 array of const
被解释为 cdecl varargs
参数,而不是 Pascal 中常规的 array of const
。
VectorCall
VectorCall
调用修饰符用于指示编译器必须使用 VectorCall
调用约定。这是 MS_ABI_Default
调用约定的变体,它在向量寄存器中传递某些浮点参数。仅在 x86-64 CPU 上受支持。
varargs
对于外部 C 函数,此修饰符只能与 cdecl
修饰符一起使用。它指示该函数在最后一个声明的变量之后接受可变数量的参数。这些参数在没有任何类型检查的情况下传递。它等效于 cdecl
过程使用 array of const
,而不必声明 array of const
。使用这种形式的声明时,不需要在可变参数两边使用方括号。
以下声明是引用 C 库中同一函数的两种方式:
procedure F1(Fmt: PChar); cdecl; varargs;
external 'c' name 'printf';
procedure F2(Fmt: PChar; Args: array of const); cdecl;
external 'c' name 'printf';
但它们必须有不同的称呼:
F1('%d %d
',1,1);
F2('%d %d
',[1,1]);
winapi
此修饰符允许你为当前平台指定本机调用约定,然后编译器将根据操作系统体系结构选择正确的调用约定,Windows-i386 上为 stdcall
,所有其他平台为 cdecl
。
function libusb_init(var ctx: plibusb_context): integer;
winapi; external libusb1;
运算符声明
比较运算符或算术运算符的参数列表必须始终包含两个参数,但一元减号或一元加号除外。比较运算符的结果类型必须是布尔值。
用户定义的简单类型以及记录和数组可用于运算符重载。运算符重载有一些限制:
1、仅在 ObjFPC 和 FPC 模式下受支持。
2、无法在类上定义运算符。
3、不能在枚举类型上定义 +
和 -
运算符。
4、当使用 {$modeSwitch ArrayOperators}
模式时,+
运算符就不能在动态数组上重载,因为它由编译器在内部处理。
运行时库的源代码包含两个大量使用运算符重载的单元:
ucomplex 单元包含复数的完整微积分。
matrix 单元包含矩阵的完整微积分。
赋值运算符
type
TPoint = record
X: Integer;
Y: Integer;
end;
// 不允许重载相同类型之间的赋值运算符
//operator := (A: TPoint) Z : TPoint;
//begin
// Z.X := A.X;
// Z.Y := A.Y;
//end;
operator := (A: Integer) Z: TPoint;
begin
Z.X := A; // 或 Result.X := A;
Z.Y := A; // 或 Result.Y := A;
end;
operator := (A: TPoint) Z: Integer;
begin
if A.X > A.Y then Z := A.X else Z := A.Y;
end;
var
A, B: TPoint;
C: Integer;
begin
C := 32;
A := C;
C := A;
B := A;
end.
赋值运算符也用于隐式类型转换。这可能会产生不良影响:
type
TPoint = record
X: Integer;
Y: Integer;
end;
operator := (A: Integer) Z: TPoint;
begin
Z.X := A;
Z.Y := A;
end;
function Succ(A: TPoint): TPoint;
begin
Result.X := A.X + 1;
Result.Y := A.Y + 1;
end;
var
A, B: Integer;
begin
A := 10;
B := Succ(A);
end;
以上代码将给出“类型不匹配”的错误,原因是编译器先找到接收 TPoint
参数的 Succ
函数,它隐式将 A
转换为 TPoint
类型,导致函数结果为 TPoint
类型,不能赋值给 B
,编译器不会进一步寻找另一个具有正确参数的 Succ
函数。可以通过 System.Succ(A)
来避免此问题。
显式类型转换也会调用赋值运算符:
var
R1: T1;
R2: T2;
begin
R2 := T2(R1);
end.
将调用:
operator := (A: T1) Z: T2;
反之则不然,在常规赋值中,编译器不会考虑显式赋值运算符。
uses
SysUtils;
type
T1 = record
F: LongInt;
end;
T2 = record
F: String;
end;
T3 = record
F: Boolean;
end;
// 赋值运算符
operator := (A: T1) Z: T2;
begin
WriteLn('隐式 T1 => T2');
Z.F := IntToStr(A.F);
end;
operator := (A: T1) Z: T3;
begin
WriteLn('隐式 T1 => T3');
Z.F := A.F <> 0;
end;
// 类型转换运算符
operator Explicit(A: T2) Z: T1;
begin
WriteLn('显式 T2 => T1');
Z.F := StrToIntDef(A.F, 0);
end;
operator Explicit(A: T1) Z: T3;
begin
Writeln('显式 T1 => T3');
Z.F := A.F <> 0;
end;
var
A1: T1;
A2: T2;
A3: T3;
begin
A1.F := 42;
A2 := A1; // 隐式 T1 => T2
// 理论上是显式的,但将使用隐式运算符,因为没有定义显式运算符
A2 := T2(A1); // 隐式 T1 => T2
// 以下内容不会编译,也没有定义赋值运算符(此处不会使用显式运算符)
//A1 := A2;
A1 := T1(A2); // 显式 T2 => T1
// 首先显式 (T2 => T1) 然后隐式 (T1 => T3)
A3 := T1(A2); // 显式 T2 => T1 隐式 T1 => T3
A3 := A1; // 隐式 T1 => T3
A3 := T3(A1); // 显式 T1 => T3
end.
算数运算符
可重载的运算符:+ - * / ** ><(包括正号、负号)
type
TPoint = record
X: Integer;
Y: Integer;
end;
operator + (A: TPoint; B: Integer) Z : TPoint;
begin
Z.X := A.X + B;
Z.Y := A.Y + B;
end;
operator + (A: TPoint) Z : TPoint;
begin
Z.X := A.X;
Z.Y := A.Y;
end;
var
A: TPoint;
B: Integer;
begin
A := A + B;
A := B + A; // 无法编译
// 只定义了 TPoint + Integer,未定义 Integer + TPoint
A := +B; // 正号
end.
比较运算符
可重载的运算符:> >= < <= = <>
如果操作数不是简单类型,则比较运算符的结果类型不必总是布尔值。
type
TPoint = record
X: Integer;
Y: Integer;
end;
operator > (A, B: TPoint) Z : String;
begin
if (A.X > B.X) and (A.Y > B.Y) then
Z := 'OK'
else
Z := 'Fail';
end;
var
A, B: TPoint;
begin
WriteLn(A = B);
end.
In 运算符
type
TPoint = record
X: Integer;
Y: Integer;
end;
operator in (const A: Integer; const B: TPoint): Boolean;
begin
Result := (A = B.X) or (A = B.Y);
end;
var
A: Integer;
B: TPoint;
begin
WriteLn(A in B);
end.
逻辑运算符
可重载的运算符:and or xor not
type
TPoint = record
X: Integer;
Y: Integer;
end;
operator and (A, B: TPoint) Z : TPoint;
begin
Z.X := A.X and B.X;
Z.Y := A.Y and B.Y;
end;
var
A, B: TPoint;
begin
A := A and B;
end.
枚举运算符
枚举器运算符可用于定义任何类型的枚举器。它必须返回与 IEnumerator
接口具有相同签名的类、对象或扩展记录。注意,在 Delphi 模式下,编译器无法识别此运算符。
program project1;
type
TEvenEnumerator = class
FCurrent: Integer;
FMax: Integer;
function MoveNext: Boolean;
property Current: Integer read FCurrent;
end;
function TEvenEnumerator.MoveNext: Boolean;
begin
FCurrent := FCurrent + 1;
Result := FCurrent <= FMax;
end;
operator enumerator(I: Integer): TEvenEnumerator;
begin
Result := TEvenEnumerator.Create;
Result.FMax := I;
end;
var
I: Integer;
N: Integer = 10;
begin
For I in N do
Writeln(I);
end.
类助手
{$ModeSwitch TypeHelpers}
type
TObjectHelper = class helper for TObject
procedure Hello();
end;
procedure TObjectHelper.Hello;
begin
WriteLn('Hello');
end;
var
A: TObject;
begin
A := TObject.Create();
A.Hello();
A.Free;
end.
记录助手
{$ModeSwitch AdvancedRecords}
type
TRecord = record
I: Integer;
end;
TRecordHelper = record helper for TRecord
procedure Hello();
end;
procedure TRecordHelper.Hello;
begin
WriteLn('Hello ', I);
end;
var
A: TRecord;
begin
A.Hello();
end.
类型助手
{$ModeSwitch TypeHelpers}
type
TLongIntHelper = type helper for LongInt
constructor Create(AValue: LongInt);
class procedure F1; static;
procedure F2;
end;
constructor TLongIntHelper.Create(AValue: LongInt);
begin
Self := AValue;
F2;
end;
class procedure TLongIntHelper.F1;
begin
Writeln('Hello');
end;
procedure TLongIntHelper.F2;
begin
Writeln('Value :', Self);
end;
var
I: LongInt = 32;
begin
LongInt.F1;
I.F1;
I.F2;
$12345678.F1;
$12345678.F2;
end.
继承
只能使用当前作用域中的最后一个帮助程序类,如果必须使用两个帮助程序的方法,则必须从一个帮助程序类派生另一个帮助程序类。
{$ModeSwitch TypeHelpers}
type
TObjectHelper = class helper for TObject
procedure Hello();
end;
procedure TObjectHelper.Hello;
begin
WriteLn('Hello');
end;
type
TObjectSubHelper = class helper(TObjectHelper) for TObject
procedure World();
end;
procedure TObjectSubHelper.World;
begin
WriteLn('World');
end;
var
A: TObject;
begin
A := TObject.Create();
A.Hello();
A.World();
end.
泛型定义
type
generic TPoint<_T> = class
X: _T;
Y: _T;
procedure Add(A: _T);
end;
procedure TPoint.Add(A: _T);
begin
X := X + A;
Y := Y + A;
end;
type
TIntPoint = specialize TPoint<Integer>;
TRealPoint = specialize TPoint<Real>;
var
A: TIntPoint;
B: TRealPoint;
begin
A := TIntPoint.Create;
A.Add(1);
WriteLn(A.X, ', ', A.Y);
B := TRealPoint.Create;
B.Add(3.2);
WriteLn(B.X, ', ', B.Y);
end.
在编写泛型函数时,有时必须初始化一个变量,其类型在泛型声明期间是未知的。这就需要用到 Default
函数来进行初始化。
子类使用父类的泛型
type
generic TParent<_T> = class
protected type
_TT = _T;
end;
TSub = class(specialize TParent<Integer>)
A: _TT;
procedure F();
end;
procedure TSub.F;
begin
WriteLn(A);
end;
var
A: TSub;
begin
A := TSub.Create;
A.F;
end.
泛型类型限定
如果模板类型必须从某个类衍生而来,则可以在模板列表中指定
// _T 必须为 TA 或 TA 的后代类
generic T<_T: TA> = class
// 可以指定多个泛型
generic T<T1, T2: TA; T3: TB> = class
// T 必须为 TA 或 TA 的后代类,必须实现 IA,IB 接口
generic T<T: TA, IA, IB> = class
异常处理
raise
语句将引发异常,在 try
语句块中引发的异常会被 except
捕获到:
try
raise TObject.Create; // 引发异常
except on TObject do // 捕获 TObject 异常
Exit; // 处理 TObject 异常
else // 捕获其它异常
raise; // 向上传递捕获到的异常
end;
异常实例必须是任何类的初始化实例。如果省略异常实例,则会重新引发当前异常。在 SysUtils
单元中定义了许多异常类。
异常地址
异常地址和帧地址是可选的。如果未指定,编译器将自行提供地址。下面的示例指定了异常地址和帧地址:
uses SysUtils;
procedure Error(const Msg: String);
begin
raise Exception.Create(Msg) at
get_caller_addr (get_frame), // 使用调用者的地址报告异常
get_caller_frame(get_frame); // 使用调用者的栈帧报告异常
end;
procedure Test;
begin
Error('Error');
end;
begin
Test;
end.
将引发以下异常:
An unhandled exception occurred at $0000000000401100:
Exception: Error
$0000000000401100 TEST, line 12 of project1.lpr
$000000000040111E main, line 16 of project1.lpr
第 12 行是 Test 函数中调用 Error 的地方,而不是在 Error 中引发异常的地方,虽然 Error 才是实际引发异常的函数,但它将异常地址修改成了其调用者 Test。
finally
如果 try
语句块中有 finally
关键字,则:
1、在执行 Exit 时,会首先执行 finally
中的语句,然后再实际退出。
2、在 try 中出现异常时,会首先执行 finally
中的语句,然后重新引发异常。
try
try
raise TObject.Create;
finally
WriteLn('Finally');
end;
except on TObject do
Exit;
else
raise;
end;
序数操作
Inc(I) // 序数自增(I += 1)
Dec(I) // 序数自减(I -= 1)
Prev(I) // 获取前一个序数(I + 1)
Succ(I) // 获取后一个序数(I - 1)
Ord(I) // 获取序数的索引值
类型转换
// 第二个参数是结果宽度
BinStr(I, Len) // 整数转二进制
OctStr(I, Len) // 整数转八进制
HexStr(I, Len) // 整数转十六进制
常用 fpc 参数
-Mobjfpc 支持 Object Pascal 的 FPC 模式
-Sc 支持 C 样式的运算符(*=, +=, /=, -=)
-Sg 启用 LABEL 和 GOTO(在 -Mtp 和 -Mdelphi 中默认)
-Sh 使用引用计数字符串(即默认情况下使用 AnsiString 而不是 ShortString)
-Si 打开声明为 inline 的过程/函数的内联
-CX 同时创建智能链接库
-Cg 生成 PIC 代码
-O3 3 级优化(-O2 + 慢速优化)
-XX 尝试智能链接单元(定义 FPC_LINK_SMART)
-l 写 Logo
-vewnhibq 要显示的消息类型(e Error,w Warning,n Note, h Hint,i Info,b FullPath,q MessageNumber)
-Fi Include 路径
-Fu Unit 路径
-FU Unit 输出路径
-FE Exe 输出路径
-o Exe 路径
完整 fpc 参数
$fpc -h
Free Pascal 编译器 版本 3.2.2 [2021/07/09] 用于 x86_64
Copyright (c) 1993-2021 by Florian Klaempfl and others
fpc [选项] <输入文件> [选项]
仅列出对默认或选定平台有效的选项(当前为 Linux)。
在布尔开关选项后放置 + 以启用它,- 禁用它。
@<x> 除了默认的 fpc.cfg 之外,还可以从 <x> 中读取编译器选项。
-a 编译器不删除生成的汇编文件
-a5 不要为 2.25 以上的 GNU Binutils 生成 Big Obj COFF 文件(Windows, NativeNT)
-al 列出汇编文件中的源代码行
-an 列出汇编文件中的节点信息(-dEXTDEBUG 编译器)
-ao 向外部汇编程序调用添加一个额外的选项(对于内部,忽略此选项)
-ap 使用管道而不是创建临时汇编文件
-ar 在汇编文件中列出寄存器分配/释放信息
-at 在汇编文件中列出临时分配/释放信息
-A<x> 输出格式:
-Adefault 使用默认汇编器进行汇编
-Aas 使用 GNU AS 进行汇编
-Agas 使用 GNU GAS 进行汇编
-Agas-darwin 使用 GNU GAS 汇编 darwin Mach-O64 对象文件
-Amasm 使用 ml64 汇编 Win64 对象文件(Microsoft)
-Apecoff PE-COFF(Win64)使用内部写入程序
-Aelf ELF(Linux-64bit)使用内部写入程序
-Ayasm 使用 Yasm 进行汇编(实验)
-Anasm 使用 Nasm 进行汇编(实验)
-Anasmwin64 使用 Nasm 汇编 Win64 对象文件(实验)
-Anasmelf 使用 Nasm 汇编 Linux-64bit 对象文件(实验)
-Anasmdarwin 使用 Nasm 汇编 darwin macho64 对象文件(实验)
-b 生成浏览器信息
-bl 生成本地符号信息
-B 构建所有模块
-C<x> 代码生成选项:
-C3 启用 IEEE 常量错误检查
-Ca<x> 选择 ABI;有关可能的值,请参阅 fpc -i 或 fpc -ia
-Cb 为目标体系结构的 big-endian 变体生成代码
-Cc<x> 将默认调用约定设置为 <x>
-CD 同时创建动态库(不支持)
-Ce 使用模拟的浮点数操作码进行编译
-Cf<x> 选择要使用的 fpu 指令集;有关可能的值,请参阅 fpc -i 或 fpc -if
-CF<x> 最小浮点常量精度(default,32,64)
-Cg 生成 PIC 代码
-Ch<n>[,m] <n> 字节最小堆大小(介于 1023 和 67107840 之间)和可选的 [m] 最大堆大小
-Ci IO 检查
-Cn 省略链接阶段
-Co 整数运算的溢出检查
-CO 检查整数运算是否可能溢出
-Cp<x> 选择指令集;有关可能的值,请参见 fpc -i 或 fpc -ic
-CP<x>=<y> 打包设置
-CPPACKSET=<y> <y> 集合分配: 0, 1 或 DEFAULT 或 NORMAL, 2, 4 和 8
-CPPACKENUM=<y> <y> 枚举打包: 0, 1, 2 and 4 or DEFAULT or NORMAL
-CPPACKRECORD=<y> <y> 记录打包: 0 or DEFAULT or NORMAL, 1, 2, 4, 8, 16 and 32
-Cr 范围检查
-CR 验证对象方法调用的有效性
-Cs<n> 将栈检查大小设置为 <n>
-Ct 堆叠检查(仅用于测试,请参阅手册)
-CT<x> 目标特定代码生成选项
-CTcld 在使用 x86 字符串指令之前发出 CLD 指令
-CX 同时创建智能链接库
-d<x> 定义符号 <x>
-D 生成 DEF 文件
-Dd<x> 将描述设置为 <x>
-Dv<x> 将 DLL 版本设置为 <x>
-e<x> 设置可执行文件的路径
-E 与 -Cn 相同(省略链接阶段)
-fPIC 与 -Cg 相同(生成 PIC 代码)
-F<x> 设置文件名和路径:
-Fa<x>[,y] (对于 program)在解析 uses 之前加载 <x> 和 [y] 单元
-Fc<x> 将输入代码页设置为 <x>
-FC<x> 将 RC 编译器二进制名称设置为 <x>
-Fd 禁用编译器的内部目录缓存
-FD<x> 设置搜索编译器相关工具的目录
-Fe<x> 将错误输出重定向到 <x>
-Ff<x> 将 <x> 添加到框架路径(仅限 Darwin)
-FE<x> 将 exe/unit 输出路径设置为 <x>
-Fi<x> 将 <x> 添加到 include 路径
-Fl<x> 将 <x> 添加到 library 路径
-FL<x> 使用 <x> 作为动态链接器
-Fm<x> 从编译器目录中的 <x>.txt 加载 unicode 转换表
-FM<x> 设置搜索 unicode 二进制文件的目录
-FN<x> 将 <x> 添加到默认单元的作用域(命名空间)列表中
-Fo<x> 将 <x> 添加到 object 路径
-Fr<x> 加载 error 消息文件 <x>
-FR<x> 将资源(.res)链接器设置为 <x>
-Fu<x> 将 <x> 添加到 unit 路径
-FU<x> 将 unit 输出路径设置为 <x>,覆盖 -FE 选项
-FW<x> 在 <x> 中存储生成的整个程序优化反馈
-Fw<x> 从 <x> 加载以前存储的整个程序优化反馈
-g 生成调试信息(目标的默认格式)
-gc 生成指针检查(实验性的,仅在某些目标上可用,可能会生成假的良好结果)
-gh 使用 heaptrace 单元(用于内存泄漏/损坏调试)
-gl 使用行信息单元(通过回溯显示更多信息)
-gm 生成 Microsoft CodeView 调试信息(实验)
-go<x> 设置调试信息选项
-godwarfsets 启用 DWARF 'set' 类型调试信息(breaks gdb<6.5)
-gostabsabsincludes 在 Stabs 中存储绝对/完全 include 文件路径
-godwarfmethodclassprefix 用类名作为 DWARF 中方法名的前缀
-godwarfcpp 在 DWARF 中模拟 C++ 调试信息
-godwarfomflinnum 除了 DWARF 调试信息外,还以 MS LINK 格式在 OMF LINNUM 记录中生成行号信息(Open Watcom Debugger/Linker 兼容性)
-gp 在插入符号名称中保留大小写
-gs 生成 Stabs 调试信息
-gt Trash 本地变量(用于检测未初始化的使用;多个 t 会更改 Trash 值)
-gv 生成可通过 Valgrind 跟踪的程序
-gw 生成 DWARFv2 调试信息(与 -gw2 相同)
-gw2 生成 DWARFv2 调试信息
-gw3 生成 DWARFv3 调试信息
-gw4 生成 DWARFv4 调试信息(实验性)
-i 信息
-iD 返回编译器日期
-iSO 返回编译器操作系统
-iSP 返回编译器主机处理器
-iTO 返回目标操作系统
-iTP 返回目标处理器
-iV 返回短编译器版本
-iW 返回完整的编译器版本
-ia 返回支持的 ABI 目标列表
-ic 返回支持的 CPU 指令集列表
-if 返回支持的 FPU 指令集列表
-ii 返回支持的内联汇编程序模式列表
-io 返回支持的优化列表
-ir 返回已识别的编译器和 RTL 功能的列表
-it 返回支持的目标列表
-iu 返回支持的微控制器类型列表
-iw 返回支持的整个程序优化列表
-I<x> 将 <x> 添加到 include 路径
-k<x> 将 <x> 传递给链接器
-l 写 Logo
-M<x> 将语言模式设置为 <x>
-Mfpc Free Pascal 方言(默认)
-Mobjfpc 支持 Object Pascal 的 FPC 模式
-Mdelphi Delphi 7 兼容模式
-Mtp TP/BP 7.0 兼容模式
-Mmacpas Macintosh Pascal 方言兼容模式
-Miso ISO 7185 模式
-Mextendedpascal ISO 10206 模式
-Mdelphiunicode Delphi 2009 及其之后版本兼容模式
-n 不读取默认配置文件
-o<x> 将生成的可执行文件的名称更改为 <x>
-O<x> 优化:
-O- 禁用优化
-O1 1 级优化(快速且对调试器友好)
-O2 2 级优化(-O1 + 快速优化)
-O3 3 级优化(-O2 + 慢速优化)
-O4 4 级优化(-O3 + 优化,可能会产生意想不到的副作用)
-Oa<x>=<y> 设置对齐
-Oo[NO]<x> 启用或禁用优化;有关可能的值,请参阅 fpc -i 或 fpc -io
-Op<x> 针对目标 cpu 进行优化;有关可能的值,请参阅 fpc -i 或 fpc -ic
-OW<x> 生成针对 <x> 优化的整个程序优化反馈;有关可能的值,请参阅 fpc -i 或 fpc -iw
-Ow<x> 执行整个程序优化 <x>;有关可能的值,请参阅 fpc -i 或 fpc -iw
-Os 优化尺寸而不是速度
-pg 为 gprof 生成配置文件代码(定义 FPC_PROFILE)
-P<x> 目标 CPU/编译器相关选项:
-PB 显示默认编译器二进制文件
-PP 显示默认目标 CPU
-P<x> 设置目标 CPU(aarch64, arm, avr, i386, i8086, jvm, m68k, mips, mipsel, powerpc, powerpc64, sparc,x86_64)
-R<x> 汇编阅读风格:
-Rdefault 对目标使用默认汇编器
-Ratt 读 AT&T 样式的汇编器
-Rintel 读 Intel 样式的汇编器
-S<x> 语法选项:
-S2 等同于 -Mobjfpc
-Sc 支持 C 样式的运算符 (*=,+=,/= and -=)
-Sa 启用断言
-Sd 等同于 -Mdelphi
-Se<x> 错误选项。<x> 是以下各项的组合:
<n> : 编译器在遇到第 <n> 个 error 后停止(默认值为 1)
w : 编译器也会在 warning 后停止
n : 编译器也会在 note 后停止
h : 编译器也会在 hint 后停止
-Sf 在编译器和 RTL 中启用某些功能;有关可能的值,请参阅 fpc -i 或 fpc -ir
-Sg 启用 LABEL 和 GOTO(在 -Mtp 和 -Mdelphi 中默认)
-Sh 使用引用计数字符串(即默认情况下使用 AnsiString 而不是 ShortString)
-Si 打开声明为 inline 的过程/函数的内联
-Sj 允许类型化常量可写(在所有模式下都是默认值)
-Sk 加载 fpcylix 单元
-SI<x> 将接口样式设置为 <x>
-SIcom COM 兼容接口(默认)
-SIcorba CORBA 兼容接口
-Sm 支持 C 样式的宏(全局)
-So 等同于 -Mtp
-Sr ISO 模式下的透明文件名
-Ss 构造函数名称必须是 init(必须执行析构函数)
-Sv 支持向量处理(如果可用,则使用 CPU 向量扩展)
-Sx 启用 exception 关键字(Delphi/OjFPC 模式下的默认值)
-Sy @<pointer> 返回一个类型化指针,与 $T+ 相同。
-s 不要调用汇编程序和链接器
-sh 生成脚本以在主机上链接
-st 生成脚本以在目标上链接
-sr 跳过寄存器分配阶段(与 -alr 一起使用)
-T<x> 目标操作系统:
-Taros AROS
-Tdarwin Darwin/Mac OS X
-Tdragonfly DragonFly BSD
-Tembedded Embedded
-Tfreebsd FreeBSD
-Tiphonesim iPhoneSimulator
-Tlinux Linux
-Tnetbsd NetBSD
-Topenbsd OpenBSD
-Tsolaris Solaris
-Twin64 Win64 (64 位 Windows 系统)
-u<x> 取消定义符号 <x>
-U 单元选项:
-Un 不检查单元名是否与文件名匹配
-Ur 生成 release 单元文件(从不自动重新编译)
-Us 编译一个系统单元
-v<x> 冗长模式,<x> 是以下字母的组合:
e : 显示 error(默认值) 0 : 不显示任何内容(除了 error)
w : 显示 warning u : 显示单元信息
n : 显示 note t : 显示已尝试/已使用的文件
h : 显示 hint c : 显示条件
i : 显示一般信息 d : 显示调试信息
l : 显示行号 r : Rhide/GCC 兼容模式
s : 显示时间戳 q : 显示消息号
a : 显示所有 x : 显示有关调用的工具的信息
b : 使用完整路径写文件名消息 p : 使用 parse tree 写入 tree.log
v : 写带有大量调试信息的 fpcdebug.txt
z : 将输出写入 stderr
m<x>,<y> : 不显示编号为 <x> 和 <y> 的消息
-V<x> 将 '-<x>' 附加到所使用的编译器二进制文件名之后(例如,对于版本)
-W<x> 目标特定选项(target)
-WA 指定本机类型应用程序(Windows)
-Wb 创建捆绑包而不是库(Darwin)
-WB 创建可重新定位的映像(Windows)
-WB<x> 将映像基准设置为 <x>(Windows)
-WC 指定控制台类型的应用程序(Windows)
-WD 使用 DEFFILE 导出 DLL 或 EXE 的函数(Windows)
-We 使用外部资源(Darwin)
-WG 指定图形类型应用程序(Windows)
-Wi 使用内部资源(Darwin)
-WI 打开/关闭 import 区段的使用(Windows)
-WM<x> 最低 Mac OS X 部署版本:10.4, 10.5.1, ...(Darwin)
-WN 不生成调试所需的重新定位代码(Windows)
-WP<x> 最低 iOS 部署版本:8.0, 8.0.2, ...(iphonesim)
-WR 生成重新定位代码(Windows)
-WX 启用可执行栈(Linux)
-X 可执行文件选项:
-X9 为早于 2.19.1 版本(Linux)的 GNU Binutils ld 生成链接脚本
-Xc 将 --shared/-dynamic 传递给链接器(BeOS、Darwin、FreeBSD、Linux)
-Xd 不搜索默认库路径(有时不使用 -XR 交叉编译时需要)
-Xe 使用外部链接器
-Xf 将 pthread 库名称替换为链接 (BSD)
-Xg 在单独的文件中创建调试信息,并将 debuglink 区段添加到可执行文件
-XD 尝试动态链接单元(定义 FPC_LINK_DYNAMIC)
-Xi 使用内部链接器
-XLA 定义用于链接的库替换
-XLO 定义库链接的顺序
-XLD 排除标准库的默认顺序
-Xm 生成链接映射
-XM<x> 设置 'main' 程序例程的名称(默认为 'main')
-Xn 使用目标系统本机的链接器而不是 GNU ld(Solaris、AIX)
-Xp<x> 首先在 <x> 目录中搜索编译器二进制文件
-XP<x> 在 binutils 名称前面加前缀 <x>
-Xr<x> 将链接器的 rlink 路径设置为 <x>(交叉编译需要,请参阅 ld 手册了解更多信息)(BeOS,Linux)
-XR<x> 将 <x> 添加到所有链接器搜索路径的开头(BeOS、Darwin、FreeBSD、Linux、Mac OS、Solaris)
-Xs 修剪(Strip)可执行文件中的所有符号
-XS 尝试静态链接单元(默认值,定义 FPC_LINK_STATIC)
-Xt 使用静态链接(将 -static 传递给链接器)
-Xv 为“虚拟条目调用”生成表
-XV 使用 VLink 作为外部链接器(Amiga、MorphOS 上的默认值)
-XX 尝试智能链接单元(定义 FPC_LINK_SMART)
-? 分页显示本帮助
-h 显示本帮助
项目目录结构
https://wiki.freepascal.org/Lazarus_project_files
Lazarus 项目的典型目录结构:
backup
自动创建,无需版本控制
包含项目源文件的备份副本。
lib
自动创建,无需版本控制
包含适用于不同 CPU 和操作系统的已编译二进制文件。
<Project name>.app
自动创建,无需版本控制
macOS(darwin)“应用程序包”文件(macOS 特别处理扩展名为 .app
的文件夹)。
frames
手动创建,需要版本控制
TFrame 相关文件(*.lfm
,*.pas
),建议用于具有许多框架的大型项目。
include
手动创建,需要版本控制
依赖于平台的 include。包含不同操作系统(darwin、linux、win32、win64 等)的子目录。建议用于具有特定平台单元的多平台项目。
languages
手动创建,需要版本控制
包含 *.po
文件,其中包含不同语言的资源字符串。建议用于多语言项目。
images
手动创建,需要版本控制
图片文件(*.png
,*.jpg
,*.ico
,*.xpm
等),可以通过 lazres
实用程序编译成资源。建议用于包含许多图像文件的项目和包。
logs
手动创建,无需版本控制
调试输出文件(*.txt
、*.log
)。
项目文件扩展名
.lpi
需要版本控制,Lazarus 工程信息
包含特定于项目的设置,如编译器设置和所需的包。以 XML 格式存储。
.lps
无需版本控制,Lazarus 程序段
个人数据,如光标位置、源代码编辑器文件、个人构建模式。以 XML 格式存储。
.lpr
需要版本控制,Lazarus 程序
主程序的 Pascal 源代码。
.lfm
需要版本控制,Lazarus 窗体
窗体上所有对象的窗体配置信息(以特定于 Lazarus 的文本格式存储,类似于 Delphi dfm
;操作由相应的 *.pas
文件中的 Pascal 源代码描述)。
.pas
需要版本控制,Pascal 代码
通常用于存储在相应 *.lfm
文件中的窗体。
.pp
, .p
需要版本控制,Pascal 代码
如果你想避免与 Delphi 源代码文件混淆,这很有用。
.inc
需要版本控制,Pascal 代码
包含在 Pascal 代码文件中。通常包含依赖于平台的定义和例程。
.lrs
无需版本控制,Lazarus 资源
生成的 Lazarus 资源文件;不要与 Windows 资源文件混淆。可以使用 lazres
工具(在目录 Lazarus/Tools 中)通过命令 lazres myfile.lrs myfile.lfm
创建此文件。
.compiled
无需版本控制,FPC 编译状态
编译器不生成任何 .compiled
文件。它们是由一些构建系统(make
、fpmake
、lazbuild
)创建的。
.ppu
无需版本控制,已编译的单元
由 Free Pascal 编译器为每个单元和程序创建的编译后的文件。
.o
, .or
无需版本控制,对象文件
由编译器创建,每个 .ppu
文件都有一个链接器所需的相应 .o
文件。
.lpk
需要版本控制,Lazarus 包信息
特定于包的设置,例如编译器设置和所需的包;以 XML 格式存储。
.lrt
无需版本控制,Lazarus 资源字符串表
Lazarus resourcestring
表是在保存 lfm
文件时创建的(需要启用 i18n)。它包含 lfm
的 TTranslateString
属性。不要编辑它们,它们会被覆盖。
.rst
无需版本控制,资源字符串表
编译器为每个单元创建的带有 resourcestring
部分的 resourcestring
表。不要编辑它们,它们会被覆盖。
.rc
需要版本控制,资源定义文件
Delphi 兼容的资源定义脚本(.rc
文件),用于描述应用程序使用的资源。
.po
需要版本控制,GNU gettext
文件
当 i18n 被启用时,IDE 会使用 rst
和 lrt
文件中的资源字符串创建/更新 .po
文件。
.ico
需要版本控制,图标文件
应用程序文件的图标。
.res
根据情况进行版本控制,二进制资源
Delphi 中基本上有三种类型:
1)自动生成的 .res
文件。
2)其他有其源代码(.rc
)的 .res
文件,并在项目或源代码中使用相应的 .rc
文件进行声明,如 {$R 'logo.res' 'logo.rc'}
。
3)其他没有其源代码的 .res
文件。
所以第一类不需要版本控制(本质上与 .dcu
/.o
或 .ppu
等其他预先生成的文件相同)。第二类基本上也不需要,因为它们可以重新生成。应保留第 3 类。在较旧的 Delphi 中,这些文件通常用于在 Delphi 不知道 Windows 版本的情况下启用主题。这对 Lazarus 来说问题不大。你也可以将类别 2 转换为 3,只需包含 .res
(删除 .rc
源代码),这样你就不必向版本控制中添加图像等。