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。