字符串初始化和测量

我们从 字符串类型 章节已经知道,在代码中只需描述一个 string 类型的变量,该代码将可以执行。

对于 string 类型的任何变量,为服务结构体分配 12 个字节,该服务结构体是该字符串的内部表示。该结构体包含文本存储所在的内存地址(指针)信息以及一些其它元信息。文本本身也要求充足的内存,但该缓冲区的分配没有采用多少优化措施。

具体而言,我们可以描述字符串及其显式初始化,包括空字面量:

string s = ""// pointer to the literal containing '\0'

在这种情况下,指针将直接设置到该字面量,没有为该缓冲区分配内存(即使该字面量较长)。很明显,已为该字面量分配了静态内存,可直接使用。除非程序中的任何指令更改了该行的内容,否则将为缓冲区分配内存。例如(注意字符串允许加法运算 '+'):

int n = 1;
s += (string)n;    // pointer to memory containing "1"'\0'[plus reserve]

此后,字符串实际上包含文本 "1",并且严格来说,需要占用两个字符的内存:数字 "1" 和隐式终结符零 '\0'(该字符串的终结符)。然而,系统将分配一个较大的缓冲区,其中预留了某些空间。

若声明一个没有初始值的变量,编译器仍然隐式初始化该变量,虽然在这种情况下是以特殊 NULL 值初始化:

string z// memory for the pointer is not allocated, pointer = NULL

这种字符串对于每个结构体仅需要 12 个字节,指针不指向任何地方:这就是 NULL 代表的含义。

在将来版本的 MQL5 编译器中,此行为可能更改,最初始终为空字符串分配小块内存,提供某些预留空间。

除了这些内部特性外,string 类型的变量与其它类型的变量没有区别。然而,由于字符串的长度可能变化,更重要的是,在运算期间它们可能更改其长度,这会对内存分配效率和性能造成不利影响。

例如,如果在某个点,程序需要向字符串添加一个新字,但可能为该字符串分配的内存却不够。然后,在用户觉察不到的情况下,MQL 程序执行环境将找到新的增加了大小的空闲内存块,并将旧值随添加的新字一同拷贝到其中。之后,旧地址被该行的服务结构体中的一个新地址所替换。

如果这样的操作数量众多,由于拷贝导致的性能降低可能变得明显,此外,程序内存会碎片化:拷贝后释放的旧的小内存区域会形成一些空隙,这些空隙不适合大字符串使用,从而导致内存浪费。当然,终端能够控制此类情况并重新整理内存,但这也是有代价的。

解决这一问题最有效的方法,是事先明确指定字符串的缓冲区大小,并使用内置 MQL5 API 函数将其初始化,这些函数将在本节后文中探讨。

这一优化的依据是,分配的内存大小可超过字符串的当前长度以及可能的将来长度,该长度由文本中的第一个空字符确定。这样,我们可以分配 100 个字符的缓冲区,但从一开始将 '\0' 放在最开头,这将提供一个零长度字符串 ("")。

当然,此类情况下假定编程人员能够事先粗略计算字符串的预期长度及其增长速度。

由于 MQL5 中的字符串是基于双字节字符(其确保 Unicode 支持),以字符数表示的字符串和缓冲区大小应乘以 2,以获得占用内存量和分配内存量(以字节数表示)。

本节末尾会提供使用所有函数的一般示例 (StringInit.mq5)。

bool StringInit(string &variable, int capacity = 0, ushort character = 0)

StringInit 函数用于初始化(分配和填充内存)和反初始化(释放内存)字符串。要处理的变量在第一个参数中传递。

如果 capacity 参数大于 0,则为字符串分配指定大小的缓冲区(内存区域),并以 character 符号填充。如果 character 为 0,则字符串长度将为零,因为第一个字符是终止符。

如果 capacity 参数为 0,则之前分配的内存被释放。如果变量刚刚声明但还未初始化,变量的状态变为与声明之前的状态相同(缓冲区的指针为 NULL)。实现相同效果更简单的方法是将字符串变量设置为 NULL。

函数返回成功指示器 (true) 或错误 (false)。

bool StringReserve(string &variable, uint capacity)

StringReserve 函数增加或减少 variable 字符串的缓冲区大小,至少为 capacity 参数中指定的字符数。如果 capacity 值小于当前字符串长度,则函数不执行任何操作。实际上,缓冲区大小可能大于请求的大小:环境如此分配是出于该字符串将来操纵效率的考虑。因此,如果以小于缓冲区的值调用该函数,该函数可能忽略请求,仍然返回 true(“无错误”)。

当前缓冲区大小可使用 StringBufferLen 函数(见下文)获取。

如果成功,函数返回 true,否则返回 false

不同于 StringInitStringReserve 函数不更改字符串的内容,也不会使用字符填充。

bool StringFill(string &variable, ushort character)

StringFill 函数使用 character 字符填充指定 variable 的整个当前长度(直至第一个零)。如果为字符串分配了缓冲区,修改原位完成,没有中间换行和拷贝操作。

函数返回成功指示器 (true) 或错误 (false)。

int StringBufferLen(const string &variable)

函数返回为 variable 字符串分配的缓冲区大小。

请注意,对于字面量初始化的字符串,最初不会分配缓冲区,因为指针指向该字面量。因此,即使 StringLen 字符串(见下文)的长度可能更大,该函数也将返回 0。

值 -1 表示该行属于客户端终端,不能更改。

bool StringSetLength(string &variable, uint length)

该函数为 variable 字符串设置指定的字符长度 length length 的值不得大于字符串的当前长度。换言之,该函数只允许你缩短该字符串,而不能加长该字符串。调用 StringAdd 函数或者执行了加法运算 '+’ 时,会自动增加该字符串的长度。

函数 StringSetLength 的等效执行是 StringSetCharacter(variable, length, 0) 调用(参见 处理符号和代码页章节)。

如果在函数调用前已为字符串分配了缓冲区,则该函数不会更改缓冲区。如果字符串没有缓冲区(其指向字面量),则减少长度会分配新缓冲区并将缩短的字符串拷贝到该缓冲区。

如果成功或失败,函数分别返回 truefalse

int StringLen(const string text)

该函数返回字符串 text 中的字符数。终止零字符不计入。

请注意,该参数由值传递,因此,你不仅可以计算变量中的字符串长度,而且可以计算任何其它中间值的字符串长度:计算结果值或字面量。

创建了 StringInit.mq5 脚本以演示上述函数。它使用特殊版本的 PRT 宏 (PRTE),其将表达式的结果解析为 truefalse,并且对于后者,还输出错误代码:

#define PRTE(APrint(#A"=", (A) ? "true" : "false:" + (string)GetLastError())

为了将字符串及其当前指标(行长度和缓冲区大小)调试输出到日志,实现了 StrOut 函数:

void StrOut(const string &s)
{
   Print("'"s"' ["StringLen(s), "] "StringBufferLen(s));
}

该函数使用内置 StringLenStringBufferLen 函数。

测试脚本对 OnStart 中的一个字符串执行一系列操作:

void OnStart()
{
   string s = "message";
   StrOut(s);
   PRTE(StringReserve(s100)); // ok, but we get a buffer larger than requested: 260
   StrOut(s);
   PRTE(StringReserve(s500)); // ok, buffer is increased to 500
   StrOut(s);
   PRTE(StringSetLength(s4)); // ok: string is shortened
   StrOut(s);
   s += "age";
   PRTE(StringReserve(s100)); // ok: buffer remains at 500
   StrOut(s);
   PRTE(StringSetLength(s8)); // no: string lengthening is not supported
   StrOut(s);                   //     via StringSetLength
   PRTE(StringInit(s8, '$')); // ok: line increased by padding
   StrOut(s);                   //     buffer remains the same
   PRTE(StringFill(s0));      // ok: string collapsed to empty because
   StrOut(s);                   //     was filled with 0s, the buffer is the same
   PRTE(StringInit(s0));      // ok: line is zeroed, including buffer
                                // we could just write s = NULL;
   StrOut(s);
}

该脚本将记录以下消息:

'message' [7] 0
StringReserve(s,100)=true
'message' [7] 260
StringReserve(s,500)=true
'message' [7] 500
StringSetLength(s,4)=true
'mess' [4] 500
StringReserve(s,10)=true
'message' [7] 500
StringSetLength(s,8)=false:5035
'message' [7] 500
StringInit(s,8,'$')=true
'$$$$$$$$' [8] 500
StringFill(s,0)=true
'' [0] 500
StringInit(s,0)=true
'' [0] 0

请注意,若 StringSetLength 调用时增加字符串长度,则结束时会出现错误 5035 (ERR_STRING_SMALL_LEN)。