跳轉至內容

Pascal 程式設計/例程

來自 Wikibooks,開放世界的開放書籍

在第一章已經提到了例程。例程,如之前所述,是可以重複使用的程式碼片段,可以反覆使用。例程的示例包括read / readLnwrite / writeLn。您可以根據需要呼叫這些例程任意多次。在本章中,您將學習

  • 如何定義您自己的例程,
  • 定義和宣告之間的區別,以及
  • 函式和過程之間的區別。

不同場合的不同例程

[編輯 | 編輯原始碼]

例程有兩種形式。在 Pascal 中,例程可以替換語句,也可以替換(子)表示式。可以在允許使用語句的地方使用的例程稱為procedure(過程)。作為表示式的一部分呼叫的例程是function(函式)。

function(函式)是一種返回值的例程。Pascal 定義了包括odd在內的多個函式。函式odd將一個integer(整數)表示式作為引數,並根據提供的引數的奇偶性返回falsetrue(通俗地說,就是它是否能被 2 整除)。讓我們看看函式odd 的實際應用

program functionDemo(input, output);
var
	x: integer;
begin
	write('Enter an integer: ');
	readLn(x);
	
	if odd(x) then
	begin
		writeLn('Now this is an odd number.');
	end
	else
	begin
		writeLn('Boring!');
	end;
end.

Odd(x) 讀作“x 的奇偶性”。首先,評估括號中的表示式。這裡它僅僅是x,更確切地說,是變數的值,但只要最終計算結果為integer(整數)表示式,也允許更復雜的表示式。然後,此表示式的(即實際引數)被傳遞到一個(在本例中不可見的)程式碼塊,該程式碼塊處理輸入,對其執行一些計算,並根據計算結果返回falsetrue。函式的返回值最終將填充到函式呼叫的位置。在您的腦海中,您可以將false / true 替換為odd(x),儘管這取決於給定的輸入而動態變化。

另一方面,過程不能用作表示式的部分。您只能在允許使用語句的地方呼叫過程。

procedure(過程)可以使用函式,反之亦然。不要將function(函式)僅僅理解為表示式的替代品。在下一節中,我們將學習原因。

基本原理

[編輯 | 編輯原始碼]

例程的二分法,區分procedure(過程)和function(函式),旨在溫和地推動程式設計師編寫“乾淨”的程式。這樣做,例程不會隱藏它只是一個語句序列的替代品,還是一個複雜、難以寫出的表示式的簡寫。這種表示法在不引入討厭的偽型別(例如,C 程式語言中的void,其中每個例程都是一個函式,但“無效”資料型別void 將允許您使其(部分)表現得像procedure)的情況下起作用。

定義例程遵循您從第一個program 開始就熟悉的模式。program 在某些方面類似於特殊的例程:您可以透過 OS 定義的方式執行它任意多次。program 的定義幾乎與例程的定義完全一樣。

例程由以下部分定義:

  1. 一個頭部,以及
  2. 一個塊

按照這個順序。例程頭部根據它是function還是procedure,顯示出一些差異。我們首先看看塊,因為它們對這兩種型別的例程都是一樣的。

一個是生產性部分(語句)和(可選的)宣告和定義的綜合。在標準 Pascal(如 ISO 標準 7185 所述)中,塊具有固定的順序:[fn 2]

  1. 常量定義(const 部分)
  2. 型別定義(type 部分)
  3. 變數宣告(var 部分)
  4. 例程宣告和定義
  5. 序列(begin end,可能為空)

除了最後一個生產性部分之外,所有專案都是可選的。

Note 部分(consttypevar 部分)不能為空。一旦您指定了部分標題,您就必須在剛剛開始的部分中至少定義/宣告一個符號。

EP 中,已取消固定順序限制。在那裡,部分和例程宣告以及定義可以根據需要出現多次,並且不必遵循特定順序。結果在“範圍”一章中詳細介紹。本書其餘部分將參考 EP的定義,因為所有主要的編譯器都支援這一點。但是,標準 Pascal 定義的順序是一個很好的指導方針:在可能使用這些型別(即 var 部分)的部分之前定義型別是有意義的。

例程頭部由以下部分組成:

  1. 單詞 functionprocedure
  2. 標識此例程的識別符號,
  3. 可能的引數列表,以及,
  4. 最後,對於函式,呼叫此函式產生的表示式的型別,即結果資料型別

例程的引數列表還定義每個引數的資料型別。因此,函式 odd 的頭部可能如下所示

function odd(x: integer): Boolean;

請注意引數列表之後的分號 (:),它分隔函式的結果資料型別。您可以將函式視為一種特殊的變數宣告,它也用分號分隔識別符號,但對於函式,"變數"的值是動態計算的。

形式引數,即例程頭部上下文中的引數,用分號分隔。考慮以下過程頭部

procedure printAligned(x: integer; tabstop: integer);

請注意,每個例程頭部都以分號結尾。

雖然例程頭部告訴處理器(通常是編譯器),“嘿,有一個具有以下屬性的例程:[…]”,但這還不夠。您必須“充實”,給例程一個主體。這是在隨後的塊中完成的。

在塊內,所有引數都可以像變數一樣讀取。

函式結果

[編輯 | 編輯原始碼]

在定義函式的塊的序列中,自動存在一個函式名稱的變數。您必須恰好分配一次值,以便函式在數學上定義。參考此示例

function getRandomNumber(): integer;
begin
	// chosen by fair dice roll,
	// guaranteed to be random
	getRandomNumber := 4;
end;

請注意,該塊不包含宣告變數 getRandomNumbervar 部分,但它已由函式的頭部隱式宣告:名稱和資料型別都是函式頭部的一部分。

例程宣告大多數情況下是隱式的。宣告例程或一般任何識別符號是指向處理器(即通常是編譯器)提供資訊以正確解釋您的程式原始碼的過程。此資訊不會直接編碼在您的可執行程式中,但它隱式存在。示例包括:

  • 變數宣告告訴處理器安裝適當的規定以預留一些記憶體空間。這塊記憶體將根據其關聯的資料型別進行解釋。但是,變數的名稱或資料型別都不會以任何方式儲存在您的程式中。只有處理器在讀取您的原始碼檔案時才知道此資訊。
  • 例程頭部構成例程宣告(通常緊隨其定義之後[fn 3])。同樣,例程頭部中提供的資訊不會直接儲存在可執行檔案中,但它們可以確保處理器(編譯器)正確轉換您的原始碼。
  • 同樣,type 宣告僅用於乾淨和抽象的程式設計,但這些宣告不會最終出現在可執行程式檔案中。[fn 4]

宣告使識別符號能夠表示某個物件(從數學上講是“物件”)。另一方面,定義將根據其名稱定義此物件的確切含義。無論是常量的值、變數的值還是例程中採取的步驟(語句序列),透過定義定義的資料都將在您的可執行檔案中生成特定程式碼,這可能會根據相關宣告中提供的資訊而有所不同;編寫具有資料型別 integer 的變數與編寫型別 real 的值根本不同。正確儲存、計算和檢索 integerreal 值的程式碼不同,但計算機並不知道這一點。它只是執行給定的指令,例如,某些指令類似於 Pascal 資料型別 real 上的操作,從某種意義上說,這是一個“巧合”。

呼叫例程

[編輯 | 編輯原始碼]

例程根據其簽名進行選擇。例程簽名由以下部分組成:

  1. 例程的名稱,
  2. 所有引數的資料型別,以及
  3. (隱式)它們的正確順序。

因此,函式 odd 的簽名讀取為 odd(integer)。名為 odd 的函式接受一個 integer 值作為第一個(也是唯一一個)引數。

Note 在其他一些程式語言中,返回值的資料型別也屬於例程的簽名。如果您在程式語言之間切換,請記住術語簽名的不同定義。

Pascal 允許你宣告和定義同名的過程,但它們的形式引數不同。這通常稱為過載。當呼叫過程時,必須正好有一個同名過程接受引數及其對應的資料型別。

預定義過程

[編輯 | 編輯原始碼]
Pascal 的預定義函式(節選)
簽名 描述 返回值型別
abs(integer) 引數的絕對值 整數
odd(integer) 奇偶性(給定值是否能被二整除) 布林值
sqr(integer) 值的平方 整數

持久變數

[編輯 | 編輯原始碼]

一些編譯器,例如 FPC,允許你使用常量就像變數一樣,但它們的生命週期不同。在下面的示例中,“常量” numberOfInvocations 在整個程式執行期間都存在,但只能在其宣告的範圍內訪問。

program persistentVariableDemo(output);
{$ifDef FPC}
	// allow assignments to _typed_ “constants”
	{$writeableConst on}
{$endIf}

procedure foo;
const
	numberOfInvocations: integer = 0;
begin
	numberOfInvocations := numberOfInvocations + 1;
	writeLn(numberOfInvocations);
end;

begin
	foo;
	foo;
	foo;
end.

程式將為每次呼叫列印 123。第 2、4 和 5 行包含精心設計的註釋,這些註釋指示編譯器支援持久變數。這些註釋是非標準的,但其中一些在附錄中進行了說明,“預處理器功能”章節

請注意,型別化“常量”的概念並非標準化。一些面向物件程式設計擴充套件將提供更強大的工具來實現如上所示的行為。我們主要向您解釋了持久變數的概念,以便您可以閱讀和理解其他人的原始碼。

過程可以根據需要使用多次。它們不是簡單的“文字替換”工具:過程的定義不會“複製”到其被呼叫的位置,即呼叫站點。可執行程式檔案的大小基本保持不變。

利用過程也有利於,並且通常有利於程式的開發進度。透過將程式設計專案分解成更小的可理解的問題,你可以專注於解決作為大任務一部分的孤立問題。這種方法被稱為分而治之。我們現在要求你逐漸轉向在開始輸入任何內容之前更多地思考你的程式設計任務。你可能需要花更多時間思考,例如,如何構建過程的引數列表。這個過程需要什麼資訊,什麼引數?在哪裡以及如何透過過程定義來概括重複出現的模式?識別這些問題需要時間和專業知識,因此如果你沒有看到任務示例答案中顯示的所有內容,請不要氣餒。你會從錯誤中學習。

但是,請記住,過程並不是萬能藥。在某些非常特定的情況下,你可能不希望使用過程。然而,識別這些情況超出了本書的範圍。為了本書的目的,以及在 99% 的所有程式設計專案中,如果可能,你都希望使用過程。現代編譯器甚至可以識別某些“不必要”使用過程的情況,但唯一的好處是你的原始碼結構更加清晰,因此更易讀,儘管是以犧牲抽象度增加複雜度為代價的。[腳註 5]

編寫一個(現在臭名昭著的)程式,以大寫字母(至少跨三行)輸出單詞“Mississippi”。應該清楚的是,編寫四個過程,printMprintIprintSprintP,將大大加快開發速度。
可接受的答案可能如下所示
program mississippi(output);

const
	width = 8;

procedure printI;
begin
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn;
end;

procedure printM;
begin
	writeLn('#    #':width);
	writeLn('##  ##':width);
	writeLn('# ## #':width);
	writeLn('#    #':width);
	writeLn('#    #':width);
	writeLn;
end;

procedure printP;
begin
	writeLn( '###  ':width);
	writeLn( '#  # ':width);
	writeLn( '###  ':width);
	writeLn( '#    ':width);
	writeLn( '#    ':width);
	writeLn;
end;

procedure printS;
begin
	writeLn('  ###  ':width);
	writeLn(' #   # ':width);
	writeLn('  ##   ':width);
	writeLn('#   #  ':width);
	writeLn(' ###   ':width);
	writeLn;
end;

begin
	printM;
	printI;
	printS;
	printS;
	printI;
	printS;
	printS;
	printI;
	printP;
	printP;
	printI;
end.
可接受的答案可能如下所示
program mississippi(output);

const
	width = 8;

procedure printI;
begin
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn( '#   ':width);
	writeLn;
end;

procedure printM;
begin
	writeLn('#    #':width);
	writeLn('##  ##':width);
	writeLn('# ## #':width);
	writeLn('#    #':width);
	writeLn('#    #':width);
	writeLn;
end;

procedure printP;
begin
	writeLn( '###  ':width);
	writeLn( '#  # ':width);
	writeLn( '###  ':width);
	writeLn( '#    ':width);
	writeLn( '#    ':width);
	writeLn;
end;

procedure printS;
begin
	writeLn('  ###  ':width);
	writeLn(' #   # ':width);
	writeLn('  ##   ':width);
	writeLn('#   #  ':width);
	writeLn(' ###   ':width);
	writeLn;
end;

begin
	printM;
	printI;
	printS;
	printS;
	printI;
	printS;
	printS;
	printI;
	printP;
	printP;
	printI;
end.

註釋

  1. 某些 Pascal 方言在這方面並不那麼嚴格:FPC 有一個選項 {$extendedSyntax on},它將允許上述程式繼續編譯。
  2. 已有意省略label 部分。
  3. 擴充套件 Pascal 標準允許所謂的“前向宣告”[遠端指令]。過程的前向宣告只是宣告,沒有定義。
  4. 一些編譯器支援生成非標準的“執行時型別資訊”(RTTI)。透過啟用 RTTItype 宣告確實會生成儲存在程式中的資料。
  5. 一種這樣的編譯器最佳化稱為內聯。這將有效地將過程定義複製到呼叫站點。純函式 甚至可以透過將其定義為獨立函式而受益,前提是編譯器支援適當的最佳化。


下一頁: 列舉 | 上一頁: 表示式和分支
首頁: Pascal 程式設計
華夏公益教科書