Chương trình con : Hàm và thủ tục trong pascal

Xin chào các bạn, Hôm nay WIKIPASCAL sẽ hướng dẫn các bạn về Dữ liệu có cấu trúc - Phần 1: Mảng (Pascal) trong Pascal
Chuong trinh con : Ham va thu tuc trong pascal - Wiki Pascal
Chương trình con : Hàm và thủ tục trong pascal

I – Khái niệm


1. Khái niệm về chương trình con:



Turbo Pascal là ngôn ngữ có cấu trúc cao. Do đó, một chương trình lớn có thể chia thành nhiều chương trình con với hai mục đích :
a) Dễ kiểm tra, dề điều khiển từng phần của chương trình.

b) Tránh lặp đi lặp lại những đoạn chương trình dùng nhiều lần. Điều này vừa gây mất thời gian cho người lập trình vừa làm cho chương trình thêm lôi thôi, mất thẩm mĩ.


2. Thủ tục và hàm:


Trong Pascal có hai loại chương trình con :


Procedure ( thủ tục )

Function ( hàm )



Sự khác nhau cơ bản và duy nhất của hai loại chương trình con này là : Function trả lại cho một giá trị kết quả vô hướng thông qua tên của Function và do đó nó được sử dụng trong một biểu thức. Còn Procedure không trả lại kết quả thông qua tên của nó nên các Procedure không thể viết trong các biểu thức.​


3. Cấu trúc của Procedure và Function :




Code:
Procedure Ten_thu_tuc ( Khai báo các tham số hình thức ) ;

(* Khai báo : Label, Const, Type, Var, hoặc các Procedure và Function *) ;

Begin

  ... (* Thân chương trình con *)

End ;

Function Ten_ham ( khai báo các tham số hình thức ) : kieu_du_lieu_cua_ham ;

(* Khai báo : Label, Const, Type, Var, hoặc các Procedure và Function *) ;

Begin

  ... (* Thân chương trình con *)

End ;



Thân của chương trình con được đặt giữa hai chữ Begin và End với End kết thúc bằng dấu chấm phẩy (;) chứ không phải dấu chấm (.) như của chương trình chính.​


4. Ví dụ về thủ tục và hàm:




Code:
Program Vidu ;

Uses Crt ; (* Crt là một Unit chứa các chương trình con về màn hình, bàn hình... *)

Var A, B, C, D : Integer ;

  Z : Real ;

(* ---------------------------------------------------------------------- *)

Procedure Tieu_de ;

Begin

  Writeln (' **************************************** ') ;

  Writeln (' * MINH HOA CHUONG TRINH CON * ') ;

  Writeln (' **************************************** ') ;

End ;

(* ------------------------------------------------------------------- *)

Procedure Enter (Var X, Y : Integer ) ;

Var OK : Char ;

Begin

  Repeat

  Write (' Tu so = ') ; Readln (X) ;

   Write (' Mau so = ') ; Readln (Y) ;

  Write (' Co sua so lieu khong (c, k) ? ') ;

  OK := Readkey ;

  Writeln ;

  Until (OK = ' K ') or (OK = ' k ') ;

End ;

(* ------------------------------------------------------------------- *)

Function Chia (X, Y : Integer) : Real ;

Begin

  If Y <> 0 Then Chia := X / Y

  Else

  Begin

  Writeln (#7,' Khong chia duoc cho 0.') ;

  (* Máy sẽ báo lỗi nếu mẫu số là 0 *)

  Halt ; (* Thu tuc Halt dung chuong trinh lai *)

  End ;

End ;

(* ------------------------------------------------------------------- *)

BEGIN

  Tieu_de ;

  Enter (A, B) ;

  Enter (C, D) ;

  Z := Chia (A, B) * Chia (C, D) ;

  Writeln (' Ti so (A / B) * (C / D) la : ', Z) ;

  Writeln (' Hay an Enter de ket thuc ! ') ;

  Readln ;

END.



Chương trình này sẽ đọc từng cặp số nguyên A, B và C, D, sau đó đọc tỉ số của 2 số đó qua Function Chia và cuối cùng là tính tích của 2 tỉ số đó rồi báo ra kết quả. Như vậy, nhìn vào thân chương trình chính ta thấy công việc được hình dung ra một cách rất sáng sủa.

Đầu tiên, chương trình con (CTC) Tieu_de có nhiệm vụ in ra vài dòng tiêu đề, CTC này không cần tham số.

Procedure Enter có nhiệm vụ là vào dữ liệu X, Y, là hai tham số hình thức, cũng đồng thời là kết quả của Procedure. Hai tham số này sẽ được thay thế bằng hai tham số thực sự được khai báo ở chương trình chính là A và B khi ta gọi Procedure bằng câu lệnh Enter (A, B), tức là A sẽ tương ứng với X và B sẽ tương ứng với Y. Lời gọi Enter (A, B)được hiểu là đọc vào hai tham số A và B. Tương tự như vậy, lần gọi thứ hai Enter (C, D)sẽ đọc hai giá trị của hai tham số thực sự là C và D.

Trong CTC, ta cũng có thể có phần khai báo riêng của nó. Trong ví dụ trên, biến OK là biến riêng hay còn thường được gọi là biến địa phương của Procedure Enter để phân biệt với các biến toàn cục được mô tả ở chương trình chính là A, B, Z...

Function Chia có hai tham số hình thức là X và Y. Ta có thể viết lời gọi Function trong biểu thức như việc tính Z ở ví dụ trên. Lời gọi của Procedure không làm đươc như vậy vì tên của Procedure không có giá trị. Khi khai báo Function, ta còn phải khai báo thêm kiểu dữ liệu của Function. Trong ví dụ trên, Function Chia có kiểu là Real, đó cũng là sự khác nhau giữa hai loại CTC.​


Giả sử, để tính giá trị của Z = (A / B) / (C / D), ta có thể viết :

Code:
Z := Chia (Chia(A, B), Chia(C, D)) ;


II – Chuyển tham số cho chương trình con



Chương trình con có thể được khai báo mà không dùng tham số khi các CTC tính toán trực tiếp với các biến toàn cục hoặc CTC không dùng đến bất cứ biến hay hằng nào như thủ tục Tieu_de mà ta đã xét ở bài trước.

Việc chuyển tham số cho CTC là một cơ cấu thay thế tương ứng, nó cho phép một quá trình được lặp đi lặp lại nhiều lần với các "toán hạng" khác nhau. Thí dụ Enter (A, B) sẽ thay thế A vào vị trí của X, B vào vị trí của Y. Tương tự với Enter (C, D).

Chuyển tham số bằng biến : tham số hình thức trong phần tiêu đề của CTC sẽ được đặt sau từ khóa Var. Với tham biến, các tham số thực sự sẽ phải là biến chứ không được là giá trị. Thí dụ lời gọi Enter (3, 7) sẽ không được chấp nhận vì 3 và 7 là hai giá trị chứ không phải là biến. Các tham số thực sự là các tham biến có thể được thay đổi trong CTC và khi ra khỏi CTC nó vẫn giữ các giá trị đã thay đổi đó. Khi khai báo các tham số mà không có từ khóa Var trong một nhóm tham số hình thức thì các tham số của nhóm này là các tham số giá trị (tham trị). Khi đó các tham số thực sự phải là một biểu thức. Tham số hình thức tương ứng sẽ được coi như một biến địa phương của CTC, nó nhận giá trị của tham số thực như là giá trị ban đầu ở vào thời điểm thay vào CTC. Chương trình con sau đó có thể thay đổi giá trị của các tham trị này ở bên trong CTC bằng các phép gán. Song trong mọi trường hợp điều đó không làm thay đổi giá trị của tham số thực. Do vậy, một tham trị không bao giờ là kết quả tính toán của CTC.​


Sự khác nhau giữa hai loại tham số này được minh họa bằng ví dụ sau :


Code:
Program Tham_so ;

Var A, B : Integer ;

(* ----------------------------------------------------------- *)

Procedure Thidu_Thamso (X : Integer ; Var Y : Integer) ;

Begin

  X := X + 1 ;

  Y := Y + 1 ;

  Writeln (X : 6, Y : 6) ;

End ;

(* ----------------------------------------------------------- *)

BEGIN

  A := 0 ;

  B := 3 ;

  Thidu_Thamso (A, B) ;

  Writeln (A : 6, B : 6) ;

END.



Kết quả cho ra :


1 4

0 4


Trong thí dụ trên, thủ tục Thidu_Thamso có hai loại tham số : tham trị X và tham biến Y. Trước khi gọi thủ tục này với hai tham số thực sự là A và B tương ứng thì A = 0 và B = 3. Trong thủ tục ta có hai lệnh làm thay đổi giá trị của A và B bằng cách tăng thêm 1. Lệnh Writeln (X, Y)cho ra kết quả là 1 và 4 tương ứng. Tuy nhiên sau khi ra khỏi CTC, lệnh Writeln (X, Y) báo cho ta giá trị của B đã bị thay đổi trong CTC vì B là tham biến, còn A vẫn giữ nguyên giá trị trước khi gọi thủ tục, tức A vẫn bằng 0, vì A chỉ là tham trị.

Như vậy, khi chuyển một tham số cho CTC, nếu ta muốn bảo vệ giá trị của tham số đó khỏi bị CTC "vô tình phá" thì tham số đó phải được dùng như là tham trị. Còn một tham số nếu muốn dùng để lấy kết quả do CTC đem lại thì tham số đó phải là tham biến.

III – Biến toàn cục và biến địa phương


Xét ví dụ sau đây :


Code:
PROGRAM VI_DU

VAR

X : Integer ;

(* ----------------------------------------- *)

Procedure A ;

Var Y : Integer ;

  Procedure AA ;

  Var M,N : Integer ;

  Begin

  …

  End ;

  Procedure AB ;

  Var M,N : Integer ;

  Begin

  ...

  End ;

  Begin

  …

  End ; (* Procedure A *)

(* -------------------------------------------- *)

Procedure B ;

Var X,Z : Integer ;

  Procedure BA ;

  Begin

  …

  End ;

Begin

  …

End ; (* Procedure B *)

(* --------------------------------------------- *)

BEGIN

  …

END. (* Chương trình chính *)



Như ta đã biết, các biến được khai báo trong chương trình chính được gọi là biến toàn cục. Các biến này có thể được dùng ở mọi nơi trong chương trình. Các biến được khai báo trong một CTC được gọi là các biến địa phương và nó chỉ có tác dụng trong phạm vi CTC đó hay trong Bloc đó. Khi CTC kết thúc thì các biến này cũng mất tác dụng theo. Để diễn tả tầm tác dụng của các biến, của các khai báo, người ta đưa ra khái niệm mức : chương trình chính có mức 0, các chương trình tiếp theo có mức là 1,2,… tùy theo vị trí khai báo. Trong hình 2, chương trình con A và B có mức là 1, chương trình con AA, AB, BA có mức là 2.


Sau đây là một số quy tắc sử dụng :

+ Tầm tác dụng của 1 tên (biến, hằng, kiểu…) được xác định bằng mức Bloc trong đó tên được khai báo và bằng các mức Bloc khác có mức cao hơn và nằm trong Bloc chứa khai báo.
Trong ví dụ vừa rồi, biến Y được khai báo trong CTC A (có mức là 1). Như vậy biến Y có thể được sử dụng ở trong CTC AA và AB (là 2 CTC có mức cao hơn và nằm trong CTC A). Ngoài ra Y không thể sử dụng ở CTC B, BA, BB vì chúng không phải là CTC của A.

+ Tầm quan trọng của các biến khai báo ở mức 0 (chương trình chính)là toàn bộ chương trình.

+ Ở các mức khác nhau của các CTC, ta có thể khai báo 1 biến có cùng tên với biến ở mức khác. Tên biến này không phải là một biến duy nhất mà là hai biến khác nhau với tầm quan trọng khác nhau. Ví dụ trong hình 2, CTC B có biến địa phương X và trong chương trình chính có biến toàn cụa cũng có là X. Khi đó trong CTc thì biến X địa phương có tác dụng, còn khi CTC kết thúc thì biến toàn cục lại lấy lại tác dụng của nó. Hãy xét ví dụ cụ thể như sau :​

Code:
Program Tam_Tac_Dung) ;

Var I : Integer ; (* Biến I toàn cục *)

(* ------------------------------------------------- *)

  Procedure Dia_Phuong ;

  Var I : Integer ; (* Biến I địa phương *)

  Begin

  I := 7 ;

  Writeln (I : 6) ;

  End ;

(* ------------------------------------------------ *)

BEGIN

  I :=5 ;

  Writeln(I : 6) ;

  Dia_Phuong ;

  Writeln(I : 6) ;

END.



Kết quả cho ra :


5 (* giá trị của I toàn cục *)

7 (* giá trị của I địa phương *)

5 (* giá trị của I toàn cục *)


Tên biến I được dùng cho cả biến toàn cục và biến địa phương. Đầu tiên biến I toàn cục nhận giá trị bằng 5. Sau đó thủ tục Dia_Phuong được gọi, vì thủ tục này cũng có biến là I (biến địa phương) nên biến I toàn cục được xem như tạm bị treo không dùng đến. Biến địa phương lấy giá trị bằng 7. Sau khi kết thúc chương trình con, biến I địa phương bị mất và biến I toàn cục lại được khôi phục lại tác dụng. Tất nhiên nó vẫn giữ giá trị bằng 5 là giá trị có được trước khi gọi thủ tục Dia_phuong.

Trong trường hợp trong thủ tục Dia_phuong, ta muốn chiếu đến biến I toàn cục, ta vẫn có thể dùng nó bằng cách chỉ rõ tên chương trình ngoài tên biến : Tam_tac_dung.i. Cách tham chiếu như trên cũng tương tự như khi ta chỉ ra đường dẫn trực tiếp trên DOS.

IV – Tính đệ quy của chương trình con


Trong Procedure và Function có thể có lời gọi của chính nó. Tính chất này dược gọi là tính đệ qui.

Thí dụ tính giai thừa qua định nghĩa :


N! = 1 x 2 x... x ( N - 1 ) x N


hoặc định nghĩa :

N! = 1 khi N = 0
hoặc
N! = N x ( N - 1 )! khi N >= 1


Khi đó, hàm Giai_thua được định nghĩa như sau :


Function Giai_thua( N : Integer ) : Integer ;


Code:
Begin

  If N = 0 Then Giai_thua := 1 ;

  Else Giai_thua := N * Giai_thua( N-1 ) ;

End ; 



Một điều cần lưu ý là ta phải hết sức thận trọng lường trước việc kết thúc của quá trình đệ qui này. Trong thí dụ trên, lệnh gán :



Code:
K := Giai_thua( -1 ) ; 



sẽ khởi động một quá tình đệ qui rất dài về mặt lý thuyết vì tham số âm bị xử lý sai.


Ở thí dụ trên, ta đã khai báo giai_thua là Integer nên sẽ bị một hạn chế : chỉ có thể tính với N < 8 vì nếu N >= 8, Giai_thua sẽ mang giá trị lớn hơn 32767 là giới hạn trên của số nguyên. Một trong các biện pháp khắc phục là ta khai báo Giai_thua là Real.​

Code:
Function Giai_thua( N : Integer ) : Real ; 

Khi sử dụng Giai_thua là Real, ta phải chú ý sử lý thêm một ít. Ví dụ, để viết giá trị của Giai_thua là số thực sang số nguyên, ta phải sử dụng cách viết có quy cách với phần thập phân bị cắt :​

Code:
Writeln ( Giai_thua(12) : 0 : 0 ) ; 


Thí dụ tính giai thừa ở trên về phương diện ví dụ nó rất đơn giản và dể hiểu. Song về phương diện kĩ thuật lập trình thì đấy là một thí dụ không đẹp lắm vì người ta có thể tính giai thừa một cách tiết kiệm hơn bằng chương trình sau khi sử dụng lệnh lặp While :​

Code:
Function Giai_thua( N : Integer ) : Integer ;

Var I, K :Integer ;

Begin

  I := 0 ;

   K := 1 ; (* Phải dùng biến địa phươn K để chứa kết quả trung gia *)

  While I < N Do

  Begin

  I := I + 1 ;

  K := K * I ;

  End ;

  Giai_thua := K ; (* Gán kết quả từ biến trung gian K vào tên hàm*)

End ; 


Trong cách dùng sau, ta chỉ mất hai ô nhớ địa phương tương ứng với hai biến I và K. Còn trong cách dùng trước, mỗi lần dùng Giai_thua(N), máy lại phải bố trí thêm một ô nhớ chứa kết quả Giai_thua trung gian.​
Nói chung nên tránh dùng đệ qui khi mà ta có thể dùng phép lặp để tính toán.​

V – Forward (Tham khảo trước)



Các CTC còn có thể được gọi ra trước khi có định nghĩa chúng nếu như trước đó có lời khai báo kiểu Forward. Cách thức viết được thể hiện qua ví dụ dưới đây. Lưu ý là danh sách các tham số hình thức cần liệt kê ra trong lời khai báo trước (Forward) còn trong lời khai báo chi tiết thì không có.​

Code:
Program Vi_du_Forward ;

Var Alpha : Integer ;

(* Khai báo trước Procedure Test2 với từ Forward *)

Procedure Test2 ( Var A : Integer ) ; Forward ;

(* -------------------------------------------------------- *)

Procedure Test1 ( Var A : Integer ) ;

  Begin

  A := A - 1 ;

  (* Dùng Test2 trước khi khai báo chi tiết *)

  If A > 0 Then Test2(A) ;

  End ;

(* -------------------------------------------------------- *)

Procedure Test2 ;

  Begin

  A := A Div 2 ;

  If A > 0 Then Test1(A) ;

  End ;

(* -------------------------------------------------------- *)

BEGIN

  Alpha := 15 ;

  Test1( Alpha ) ;

END.



Ta thấy thủ tục Test1 dùng thủ tục Test2, còn thủ tục Test2 lại dùng thủ tục Test1. Do đó ta thấy ngay trong trường hợp này không thể định nghĩa thủ tục Test1 và Test2 một cách bình thường được mà phải dùng đến Forward.

VI – Đơn vị chương trình


1. Unit:



Trong Pascal, người ta đã chia nhỏ thư viện CTC thành các thư viện nhỏ hơn, mỗi thư viện nhỏ đó được gọi là một Unit (đơn vị chương trình) như :​


CRT là Unit chứa các CTC xử lý màn hình ( như lệnh Gotoxy, ClrScr... ), bàn phím ( như Readkey... ).

PRINTER là Unit chứa các CTC và dữ liệu về thủ tục in như Writeln ( Lst,... ).

DOS là Unit chứa các CTC và dữ liệu khai thác các hàm của Dos.

SYSTEM là Unit chứa các CTC và dữ liệu hệ thống.

( Lưu ý rằng các Unit trên được để trong file TURBO.TPL ( TPL : Turbo Pascal Library ).

GRAPH là một Unit về đồ họa, được để trong một file riêng có tên là GRAPH.TPU. Đây là một Unit chứa tất cả các CTC về đồ họa. Vì vậy khi bạn dùng đồ họa, nhớ phải copy file GRAPH.TPU.

STRING chỉ có trong Turbo 7.0 để sử dụng kiểu xâu kí tự kệt thúc bằng kí tự có mã #0 ( kí tự Null ).

TURBO3 là Unit tạo sự tương thích với các chương trình đã viế bằng Turbo Pascal 3.0, được chứa trong file TURBO3.TPU.

GRAPH3 thực hiện các CTC vẽ theo kiểu con rùa có ở trong Turbo 3.0, chứa trong fileGRAPH3.TPU


Tuy nhiên, hai Unit cuối có lẽ bạn sẽ không phải dùng đến, bởi vì phiên bản Turbo 3.0 và các chương trình viết bằng phiên bản này đã cũ và "cổ lỗ" lắm rồi, bạn có muốn tìm cũng không ra !

Việc chia nhỏ thư viện này giúp cho chương trình dịch chạy nhanh hơn vì không phải đọc lại tất cả các CTC nếu như không có nhu cầu. Mặt khác, chương trình dịch ra cũng gọn hơn vì không phải chứa những cái không dùng đến

Người ta có thể nhóm một số dữ liệu và các CTC liên quan lại thành các Unit ( đơn vị chương trình ). Các Unit này một khi đã được dịch hoàn chỉnh nếu không có sự sửa đổi thì ta có thể dùng ngay, không cần dịch lại và thậm chí chúng còn có thể được dùng chung cho nhiều chương trình khác nhau. Trong trương hợp có sự thay đổi trong một Unit nào đó thì chỉ có các Unit có liên quan mới bị dịch lại.​


2. Cách thức của một Unit :


Một Unit do bạn tạo ra sẽ đươc chứa trong một file với một tên nào đó, thí dụ KHOI1.PAS. Cách thức của một Unit như sau :

Unit Khoi1 ; (* Tên Unit thường nên đặt trùng với tên file *)

Uses Ten_cac_Unit_khac_can_dung ;

Interface

(* Khai báo tên các thủ tục và hàm được viết đầy đủ trong phần Implementation dưới đây. Tên của các thủ tục và hàm khai báo trong phần này là tên các CTC mà các Unit khác cần dùng đến *)


Code:
Procedure Thidu1 ( Var X, Y : Real ) ;

Function TenF ( I, J : Integer ) : Real ;

Implementation

(* Khai báo các kiểu dữ liệu, các biến... cùng đầy đủ các thủ tục và hàm của Unit này *)

Const PP = 3.5 ;

Var X, Y : Real ; (* Khai báo các biến cho Unit này *)

(* ------------------------------------------------------------------------- *)

Procedure Thi_du1 ( Var X, Y : Real ) ;

Begin

  ...

End ;

(* ------------------------------------------------------------------------- *)

Function TenF ( I, J : Integer ) : Real ;

Begin

  ...

End ;

END.

(* Kết thúc Unit bằng END có dấu chấm. Lưu ý ở phía trên sẽ không có chữ BEGIN tương ứng với chữ END kết thúc Unit này *)

Khi dịch tệp trên bằng cách ấn Alt _ F9, Turbo Pascal sẽ tạo ra file KHOI1.TPU ( TPU : Turbo Pascal Unit ). Bạn nhớ rằng để dịch đúng một Unit bạn đang soạn thảo, bạn phải ấn Alt _ F9 chứ không phải Ctrl _ F9.


3. Cách gọi các Unit:


Trong chương trình chính hay trong một Unit, nếu ta cần dùng đến một Unit khác, thì ở phần đầu chương trình ta phải viết dòng :

Uses Unit1, Unit2,..., UnitN ;

Các thủ tục ClrScr, Readkey... là các thủ tục đã được dịch sẵn và để trong Unit có tên là CRT. Vì vậy, muốn dùng nó, ta phải viết :


Code:
Program SD_Unit ;

Uses Crt ;

Type

  ...

BEGIN

  Clrscr ;

END.

hoặc một chương trình khác trong đó dùng nhiều Unit khác nhau :


Code:
Program Vidu_Unit ;

Uses Crt, Dos, Graph, Printer ;

Nhận xét

Bài đăng phổ biến từ blog này