Pascal 编程/字符串
数据类型 string(…)
用于存储有限的 char
值序列。它是 array
的特例,但与 array[…] of char
不同,数据类型 string(…)
具有某些优势,有助于有效使用它。
数据类型 string(…)
如这里所示是 ISO 标准 10206 中定义的 *扩展* Pascal 扩展。由于它在实践中的高度相关性,本主题已放在本维基教科书的标准 Pascal 部分,紧接在关于 数组 的章节之后。
许多编译器对构成 string 的内容有不同的理解。请参阅 *他们的* 手册了解他们特有的差异。请放心,GPC 支持 string(…) ,如这里所述。 |
声明 string
数据类型总是需要一个 *最大容量*
program stringDemo(output);
type
address = string(60);
var
houseAndStreet: address;
begin
houseAndStreet := '742 Evergreen Trc.';
writeLn('Send complaints to:');
writeLn(houseAndStreet);
end.
在单词 string
之后跟着一个 *正* 整数,用括号括起来。这不是函数调用。[fn 1]
如上所述,数据类型 address
的变量只能存储 *最多* 60
个独立的 char
值。当然,可以存储更少,甚至存储 0
,但一旦设置了此限制,就无法扩展。
String
变量“知道”它们自己的最大容量:如果您使用 writeLn(houseAndStreet.capacity)
,这将打印 60
。每个 string
变量自动具有一个名为 capacity
的“字段”。此字段通过写入相应的 string
变量的名称以及用点 (.
) 连接的单词 capacity
来访问。此字段是只读的:您不能向它赋值。它只能出现在表达式中。
所有 string
变量都有当前 *长度*。这是每个 string
变量当前包含的合法 char
值的总数。要查询此数字,EP 标准定义了一个名为 length
的新函数
program lengthDemo(output);
type
domain = string(42);
var
alphabet: domain;
begin
alphabet := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
writeLn(length(alphabet));
end.
length
函数返回一个非负的 integer
值,表示所提供字符串的长度。它还接受 char
值。[fn 2] 一个 char
值的长度按定义为 1
。
可以保证 string
变量的 length
将始终小于或等于其相应的 capacity
。
可以使用 :=
运算符复制整个字符串值,前提是 LHS 上的变量具有与 RHS 字符串表达式相同或更大的 capacity。这与普通 array
的行为不同,它要求维度和大小完全匹配。
program stringAssignmentDemo;
type
zipcode = string(5);
stateCode = string(2);
var
zip: zipcode;
state: stateCode;
begin
zip := '12345';
state := 'QQ';
zip := state; // ✔
// zip.capacity > state.capacity
// ↯ state := zip; ✘
end.
只要没有发生剪切,即由于容量太短而省略的值,赋值就可以了。
值得注意的是,否则字符串在内部被视为数组。[fn 3] 就像一个字符数组,您可以通过指定方括号内有效的索引来独立访问(和更改)每个数组元素。但是,在索引的有效性方面存在很大差异。在任何时候,您只能指定在范围 1..length
内的索引。此范围可能为空,特别是如果 length
当前为 0
。
无法通过操作单个 string 组件来更改当前长度program stringAccessDemo;
type
bar = string(8);
var
foo: bar;
begin
foo := 'AA'; { ✔ length ≔ 2 }
foo[2] := 'B'; { ✔ }
foo[3] := 'C'; { ↯: 3 > length }
end.
|
除了length
函数,EP 还定义了一些其他对字符串进行操作的标准函数。
以下函数返回字符串。
为了获得 string
(或 char
)表达式的部分内容,函数 subStr(stringOrCharacter, firstCharacter, count)
返回 stringOrCharacter
的子字符串,该子字符串具有非负长度 count
,从正索引 firstCharacter
开始。重要的是 firstCharacter + count - 1
是 stringOrCharacter
中有效的字符索引,否则该函数将导致错误。[fn 4]
program substringDemo(output);
begin
writeLn(subStr('GCUACGGAGCUUCGGAGUUAG', 7, 3));
{ char index: 1 4 7 … }
end.
GAG
firstCharacter
索引。这里我们想要提取第三个密码子。但是,firstCharacter
不仅仅是 2 * 3
,而是 2 * 3 + 1
。在 string
变量中对字符进行索引从 1
开始。请注意,用于编码密码子的复杂实现不会使用 string
,而是定义自定义的枚举数据类型。对于string
变量,subStr
函数与指定 myString[firstCharacter..firstCharacter+count]
相同。[fn 5] 显然,如果 firstCharacter
值是某个复杂的表达式,则应首选 subStr
函数以防止出现任何编程错误。
string
的部分内容。
program substringOverwriteDemo(output);
var
m: string(35);
begin
m := 'supercalifragilisticexpialidocious ';
m[21..35] := '-yadi-yada-yada';
writeLn(m);
end.
supercalifragilistic-yadi-yada-yada
string
的长度。此外,subStr
的第三个参数可以省略:这将简单地返回给定string
从第二个参数指示的位置开始的剩余部分。[fn 6]
trim(source)
函数返回 source
的副本,不包含任何尾部空格字符,即 ' '
。在LTR 脚本中,任何右侧的空格都被认为是无关紧要的,但在计算中,它们占用(内存)空间。建议在将字符串写入磁盘或其他长期存储介质或通过网络传输之前对其进行修剪。不可否认,在 21 世纪之前,内存需求是一个更相关的问题。
函数 index(source, pattern)
在 source
中查找 pattern
的第一次出现,并返回起始索引。来自 pattern
的所有字符都与 source
中返回偏移量的字符匹配
1 | 2 | 3 | ✘ | |||||
模式
|
X
|
Y
|
X
|
|||||
---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | ✘ | |||||
模式
|
X
|
Y
|
X
|
|||||
1 | 2 | 3 | ✔ | |||||
模式
|
X
|
Y
|
X
|
|||||
源
|
Z
|
Y
|
X
|
Y
|
X
|
Y
|
X
| |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
请注意,要获得第二次或任何后续出现,您需要使用 source
的适当子字符串。
因为从数学角度来说,“空字符串”无处不在,index(characterOrString, '')
始终返回 1
。相反,因为任何非空字符串都不能出现在空字符串中,index('', nonEmptyStringOrCharacter)
始终返回 0
,在字符串的上下文中,这是一个否则无效的索引。如果 pattern
不出现在 source
中,则返回零值。如果 pattern
比 source
更长,则情况始终如此。
EP 标准为任何长度的字符串(包括单个字符)引入了另一个运算符。 +
运算符连接两个字符串或字符,或任何组合。与算术 +
不同,此运算符非交换,这意味着操作数的顺序很重要。
表达式 | 结果 |
---|---|
'Foo' + 'bar'
|
'Foobar'
|
'' + ''
|
''
|
'9' + chr(ord('0') + 9) + ' Luftballons'
|
'99 Luftballons'
|
如果你想将数据保存到某个地方,连接很有用。但是,将连接后的字符串提供给像 write
/writeLn
这样的例程,可能会带来不利:连接,特别是长字符串的连接,首先需要分配足够的内存来容纳整个结果字符串。然后,所有操作数都将复制到它们各自的位置。这需要时间。因此,在 write
/writeLn
的情况下,建议(对于非常长的字符串)使用它们接受无限数量(逗号分隔)参数的功能。
注意,常见的 LOC
stringVariable := 'xyz' + someStringOrCharacter + …;
等价于
writeStr(stringVariable, 'xyz', someStringOrCharacter, …);
后者在你想填充结果或需要一些转换时特别有用。写 foo:20
(最小宽度为 20
个字符,可能用空格 ' '
左填充)只能使用 write
/writeLn
/writeStr
。 WriteStr
是 EP 扩展。
GPC、FPC 和 Delphi 也附带了一个函数 concat
,执行相同的任务。在使用它之前,请阅读各自编译器的文档,因为有一些差异,或者只坚持使用标准化的 +
运算符。
复杂的比较
[edit | edit source]本小节中介绍的所有函数都返回一个 Boolean
值。
顺序
[edit | edit source]由于字符串中的每个字符都有一个序数值,我们可以考虑一种对它们进行排序的方法。有两种比较字符串的方法
- 一种方法使用已经介绍过的关系运算符,例如
=
、>
或<=
。 - 另一种方法是使用专用函数,例如
LT
或GT
。
它们的差异在于它们对长度不同的字符串的处理方式。前者会通过使用空格字符 (' '
) 填充 它们来使两个字符串具有相同的长度,而后者只会将它们剪切到最短的长度,但会考虑哪个字符串更长(如果有必要)。
函数名称 | 含义 | 运算符 |
---|---|---|
EQ |
等于 | =
|
NE |
不等于 | <>
|
LT |
小于 | <
|
LE |
小于或等于 | <=
|
GT |
大于 | >
|
GE |
大于或等于 | >=
|
所有这些函数和运算符都是二元的,这意味着它们分别期望和接受两个参数或操作数。如果提供相同的输入,它们可能会产生不同的结果,你将在接下来的两个小节中看到。
相等性
[edit | edit source]让我们从相等性开始。
- 如果两个操作数的长度相同,并且值,即构成字符串的字符序列相同,则
EQ
函数认为两个字符串(任何长度)是相等的。 - 另一方面,
=
比较会通过使用填充字符空格 (' '
) 来增加较短字符串中任何“缺失”的字符。[fn 7]
program equalDemo(output);
const
emptyString = '';
blankString = ' ';
begin
writeLn(emptyString = blankString);
writeLn(EQ(emptyString, blankString));
end.
True
False
emptyString
被填充以匹配 blankString
的长度,然后执行实际的逐字符 =
表达式。换句话说,Pascal 中你已经知道的术语
(foo = bar) = EQ(trim(foo), trim(bar))
实际实现通常不同,因为 trim
可能非常消耗资源(时间和内存),特别是对于长字符串而言。
因此,如果尾随空格无关紧要,但出于技术原因仍然存在(例如,因为你正在使用 array[1..8] of char
),通常使用 =
比较。只有 EQ
才能确保两个字符串在词法上相同。请注意,任一字符串的 capacity
与此无关。函数 NE
(不等于的缩写)的行为也类似。
小于
[edit | edit source]通过依次从左到右同时读取两个字符串,并比较相应的字符,可以确定一个字符串“小于”另一个字符串。如果所有字符都匹配,则这两个字符串被认为是相等的。但是,如果我们遇到一个不同的字符对,则中止处理,当前字符的关系决定整个字符串的关系。
第一个操作数 | 'A'
|
'B'
|
'C'
|
'D'
|
---|---|---|---|---|
第二个操作数 | 'A'
|
'B'
|
'E'
|
'A'
|
确定的关系 | =
|
=
|
<
|
⨯ |
如果两个字符串的长度相等,则 LT
函数和 <
运算符的行为相同。 LT
实际上甚至建立在 <
的基础上。如果提供的字符串的长度不同,事情就会变得有趣。
LT
函数首先将两个字符串都剪切到相同的(较短的)长度。(子字符串)- 然后执行常规比较,如上所述。如果缩短的版本,公共长度的版本结果是相等的,则(最初)较长的字符串被认为大于另一个字符串。
<
比较将所有剩余的“缺失”字符与空格字符 ' '
进行比较。这会导致不同的结果。
program lessThanDemo(output);
var
hogwash, malarky: string(8);
begin
{ ensure ' ' is not chr(0) or maxChar }
if not (' ' in [chr(1)..pred(maxChar)]) then
begin
writeLn('Character set presumptions not met.');
halt; { EP procedure immediately terminating the program }
end;
hogwash := '123';
malarky := hogwash + chr(0);
writeLn(hogwash < malarky, LT(hogwash, malarky));
malarky := hogwash + '4';
writeLn(hogwash < malarky, LT(hogwash, malarky));
malarky := hogwash + maxChar;
writeLn(hogwash < malarky, LT(hogwash, malarky));
end.
False True
True True
True True
<
比较时, hogwash
中的“缺失”第四个字符被假定为 ' '
。 malarky
中的第四个字符与 ' '
进行比较。上面的情况是出于演示目的而人为造成的,但如果您经常使用比普通空格字符“小”的字符,例如您在使用 ATASCII 的 1980 年代 8 位 Atari 计算机上编程,则这仍然会成为问题。 LE
、 GT
和 GE
函数将相应地执行。
有关 string
文字的详细信息
[edit | edit source]包含分隔符
[edit | edit source]在 Pascal 中, string
文字以相同字符开头并以相同字符结束。通常情况下,这个字符是直引号(打字机的) '
。如果您想在 string
文字中实际包含该字符,就会出现问题,因为您要包含在字符串中的字符已被理解为结束分隔符。按照惯例,两个连续的直引号被视为引号图像。在生成的计算机程序中,它们将被替换为单个引号。
program apostropheDemo(output);
var
c: char;
begin
for c := '0' to '9' do
begin
writeLn('ord(''', c, ''') = ', ord(c));
end;
end.
每个双引号将被替换为单个引号。字符串仍然需要分隔引号,因此您最终可能得到三个连续的引号,如上面的示例所示,甚至可能得到四个连续的引号( ''''
),如果您想要一个由单个引号组成的 char
值。
不允许的字符
[edit | edit source]一个 string
是字符的线性序列,即沿单个维度排列。
因此,字符串中唯一的非法“字符”是标记换行符(新行)的字符。以下代码段中的字符串文字是不可接受的,因为它跨越多个(源代码)行。 welcomeMessage := 'Hello!
All your base are belong to us.';
|
尽管如此,您仍然可以根据 OS 使用指示 EOL 的特定代码,但唯一跨平台(即保证无论使用的是哪种 OS 都将起作用)的过程是 writeLn
。虽然没有标准化,但许多编译器提供一个常量来表示环境中产生换行符所需的字符/字符串。在 FPC 中,它被称为 lineEnding
。Delphi 有 sLineBreak
,出于兼容性原因, FPC 也支持它。 GPC 的标准模块 GPC
提供常量 lineBreak
。您需要先 import
此模块,然后才能使用该标识符。
余数运算符
[edit | edit source]在学习了 除法 之后,您将接触到的最后一个标准 Pascal 算术运算符是余数运算符 mod
(模数的缩写)。每个 integer
除法( div
)可能会产生余数。此运算符将评估为该值。
i
|
-3
|
-2
|
-1
|
0
|
1
|
2
|
3
|
---|---|---|---|---|---|---|---|
i mod 2
|
1
|
0
|
1
|
0
|
1
|
0
|
1
|
i mod 3
|
0
|
1
|
2
|
0
|
1
|
2
|
0
|
与所有其他除法运算一样, mod
运算符不接受零值作为第二个操作数。此外, mod
的第二个操作数必须为正数。在计算机科学家和数学家之间,关于除数为负数时结果的定义有很多。Pascal 通过简单地声明负除数是非法的来避免任何混淆。
mod
运算符经常用于确保某个值保持在从零开始的特定范围内( 0..n
)。此外,您还将在 数论 中找到模数。例如,素数的定义是“不能被任何其他数字整除”。此表达式可以用 Pascal 翻译成这样
表达式 | 可以被 整除 |
---|---|
数学符号 | |
Pascal 表达式 | x mod d = 0
|
odd(x) 是 x mod 2 <> 0 的简写形式。[fn 8] |
任务
[edit | edit source]array[n..m] of string(c)
中访问单个字符?string(…)
本质上是 array
的一种特殊情况(即由 char
值组成的数组),因此您可以像往常一样访问其中的单个字符:v[i, p]
,其中 i
是范围 n..m
中的有效索引,而 p
指的是 1..length(v[i])
内的字符索引。
string(…)
包含非空白字符(即除 ' '
以外的字符)时返回 true
。program spaceTest(input, output);
type
info = string(20);
{**
\brief determines whether a string contains non-space characters
\param s the string to inspect
\return true if there are any characters other than ' '
*}
function containsNonBlanks(s: info): Boolean;
begin
containsNonBlanks := length(trim(s)) > 0;
end;
// … remaining code for testing purposes only …
注意,此函数(正确地)在提供空字符串 (''
) 时返回 false
。或者您可以编写
containsNonBlanks := '' <> s;
string(…)
数据类型才能正常工作。请记住,在这些练习中没有“最佳”解决方案。
program
,它读取一个 string(…)
,并将其中的每个字母相对于其在英文字母表中的位置移动 13 位,然后输出修改后的版本。此算法称为“凯撒密码”。为简单起见,假设所有输入都是小写。program rotate13(input, output);
const
// we will only operate ("rotate") on these characters
alphabet = 'abcdefghijklmnopqrstuvwxyz';
offset = 13;
type
integerNonNegative = 0..maxInt;
sentence = string(80);
var
secret: sentence;
i, p: integerNonNegative;
begin
readLn(secret);
for i := 1 to length(secret) do
begin
// is current character in alphabet?
p := index(alphabet, secret[i]);
// if so, rotate
if p > 0 then
begin
// The `+ 1` in the end ensures that p
// in the following expression `alphabet[p]`
// is indeed always a valid index (i.e. not zero).
p := (p - 1 + offset) mod length(alphabet) + 1;
secret[i] := alphabet[p];
end;
end;
writeLn(secret);
end.
array[chr(0)..maxChar] of char
) 的实现也是可以接受的,但必须注意正确填充它。注意,不能保证像 succ('A', 13)
这样的表达式会产生预期结果。范围 'A'..'Z'
不一定是连续的,因此您不应对其进行任何假设。如果您的解决方案使用了它,您必须对其进行 *记录*(例如,“此程序仅在使用 ASCII 字符集 的计算机上正常运行。”)。
string
是否是 回文,这意味着它可以正向 *和* 反向读取,产生相同的含义/声音,前提是单词间隙(空格)经过相应调整。为简单起见,假设所有字符都是小写,并且没有标点符号(除了空格)。program palindromes(input, output);
type
sentence = string(80);
{
\brief determines whether a lower-case sentence is a palindrome
\param original the sentence to inspect
\return true iff \param original can be read forward and backward
}
function isPalindrome(original: sentence): Boolean;
var
readIndex, writeIndex: integer;
derivative: sentence;
check: Boolean;
begin
check := true;
// “sentences” that have a length of one, or even zero characters
// are always palindromes
if length(original) > 1 then
begin
// ensure `derivative` has the same length as `original`
derivative := original;
// the contents are irrelevant, alternatively [in EP] you could’ve used
//writeStr(derivative, '':length(original));
// which would’ve saved us the “fill the rest with blanks” step below
writeIndex := 1;
// strip blanks
for readIndex := 1 to length(original) do
begin
// only copy significant characters
if not (original[readIndex] in [' ']) then
begin
derivative[writeIndex] := original[readIndex];
writeIndex := writeIndex + 1;
end;
end;
// fill the rest with blanks
for writeIndex := writeIndex to length(derivative) do
begin
derivative[writeIndex] := ' ';
end;
// remove trailing blanks and thus shorten length
derivative := trim(derivative);
for readIndex := 1 to length(derivative) div 2 do
begin
check := check and (derivative[readIndex] =
derivative[length(derivative) - readIndex + 1]);
end;
end;
isPalindrome := check;
end;
var
mystery: sentence;
begin
writeLn('Enter a sentence that is possibly a palindrome (no caps):');
readLn(mystery);
writeLn('The sentence you have entered is a palindrome: ',
isPalindrome(mystery));
end.
original
string
的修改后的 *过滤* 版本。为了演示目的,示例显示了 if not (original[readIndex] in [' ']) then
。实际上,*显式* 集合列表会更合适,即 if original[readIndex] in ['a', 'b', 'c', …, 'z']) then
。如果您只是编写了类似于 if original[readIndex] <> ' ' then
的内容,请不要担心,鉴于任务的要求,这同样很好。
LT('', '')
的结果是什么?
function
,用于确定公历中的年份是否为 闰年。每四年是一年闰年,但每百年不闰,除非是连续的第四个世纪。mod
运算符 的典型示例{
\brief determines whether a year is a leap year in Gregorian calendar
\param x the year to inspect
\return true, if and only if \param x meets leap year conditions
}
function leapYear(x: integer): Boolean;
begin
leapYear := (x mod 4 = 0) and (x mod 100 <> 0) or (x mod 400 = 0);
end;
sysUtils
unit
或 GPC 的 GPC
module
中已经有一个预制好的 function
isLeapYear
。尽可能重复使用已经可用的代码。
function
返回年份的闰年属性后,编写一个二进制 function
返回给定月份和年份中的天数。case
语句 的典型情况。请记住,结果变量必须 *正好* 赋值一次type
{ a valid day number in Gregorian calendar month }
day = 1..31;
{ a valid month number in Gregorian calendar year }
month = 1..12;
{
\brief determines the number of days in a given Gregorian year
\param m the month whose day number count is requested
\param y the year (relevant for leap years)
\return the number of days in a given month and year
}
function daysInMonth(m: month; y: integer): day;
begin
case m of
1, 3, 5, 7, 8, 10, 12:
begin
daysInMonth := 31;
end;
4, 6, 9, 11:
begin
daysInMonth := 30;
end;
2:
begin
daysInMonth := 28 + ord(leapYear(y));
end;
end;
end;
dateUtils unit
提供了一个名为 daysInAMonth
的 function
。强烈建议您重复使用 *它* 而不是您自己的代码。更多练习可以在
注意
- ↑ 实际上,这是对 EP 称为“模式”的一种区分。 模式 将在本书的扩展部分详细解释。
- ↑ 此功能在处理您或其他人可能在某个时候更改的 *常量* 时很有用。根据定义,字面量值
' '
是一个char
值,而''
(“空字符串”)或'42'
是字符串字面量。为了编写通用代码,length
接受所有可能表示char
值的有限序列的值。 - ↑ 实际上,定义本质上是
packed array[1..capacity] of char
。 - ↑ 这意味着,在 *空* 字符串的情况下,*只有* 以下函数调用 *可能是* 合法的
subStr('', 1, 0)
。不用说,这样的函数调用非常无用。 - ↑ 在使用此表示法时,字符串变量可能不可 *绑定*。
- ↑ 在空字符串或字符的情况下省略第三个参数是不允许的。
subStr('', 1)
是非法的,因为空字符串中没有“字符1
”。同样,subStr('Z', 1)
也不允许,因为'Z'
是一个char
表达式,因此始终长度为1
,使得任何需要“给我剩余/后续字符的”函数都变得过时了。 - ↑ 如果您是 GPC 用户,您需要确保您处于完全兼容 EP 的模式,例如通过在命令行中指定
‑‑extended‑pascal
。否则,不会进行填充。根据 ISO 标准 7185,标准(未扩展)Pascal 没有定义任何填充算法。 - ↑
odd
的实际实现可能不同。在许多处理器架构中,它通常类似于 x86 指令and x, 1
。