Ngôn ngữ lập trình C - Chương 6: Hàm

Một chương trình viết trong ngôn ngữ C là một dãy các hàm, trong đó có một

hàm chính ( hàm main() ). Hàm chia các bài toán lớn thành các công việc nhỏ hơn,

giúp thực hiện những công việc lặp lại nào đó một cách nhanh chóng mà không phải

viết lại đoạn chương trình. Thứ tự các hàm trong chương trình là bất kỳ, song chương

trình bao giờ cũng đi thực hiện từ hàm main().

pdf13 trang | Chuyên mục: C/C++ | Chia sẻ: dkS00TYs | Lượt xem: 2180 | Lượt tải: 0download
Tóm tắt nội dung Ngôn ngữ lập trình C - Chương 6: Hàm, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
 Nguyên mẫu của hàm thực chất là dòng đầu tiên của hàm thêm vào dấu ;. Tuy 
nhiên trong nguyên mẫu có thể bỏ tên các đối. 
 Hàm thường có một vài đối. Ví dụ như hàm max3s có ba đối là a,b,c. cả ba đối 
này đều có giá trị float. Tuy nhiên, cũng có hàm không đối như hàm main. 
 Hàm thường cho ta một giá trị nào đó. Lẽ dĩ nhiên giá trị của hàm phụ thuộc 
vào giá trị các đối. 
6.2. Hàm không cho các giá trị : 
 Các hàm không cho giá trị giống như thủ tục ( procedure ) trong ngôn ngữ lập 
trình PASCAL. Trong trường hợp này, kiểu của nó là void. 
 Ví dụ hàm tìm giá trị max trong ba số là max3s ở trên có thể được viết thành 
thủ tục hiển thị số cực đại trong ba số như sau : 
 void htmax3s(float a, float b, float c) 
 { 
Chương 6:Hàm 
66 
 float max; 
 max=a; 
 if (max<b) max=b; 
 if (max<c) max=c; 
 } 
 Lúc này, trong hàm main ta gọi hàm htmax3s bằng câu lệnh : 
 htmax3s(x,y,z); 
6.3. Hàm đệ qui : 
6.3.1. Mở đầu : 
 C không những cho phép từ hàm này gọi tới hàm khác, mà nó còn cho phép từ 
một điểm trong thân của một hàm gọi tới chính hàm đó. Hàm như vậy gọi là hàm đệ 
qui. 
 Khi hàm gọi đệ qui đến chính nó, thì mỗi lần gọi máy sẽ tạo ra một tập các 
biến cục bộ mới hoàn toàn độc lập với tập các biến cục bộ đã được tạo ra trong các 
lần gọi trước. 
 Để minh hoạ chi tiết những điều trên, ta xét một ví dụ về tính giai thừa của số 
nguyên dương n. Khi không dùng phương pháp đệ qui hàm có thể được viết như sau : 
 long int gt(int n) /* Tính n! với n>=0*/ 
 { 
 long int gtphu=1; 
 int i; 
 for (i=1;i<=n;++i) 
 gtphu*=i; 
 return s; 
 } 
 Ta nhận thấy rằng n! có thể tính theo công thức truy hồi sau : 
 n!=1 nếu n=0 
 n!=n*(n-1)! nếu n>0 
Hàm tính n! theo phương pháp đệ qui có thể được viết như sau : 
Chương 6:Hàm 
67 
 long int gtdq(int n) 
 { 
 if (n==0 || n==1) 
 return 1; 
 else 
 return(n*gtdq(n-1)); 
 } 
Ta đi giải thích hoạt động của hàm đệ qui khi sử dụng trong hàm main dưới đây : 
#include "stdio.h" 
main() 
 { 
 printf("\n 3!=%d",gtdq(3)); 
 } 
 Lần gọi đầu tiên tới hàm gtdq được thực hiện từ hàm main(). Máy sẽ tạo ra 
một tập các biến tự động của hàm gtdq. Tập này chỉ gồm các đối n. Ta gọi đối n được 
tạo ra lần thứ nhất là n thứ nhất. Giá trị của tham số thực ( số 3 ) được gán cho n thứ 
nhất. Lúc này biến n trong thân hàm được xem là n thứ nhất. Do n thứ nhất có giá trị 
bằng 3 nên điều kiện trong toán tử if là sai và do đó máy sẽ lựa chọn câu lệnh else. 
Theo câu lệnh này, máy sẽ tính giá trị biểu thức : 
 n*gtdq(n-1) (*) 
 Để tính biểu thức trên, máy cần gọi chính hàm gtdq vì thế lần gọi thứ hai sẽ 
thực hiện. Máy sẽ tạo ra đối n mới, ta gọi đó là n thứ hai. Giá trị của n-1 ở đây lại là 
đối của hàm , được truyền cho hàm và hiểu là n thứ hai, do vậy n thứ hai có giá trị là 
2. Bây giờ, do n thứ hai vẫn chưa thoả mãn điều kiện if nên máy lại tiếp tục tính biểu 
thức : 
 n*gtdq(n-1) (**) 
 Biểu thức trên lại gọi hàm gtdq lần thứ ba. Máy lại tạo ra đối n lần thứ ba và ở 
đây n thứ ba có giá trị bằng 1. Đối n=1 thứ ba lại được truyền cho hàm, lúc này điều 
kiện trong lệnh if được thoả mãn, máy đi thực hiện câu lệnh : 
 return 1=gtdq(1) (***) 
Chương 6:Hàm 
68 
 Bắt đầu từ đây, máy sẽ thực hiện ba lần ra khỏi hàm gtdq. Lần ra khỏi hàm thứ 
nhất ứng với lần vào thứ ba. Kết quả là đối n thứ ba được giải phóng, hàm gtdq(1) 
cho giá trị là 1 và máy trở về xét giá trị biểu thức 
 n*gtdq(1) đây là kết quả của (**) 
ở đây, n là n thứ hai và có giá trị bằng 2. Theo câu lệnh return, máy sẽ thực hiện lần 
ra khỏi hàm lần thứ hai, đối n thứ hai sẽ được giải phóng, kết quả là biểu thức trong 
(**) có giá trị là 2.1. Sau đó máy trở về biểu thức (*) lúc này là : 
 n*gtdq(2)=n*2*1 
n lại hiểu là thứ nhất, nó có giá trị bằng 3, do vậy giá trị của biểu thức trong (*) là 
3.2.1=6. Chính giá trị này được sử dụng trong câu lệnh printf của hàm main() nên kết 
quả in ra trên màn hình là : 
 3!=6 
Chú ý : 
 Hàm đệ qui so với hàm có thể dùng vòng lặp thì đơn giản hơn, tuy nhiên với 
máy tính khi dùng hàm đệ qui sẽ dùng nhiều bộ nhớ trên ngăn xếp và có thể dẫn đến 
tràn ngăn xếp. Vì vậy khi gặp một bài toán mà có thể có cách giải lặp ( không dùng 
đệ qui ) thì ta nên dùng cách lặp này. Song vẫn tồn tại những bài toán chỉ có thể giải 
bằng đệ qui. 
6.3.2. Các bài toán có thể dùng đệ qui : 
 Phương pháp đệ qui thường áp dụng cho các bài toán phụ thuộc tham số có hai 
đặc điểm sau : 
 Bài toán dễ dàng giải quyết trong một số trường hợp riêng ứng với các giá trị 
đặc biệt của tham số. Người ta thường gọi là trường hợp suy biến. 
 Trong trường hợp tổng quát, bài toán có thể qui về một bài toán cùng dạng 
nhưng giá trị tham số thì bị thay đổi. Sau một số hữu hạn bước biến đổi dệ qui nó sẽ 
dẫn tới trường hợp suy biến. 
 Bài toán tính n giai thừa nêu trên thể hiện rõ nét đặc điểu này. 
6.3.3. Cách xây dựng hàm đệ qui : 
Chương 6:Hàm 
69 
 Hàm đệ qui thường được xây dựng theo thuật toán sau : 
 if ( trường hợp suy biến) 
 { 
 Trình bày cách giải bài toán khi suy biến 
 } 
 else /* Trường hợp tổng quát */ 
 { 
 Gọi đệ qui tới hàm ( đang viết ) với các giá 
 trị khác của tham số 
 } 
6.3.4. Các ví dụ về dùng hàm đệ qui : 
Ví dụ 1 : 
 Bài toán dùng đệ qui tìm USCLN của hai số nguyên dương a và b. 
 Trong trường hợp suy biến, khi a=b thì USCLN của a và b chính là giá trị của 
chúng. 
 Trong trường hợp chung : 
 uscln(a,b)=uscln(a-b,b) nếu a>b 
 uscln(a,b)=uscln(a,b-a) nếu a<b 
Ta có thể viết chương trình như sau : 
#include "stdio.h" 
int uscln(int a,int b ); /* Nguyên mẫu hàm*/ 
main() 
 { int m,n; 
 printf("\n Nhap cac gia tri cua a va b :"); 
 scanf("%d%d",&m,&n); 
 printf("\n USCLN cua a=%d va b=%d la :%d",m,m,uscln(m,n)) 
 } 
int uscln(int a,int b) 
 { 
Chương 6:Hàm 
70 
 if (a==b) 
 return a; 
 else 
 if (a>b) 
 return uscln(a-b,b); 
 else 
 return uscln(a,b-a); 
 } 
Ví dụ 2 : 
Chương trình đọc vào một số rồi in nó ra dưới dạng các ký tự liên tiếp. 
# include "stdio.h" 
# include "conio.h" 
void prind(int n); 
main() 
{ 
 int a; 
 clrscr(); 
 printf("n="); 
 scanf("%d",&a); 
 prind(a); 
 getch(); 
} 
void prind(int n) 
 { 
 int i; 
 if (n<0) 
 { putchar('-'); 
 n=-n; 
 } 
Chương 6:Hàm 
71 
 if ((i=n/10)!=0) 
 prind(i); 
 putchar(n%10+'0'); 
 } 
6.4. Bộ tiền sử lý C : 
 C đưa ra một số cách mở rộng ngôn ngữ bằng các bộ tiền sử lý macro đơn 
giản. Có hai cách mở rộng chính là #define mà ta đã học và khả năng bao hàm nội 
dung của các file khác vào file đang được dịch. 
Bao hàm file : 
 Để dễ dàng xử lý một tập các #define và khai báo ( trong các đối tượng khác ), 
C đưa ra cách bao hàm các file khác vào file đang dịch có dạng : 
#include "tên file" 
Dòng khai báo trên sẽ được thay thế bởi nội dung của file có tên là tên file. Thông 
thường có vài dòng như vậy xuất hiện tại đầu mỗi file gốc để gọi vào các câu lệnh 
#define chung và các khai báo cho các biến ngoài. Các #include được phép lồng 
nhau. Thường thì các #include được dùng nhiều trong các chương trình lớn, nó đảm 
bảo rằng mọi file gốc đều được cung cấp cùng các định nghĩa và khai báo biến, do 
vậy tránh được các lỗi khó chịu do việc thiếu các khai báo định nghĩa. Tất nhiên khi 
thay đổi file được bao hàm vào thì mọi file phụ thuộc vào nó đều phải dịch lại. 
Phép thế MACRO : 
 Định nghĩa có dạng : 
#define biểu thức 1 [ biểu thức 2 ] 
sẽ gọi tới một macro để thay thế biểu thức 2 (nếu có) cho biểu thức 1. 
Ví dụ : 
#define YES 1 
 Macro thay biến YES bởi giá trị 1 có nghĩa là hễ có chỗ nào trong chương 
trình có xuất hiện biến YES thì nó sẽ được thay bởi giá trị 1. 
Chương 6:Hàm 
72 
 Phạm vi cho tên được định nghĩa bởi #define là từ điểm định nghĩa đến cuối 
file gốc. Có thể định nghĩa lại tên và một định nghĩa có thể sử dụng các định nghĩa 
khác trước đó. Phép thế không thực hiện cho các xâu dấu nháy, ví dụ như YES là tên 
được định nghĩa thì không có việc thay thế nào được thực hiện trong đoạn lệnh có 
"YES". 
 Vì việc thiết lập #define là một bước chuẩn bị chứ không phải là một phần của 
chương trình biên dịch nên có rất ít hạn chế về văn phạm về việc phải định nghĩa cái 
gì. Chẳng hạn như những người lập trình ưa thích PASCAL có thể định nghĩa : 
 #define then 
 #define begin { 
 #define end; } 
sau đó viết đoạn chương trình : 
 if (i>0) then 
 begin 
 a=i; 
 ...... 
 end; 
Ta cũng có thể định nghĩa các macro có đối, do vậy văn bản thay thế sẽ phụ thuộc 
vào cách gọi tới macro. 
Ví dụ : 
 Định nghĩa macro gọi max như sau : 
 #define max(a,b) ((a)>(b) ?(a):(b)) 
Việc sử dụng : 
 x=max(p+q,r+s); 
tương đương với : 
 x=((p+q)>(r+s) ? (p+q):(r+s)); 
Như vậy ta có thể có hàm tính cực đại viết trên một dòng. Chừng nào các đối 
còn giữ được tính nhất quán thì macro này vẫn có giá trị với mọi kiểu dữ liệu, không 
Chương 6:Hàm 
73 
cần phải có các loại hàm max khác cho các kiểu dữ liệu khác nhưng vẫn phải có đối 
cho các hàm. 
Tất nhiên nếu ta kiểm tra lại việc mở rộng của hàm max trên, ta sẽ thấy rằng nó có thể 
gây ra số bẫy. Biểu thức đã được tính lại hai lần và điều này là không tốt nếu nó gây 
ra hiệu quả phụ kiểu như các lời gọi hàm và toán tử tăng. Cần phải thận trọng dùng 
thêm dấu ngoặc để đảm bảo trật tự tính toán. Tuy vậy, macro vẫn rất có giá trị. 
Chú ý : 
 Không được viết dấu cách giữa tên macro với dấu mở ngoặc bao quanh danh 
sách đối. 
Ví dụ : 
 Xét chương trình sau : 
main() 
 { 
 int x,y,z; 
 x=5; 
 y=10*5; 
 z=x+y; 
 z=x+y+6; 
 z=5*x+y; 
 z=5*(x+y); 
 z=5*((x)+(y)); 
 printf("Z=%d",z); 
 getch(); 
 return; 
 } 
Chương trình sử dụng MACRO sẽ như sau : 
#define BEGIN { 
#define END } 
#define INTEGER int 
Chương 6:Hàm 
74 
#define NB 10 
#define LIMIT NB*5 
#define SUMXY x+y 
#define SUM1 (x+y) 
#define SUM2 ((x)+(y)) 
main() 
 BEGIN 
 INTEGER x,y,z; 
 x=5; 
 y=LIMIT; 
 z=SUMXY; 
 z=5*SUMXY; 
 z=5*SUM1; 
 z=5*SUM2; 
 printf("\n Z=%d",z); 
 getch(); 
 return; 
 END 

File đính kèm:

  • pdfCHUONG_6.pdf
Tài liệu liên quan