cslt 1 chuong 4 ham va macro
Chương 4: Hàm và macro
4.1. làm quen với hàm
4.1.1. Giới thiệu về hàm trong C
Ta đã sơ lược làm quen với chương trình con ở chương 1. Có hai loại chương trình con là hàm và thủ tục, nhưng trong C, không giống các ngôn ngữ khác, C chỉ sử dụng một loại chương trình con, đó là hàm.
Hàm là một đoạn chương trình độc lập có chức năng thực hiện một công việc nhất định rồi trả về một giá trị cho chương trình gọi nó. Tuy vậy, có một số ít hàm đặc biệt không trả về một giá trị nào.
Trên phương diện người dùng, người ta chia hàm thành hai loại: Hàm mẫu và hàm tự định nghĩa.
Hàm mẫu là các hàm đã viết sẵn và lưu trong thư viện chương trình mẫu của ngôn ngữ. Chẳng hạn, các hàm printf(), scanf() ghi trong tệp tiêu đề Stdio.h mà chúng ta đã làm quen. Vì số lượng hàm mẫu của C khá nhiều, để tiện lợi cho việc biên dịch chương trình, người ta đã phân loại các hàm mẫu và ghi vào nhiều tệp chương trình mẫu khác nhau. Khi cần gọi hàm mẫu nào, ta phải có khai báo #include để C chèn tệp tiêu đề chứa hàm mẫu đó vào chương trình.
Hàm tự định nghĩa, còn gọi là hàm của người dùng, là các hàm do người sử dụng tự viết lấy để thực hiện một công việc nào đó, theo ý đồ riêng.
Tất cả các hàm trong C đề có đặc điểm là:
- Có một tên để gọi;
- Nhận các thông số và trả kết quả cho tên hàm, kết quả này có thể là dữ liệu kiểu cơ sở hoặc có cấu trúc;
- Là một đơn vị (module) độc lập của chương trình;
- Trong một hàm không được chứa một hàm khác.
Một chương trình C bao giờ cũng gồm một hoặc nhiều hàm. Hàm main() là thành phần bắt buộc có trong mọi chương trình C, nó giống như một chương trình chính. Ngoài ra, có thể có nhiều hàm khác được ghép vào một chương trình. Các chương trình C đều được tổ chức theo mẫu:
Hàm 1
..............
Hàm 2
.............
Hàm 3
.............
Hàm n
ở các vị trí ngoài hàm (chỗ đặt dấu ......), ta có thể đặt các toán tử #include, #define, định nghĩa các kiểu dữ liệu, khai báo biến ngoài, biến tĩnh ngoài.
4.1.2. Cấu trúc tổng quát của hàm
Để viết một hàm trong C, trước hết ta phải xác định hàm được xây dựng để thực hiện công việc gì, từ đó, xác định các thành phần của hàm, thường bao gồm:
- Nguyên mẫu của hàm
- Nội dung của hàm
a. Nguyên mẫu của hàm:
Nguyên mẫu của hàm là thành phần để xác định mẫu, khuôn dạng ban đầu cho một hàm, nó có dạng:
[Kiểu dữ liệu] Tên hàm([Danh sách tham số]);
ý nghĩa của các thành phần trong nguyên mẫu, để tránh trùng lắp, ta sẽ trình bày ở phần b. ngay sau đây.
Nguyên mẫu được viết ở ngoài hàm, thường đặt ngay đầu chương trình, trước hàm main(). Tuy vậy, nguyên mẫu hàm là thành phần không nhất thiết phải có khi viết một hàm, nhưng không nên làm như thế. Với các hàm có nguyên mẫu, trình biên dịch sẽ kiểm tra việc truyền tham số, giá trị trả về có phù hợp hay không rồi mới cho thực hiện hàm. Các tệp tiêu đề (chẳng hạn tệp stdio.h, conio.h,...) luôn chứa nguyên mẫu của các hàm chuẩn trong C.
Ngoài ra, khi đã có nguyên mẫu hàm, sẽ tiết kiệm tư duy hơn khi viết chương trình vì đã thấy dạng lời gọi rất tường minh. Điều đó sẽ làm tăng hiệu quả của người lập trình.
b. Nội dung hàm
Nội dung các hàm có thể đặt ở bất kì vị trí nào trong chương trình và chúng phải độc lập với nhau. Tuy vậy, để thuận lợi cho việc kiểm tra, ta nên khai báo các hàm có nguyên mẫu ở sau nội dung hàm main(). Trường hợp các hàm không có khai báo nguyên mẫu, bắt buộc phải đặt trước hàm main().
Mọi hàm trong C đều được viết theo mẫu cú pháp sau đây:
[Kiểu dữ liệu] Tên hàm([Danh sách tham số])
[Khai báo kiểu dữ liệu cho các tham số;]
{ [Khai báo các biến cục bộ;]
Các lệnh của hàm;
[ return [Biểu thức] ; ]
}
Trong đó:
· Kiểu dữ liệu: xác định kiểu dữ liệu cho giá trị của hàm khi được trả về, nếu thiếu khai báo kiểu dữ liệu, trình dịch sẽ tự xác định giá trị của hàm có kiểu int (ngầm định). Mỗi hàm có thể trả về một giá trị hoặc không. Nếu hàm phải trả về giá trị nào đó thì nhất thiết phải khai báo kiểu dữ liệu; Ngược lại, nếu hàm không trả về giá trị nào thì phải khai báo hàm kiểu void – một kiểu dữ liệu đặc biệt của C để mô tả sự việc không có giá trị.
· Tên hàm: Đặt theo quy tắc đặt tên nói chung. Tuy vậy, nên đặt theo kiểu gợi nhớ để tiện nhớ khi gọi. Nếu viết hàm có nguyên mẫu thì tên hàm ở đây phải trùng với tên hàm trong nguyên mẫu của hàm.
· Danh sách tham số: Các tham số viết trong nguyên mẫu của hàm và khai báo trong nội dung hàm gọi là các tham số hình thức (hoặc các biến hình thức) của hàm, chúng gồm các biến viết cách nhau dấu phảy. Những tham số này sẽ nhận các giá trị thực sự nhờ việc truyền tham số mỗi khi hàm này được gọi đến. Những giá trị thực sự, các biến ta ghi vào sau tên hàm trong lời gọi hàm để hàm thực hiện, được gọi là tham số thực sự. Tham số thực sự của hàm có thể là biểu thức tổng quát nhưng phải phù hợp về kiểu với tham số hình thức tương ứng của hàm được gọi.
Các tham số hình thức bao giờ cũng là các biến (tượng trưng cho các trường nhớ) để cung cấp giá trị cho hàm khi gọi hàm thực hiện - gọi là tham số vào và lưu trữ các kết quả tính toán trong hàm - gọi là tham số ra. Ngoài ra, có những biến vừa là tham số vào (ban đầu) lại vừa là tham số ra (sau đó).
· Khai báo kiểu dữ liệu cho các tham số: Là thành phần khai báo kiểu dữ liệu cho các tham số hình thức. Có thể thực hiện khai báo này bằng một trong hai cách:
Cách 1: Đặt khai báo kiểu cho tham số trong một phần riêng, sau phần khai báo hàm. Ví dụ:
float max(a, b) /*Khai báo hàm */
float a, b; /*Khai báo kiểu dữ liệu cho tham số */
Cách 2: Đặt khai báo kiểu cho tham số ngay trong khai báo hàm. Khi chương trình có viết nguyên mẫu của hàm thì buộc phải khai báo kiểu cho tham số theo cách này.
Ví dụ:
float max(float a, float b)
Chú ý: Trong trường hợp hàm không chứa một tham số hình thức nào, khi đó ta cũng khai báo tham số hình thức là void.
Ví dụ:
float xem_kq(void);
cũng có thể viết:
void Preview(void);
· Thân hàm: Là phần được giới hạn trong cặp dấu {...}, nó gồm:
- Khai báo các biến cục bộ: là các biến chỉ sử dụng riêng trong hàm, có tác động trong thân hàm, không ảnh hưởng gì tới các hàm khác trong chương trình. Trái lại, các tham số hình thức có thể dùng để trao đổi dữ liệu từ hàm này sang các hàm khác.
- Các lệnh của hàm: là các lệnh bất kì của C để thực hiện công việc được hàm thực hiện.
- Lệnh return : Là thông báo quay trở về chương trình gọi nó.
c. Một số ví dụ về hàm:
Ví dụ 1: Chương trình tráo đổi giá trị của hai biến, có sử dụng hàm:
#include <stdio.h>
#include <conio.h>
float a, b;
void doi_cho(float a, float b); /*khai báo nguyên mẫu */
main()
{
clrscr();
a=1; b=2;
printf(“Trước khi gọi hàm, a= %.0f, b= %.0f
”, a, b);
doi_cho(a,b);
printf(“Sau khi gọi hàm, a= %.0f, b= %.0f
”, a, b);
getch();
}
void doi_cho(float a, float b)
{
float temp; /* Khai báo biến địa phương */
temp = a;
a = b;
b = temp;
printf(“TRONG hàm doi_cho, a= %.0f, b= %.0f
”, a, b);
}
Kết quả hiển thị trên màn hình là:
Trước khi gọi hàm, a= 1, b= 2
TRONG hàm doi_cho, a= 2, b= 1
Sau khi gọi hàm, a= 1, b= 2
Sở dĩ như vậy là do, hai biến a, b được dùng trong cả chương trình chính và hàm. Trong hàm, hai biến này chỉ có tác dụng cục bộ trong hàm, khi quay trở về chương trình chính, sẽ không còn ảnh hưởng gì của phép đổi chỗ hai biến.
Ví dụ 2: Chương trình tìm số lớn nhất của 3 số, có sử dụng hàm:
#include <stdio.h>
#include <conio.h>
float max_ba_so(a,b,c) /* Không có nguyên mẫu */
float a, b, c;
{
float max;
max = a;
if (max < b) max = b;
if (max < c) max = c;
return max;
}
main()
{
float x, y, z, max_ba_so();
clrscr();
printf(“Vào 3 số cách nhau khoảng trống : ”);
scanf(“%f %f %f”, &x, &y, &z);
printf(“
Số lớn nhất = %f”, max_ba_so(x,y,z));
getch();
}
Ví dụ 3: Chương trình tính tổ hợp chập k của n, có sử dụng hàm tính giai thừa:
#include <stdio.h>
#include <conio.h>
int giai_thua( int m);
main()
{
int k, n;
clrscr();
printf(“Tính tổ hợp chập k của n.
k = ”); scanf(“%d”,&k);
printf(“
n = ”); scanf(“%d”,&n);
printf(
Kết quả C%d(%d) = ”, k, n);
printf(“ %d”, giai_thua(n)/(giai_thua(k)*giai_thua(n-k)));
getch();
}
int giai_thua( int m);
{
int i, g;
g = 1;
for (i=1; i<=m; i++) g = g*i;
return g;
}
Ghi chú: Ví dụ 3 chỉ tính cho các số nguyên khá nhỏ, nếu muốn sử dụng các số lớn hơn, có thể thay kiểu của g, giai_thua thành float, double hoặc long double!
4.1.3. Phát biểu return
Trong một hàm, return là một lệnh có tính chất thông báo quay về chương trình mẹ đã gọi đến hàm. Ngoài ra, nếu trong lệnh return có kèm theo biểu thức thì giá trị biểu thức sẽ được gán cho tên hàm trước khi trở về; ở đây, return có thể trả lại giá trị của cả một biểu thức cho tên hàm, không nhất thiết phải là một đại lượng riêng lẻ. Ví dụ:
/* Hàm tính giá trị hàm bậc nhất */
float f_b1(float a, float b, float x)
{
return (a*x+b);
}
Các hàm kiểu void, vì không phải trả lại một giá trị nào cho tên hàm nên không chứa lệnh return. Ví dụ:
/* Hàm hiển thị một biến mảng một chiều */
void hien_thi(int k , float x)
{
printf(“ a(%d) = %f ”, k, x );
}
Lệnh return có thể xuất hiện nhiều lần trong một hàm với chức năng không chỉ cung cấp giá trị cho tên hàm mà còn để dừng thực hiện hàm, chuyển điều khiển về chỗ đã gọi đến nó. Ví dụ:
/* Hàm cho trị tuyệt đối của số thực */
float tuyet_doi( float x)
{
if (x<0)
return (-x);
else
return x;
}
Khi viết return, nếu kiểu của biểu thức trong lệnh return khác với kiểu giá trị của hàm, C sẽ tự động chuyển đổi kiểu theo nguyên tắc chung. Ví dụ:
#include <stdio.h>
float kq( int i)
{ return i+1; }
4.1.4. Nguyên tắc hoạt động của hàm
Để hàm thực hiện, ta phải viết lời gọi hàm từ một hàm khác hoặc chính nó. Các hàm (kể cả hàm mẫu và hàm người dùng) đều được gọi thông qua tên theo đúng cấu trúc cú pháp, có dạng:
Tên_hàm([Danh sách tham số thực sự]);
Mỗi hàm có thể viết như một lệnh hay một đại lượng. Vì thế, người ta còn có thể sử dụng biến con trỏ để trỏ tới một hàm, cách khai báo con trỏ hàm như sau:
Kiểu_con_trỏ (*tên) (Danh_sách_kiểu_các_đối) ;
Ví dụ: Khai báo
float (*f) (int, float);
Lời khai báo trên có nghĩa là: f là một con trỏ hàm kiểu float, hàm này có hai đối, một đối kiểu int và một đối kiểu float.
Con trỏ hàm là một biến dùng để chứa địa chỉ của hàm, trong chương trình, ta phải có lệnh gán để gán tên hàm cho con trỏ hàm, cũng nghĩa là cho con trỏ trỏ tới hàm đó. Trong việc gán tên hàm cho con trỏ hàm, nhất thiết kiểu hàm và kiểu của con trỏ phải giống nhau.
Sau phép gán, ta có thể dùng con trỏ hàm thay cho tên hàm.
Ta cũng có thể tạo ra một mảng con trỏ hàm bằng khai báo có dạng:
Kiểu_con_trỏ (*tên[biểu_thức_nguyên]) (Danh_sách_kiểu_các_đối) ;
Ví dụ: Có khai báo
float (*f[10]) (int, float);
sẽ tạo ra một mảng con trỏ f kiểu float, một chiều, gồm các con trỏ hàm f[0], f[1], ..., f[9] để có thể trỏ tới 10 hàm có hai đối, một đối kiểu int và một đối kiểu float.
Các chương trình sau đây sẽ minh hoạ cho việc gọi hàm thông qua con trỏ.
Chương trình 1: Tính diện tích hình tròn
#include <stdio.h>
#include <conio.h>
#include <math.h>
#define Pi 3.14159
float D_TICH( float r)
{ return (Pi*r*r); }
float (*f) (float);
main()
{
f = D_TICH; clrscr();
printf(“Vào r=”); scanf(“%f”, &r);
printf(“Diện tích hình tròn = %12.5f ”, f(r) );
getch();
}
Chương trình 2: Tính tích phân xác định
ý tưởng: Xây dựng một hàm tính tích phân theo công thức Simson, không phụ thuộc vào dạng hàm cụ thể. Bên cạnh đó, sẽ có một hàm khác để định nghĩa hàm cần tính tích phân.
#include <stdio.h>
#include <math.h>
#define pi 3.14159
double TICH_PHAN(double (*f) (double), double a, double b)
{ int i, n =1000;
double s = 0, h = (b - a)/n;
for (i=0, i<n; i++)
s = s + f(a+i*h + h/2);
return h*s;
}
double HAM_B2(double x)
{return (2*x*x + 3*x) ; }
main()
{
printf(“
Tích phân hàm bậc hai = %10.2f”, TICH_PHAN(HAM_B2, 0,2));
printf(“
Tích phân hàm sin(x) = %10.2f”, TICH_PHAN(sin, 0,pi/2));
}
Sau khi chạy chương trình, màn hình sẽ hiển thị:
Tích phân hàm bậc hai = 11.33
Tích phân hàm sin(x) = 1.00
Khi gặp một lời gọi hàm thì hàm bắt đầu thực hiện. Cụ thể là, khi gặp một câu lệnh gọi hàm ở một vị trí nào đó trong một chương trình, máy sẽ rời bỏ vị trí đó để chuyển đến hàm được thực hiện. Cơ chế làm việc như sau:
Khi thực hiện đến lệnh nào, máy có một cơ chế đánh dấu lệnh đang thực hiện bằng cách đặt địa chỉ của lệnh đó trên một ô nhớ đặc biệt (thanh ghi) gọi là con trỏ lệnh hay còn gọi tắt là điều khiển. Điều khiển đặt vào lệnh nào thì lệnh đó được thực hiện.
Trong C, khi bắt đầu thực hiện một chương trình, điều khiển bao giờ cũng được đặt vào đầu hàm main(). Khi gặp lệnh gọi hàm, điều khiển sẽ được chuyển đến vị trí đầu hàm có tên trong lời gọi. Việc thực hiện hàm được gọi sẽ diễn ra như sau:
- Nếu là loại hàm có tham số hình thức, trước tiên máy sẽ gán các tham số thực sự cho các tham số hình thức một cách tuần tự, tương ứng theo thứ tự được viết trong lời gọi.
- Máy thực hiện các lệnh trong hàm giống như một chương trình bình thường, có thể có sự tuần tự, rẽ nhánh hay lặp tùy vào cấu trúc của chương trình trong thân hàm.
- Khi gặp lệnh return hoặc dấu đóng ngoặc nhọn (}) cuối cùng của thân hàm, máy sẽ thoát khỏi hàm để trở về chương trình gọi nó và đặt điều khiển vào lệnh kế tiếp của lệnh vừa gọi hàm trong chương trình gọi.
4.1.5. Sử dụng giá trị trả về một hàm
Sau khi gọi một hàm để thực hiện, hàm có thể trả về chương trình gọi nó các giá trị. Tất cả các giá trị trả về từ hàm đều có giá trị thực sự và do đó có thể tham gia vào các biểu thức. Các giá trị trả về một hàm có thể là:
- Một giá trị trả về là tên hàm (trường hợp này đã quá quen thuộc);
- Một giá trị trả về là con trỏ;
- Các tham số ra là các biến con trỏ.
Trong trường hợp giá trị trả về của hàm thông qua con trỏ thì, một nguyên tắc của C là: Chỉ những tham số thực sự khi truyền cho hàm là biến con trỏ (thông qua địa chỉ) mới có thể là tham số ra để ghi lại được các kết quả vừa tính toán được trong hàm khi kết thúc. Khi tham số nhận là biến con trỏ thì tham số truyền phải là địa chỉ. Để thấy rõ vấn đề này, ta xét các ví dụ sau:
Ví dụ 1: Tính chu vi và diện tích hình chữ nhật
Ví dụ này sử dụng tham số hình thức là biến con trỏ.
ý tưởng của việc xây dựng chương trình là: Sử dụng một hàm để tính chu vi và diện tích hình chữ nhật. Hàm này, ngoài hai tham số hình thức để nhận giá trị hai cạnh là x, y, sẽ có thêm hai tham số hình thức dùng làm tham số ra là biến con trỏ chỉ tới trường nhớ chứa chu vi và diện tích. Văn bản chương trình như sau:
#include <stdio.h>
#include <conio.h>
void chu_nhat(float, float, float *, float * );
/* Có thể viết nguyên mẫu không cần tên biến như thế này */
void main()
{
float a, b, chuvi, dientich ;
clrscr();
printf(“
Vào chiều dài và chiều rộng = ”);
scanf(“%f %f ”, &a, &b);
printf(“
%p %f % p %f %p %p ”, &a, a, &b, b, &dientich, &chuvi);
chu_nhat(a, b, &dientich, &chuvi);
printf(“
Diện tích = %f”, dientich);
printf(“
Chu vi = %f”, chuvi);
getch();
}
/*******************************************/
void chu_nhat( float x, float y, float *dt, float *cv)
{
printf(“
%p %f % p %f %p %p ”, &x, x, &y, y, dt, cv);
*dt = x + y ; *cv = 2 * ( x + y );
}
Ví dụ 2: Tráo đổi giá trị của hai biến.
Ta đã thấy, trong ví dụ 1 của phần 4.1.2, không sử dụng địa chỉ mà chỉ qua tên biến để truyền tham số, khi ra khỏi hàm, giá trị của hai biến a và b không được đổi chỗ. Ví dụ này sẽ sử dụng địa chỉ trong việc truyền tham số cho hàm và hàm có tham số hình thức là biến con trỏ.
#include <stdio.h>
#include <conio.h>
void doi_cho(float *x, float *y);
main()
{
float a=1, b=2;
clrscr();
printf(“Trước khi gọi hàm, a= %.0f, b= %.0f
”, a, b);
doi_cho(&a, &b);
printf(“Sau khi gọi hàm, a= %.0f, b= %.0f
”, a, b);
getch();
}
void doi_cho(float *x, float *y)
{
float temp; /* Khai báo biến địa phương*/
temp = *x;
*x = *y;
*y = temp;
printf(“TRONG hàm doi_cho, a= %.0f, b= %.0f
”, *x, *y);
}
Kết quả hiển thị trên màn hình là:
Trước khi gọi hàm, a= 1, b= 2
TRONG hàm doi_cho, a= 2, b= 1
Sau khi gọi hàm, a= 2, b= 1
Ví dụ 3: Nhập một dãy n số nguyên (n ≤ 200) vào mảng một chiều a và một số nguyên x, sau đó tìm một phần tử đầu tiên trong mảng a có giá trị bằng x. Tìm kiếm thực hiện theo kiểu tuần tự, tức là tìm từ đầu mảng. Nếu tìm thấy thì dừng và trả về con trỏ chứa địa chỉ phần tử vừa tìm thấy, ngược lại, nếu không tìm thấy hàm trả về giá trị NULL.
#include <stdio.h>
#include <conio.h>
int *tim_kiem(int *a, int n, int x);
int a[200];
void main()
{
int *p, n, x;
printf(“Số phần tử của mảng a là n = “); scanf(“%d”, &n);
for (x=0; x<n; x++)
{ printf(“
Vào a[%d] = ’’, x ); scanf(“%d”, &a[x]); }
printf(“
Vào phần tử cần tìm x= ”);
scanf(“%d”, &x);
p = tim_kiem(a, n, x);
if ( p != NULL )
printf(“
Phần tử tìm thấy = %d ”, *p);
else
printf(“
Trong mảng a không có phần tử = %d ”, x);
getch();
}
/* Đây là hàm tim_kiem */
int *tim_kiem(int *a, int n, int x)
{
int i, kq, *pi ;
kq = 0; pi = a; /* kq=0 là không tìm thấy, kq=1 là tìm thấy */
for (i=0; i<n; i++)
{ if (*pi ==x)
{ kq=1; break; }
else ++pi;
}
if (kq = = 1) return pi; else return NULL;
}
4.2. Truyền tham số cho hàm
4.2.1. Biến toàn cục và biến địa phương
4.2.1.1. Biến toàn cục
Biến toàn cục, nói chung, là biến được sử dụng chung ở nhiều hàm trong một chương trình.
Khi các biến được khai báo ở ngoài các hàm thì biến đó là biến toàn cục.
Trong C, mỗi biến toàn cục có một miền tác dụng nhất định, tức là nó chỉ có tác dụng, được phép sử dụng ở những chỗ nào đó trong chương trình. Miền tác dụng của các biến toàn cục là toàn bộ phần chương trình nguồn đứng sau khai báo biến đó.
Về nguyên tắc, có thể khai báo biến toàn cục ở nhiều chỗ khác nhau trong chương trình và khai báo ở đâu thì nó có tác dụng từ đó đến cuối chương trình. Tuy vậy, để dễ quản lí và kiểm soát, người ta thường khai báo các biến toàn cục ở ngay đầu chương trình.
Ví dụ: Khai báo hai biến: n, x là biến toàn cục
main()
{
. . . . .
}
int n; float x;
ham1( ...)
{ ..... }
ham2(...)
{ ..... }
Khi đó, hai biến n và x có tác dụng với hai hàm ham1 và ham2 mà không có tác dụng với hàm main().
Việc sử dụng biến toàn cục có một tiện lợi là có thể làm việc với biến toàn cục ở tất cả mọi nơi thuộc miền tác dụng của biến đó, nhưng đây lại là điều đáng lo ngại bởi nếu có sự thay đổi giá trị biến tại một chỗ nào đó mà không kiểm soát được sẽ làm ảnh hưởng tới chương trình chung, dẫn tới một kết quả sai lầm đáng tiếc. Khi viết các hàm, nên tâm niệm là: các biến càng độc lập thì càng dễ kiểm soát, cả trong khi viết, chạy thử và hiệu chỉnh chương trình.
Lưu ý: Biến toàn cục tồn tại trong suốt thời gian chương trình chứa nó hoạt động. Việc cấp phát bộ nhớ cho các biến này được xác định ngay khi kết nối các module chương trình. Cách cấp phát như thế được gọi là lớp cấp phát bộ nhớ tĩnh (static allocation class) và do đó các biến này cũng được gọi là biến tĩnh. Tất cả các biến tĩnh đều được khởi tạo với giá trị ban đầu bằng 0 trừ khi trong khai báo có khởi tạo giá trị cho chúng.
4.2.1.1. Biến địa phương
Biến địa phương là biến được khai báo trong một hàm. Khi đó, miền tác dụng của biến địa phương là chính hàm mà nó được khai báo. Các biến địa phương sẽ không có mối liên hệ nào với biến toàn cục có cùng tên và cùng kiểu.
Các tham số hình thức trong hàm cũng thuộc loại biến địa phương, chúng chỉ có ý nghĩa khi hàm đó được gọi đến và chỉ có tác dụng trong hàm.
Ví dụ: Một chương trình C có dạng:
main()
{
float a;
char c;
...........
}
float ham1(...)
{
char c;
.........
}
Thế thì, các biến a, c là biến địa phương của hàm main(); Biến n, c là biến địa phương của hàm ham1( ); Biến c trong hàm main( ) không liên quan gì đến biến c trong hàm ham1( ); biến n trong hàm ham1( ) không liên quan gì đến biến toàn cục n.
Lưu ý: [1]. Biến địa phương chỉ tồn tại trong thời gian hàm đó hoạt động. Việc cấp phát bộ nhớ cho biến địa phương không được thực hiện ngay từ đầu mà chỉ khi nào gọi đến hàm nào thì mới cấp phát bộ nhớ cho các biến địa phương được khai báo trong hàm đó, mỗi lần gọi hàm là một lần cấp phát; Sau khi thực hiện xong hàm sẽ giải phóng ngay bộ nhớ khi ra khỏi hàm. Vì thế các biến địa phương không lưu giữ kết quả cho các lần sau. Cách cấp phát như thế gọi là cấp phát bộ nhớ tự động (automatic). Cách cấp phát bộ nhớ tự động không khởi tạo giá trị ban đầu cho biến địa phương.
[2]. Có thể yêu cầu chương trình cấp phát bộ nhớ cho các biến địa phương theo cơ chế tĩnh (static), nghĩa là: cấp phát bộ nhớ ở một vùng nhớ cố định được dành riêng cho các biến của hàm – gọi là cấp phát biến địa phương tĩnh (biến địa phương static). Vì thế, khi hàm đã kết thúc hoạt động, giá trị các biến kiểu này vẫn có thể tồn tại. Để thực hiện yêu cầu cấp phát này, ta khai báo biến biến địa phương tĩnh static bằng cách thêm từ khoá static vào trước mỗi khai báo. Các biến địa phương static đều được khởi tạo giá trị ban đầu bằng 0, trừ khi trong khai báo có khởi tạo giá trị cho chúng.
Ví dụ:
#include <stdio.h>
void vi_du(void);
main()
{ int n;
for (n=1; n<5; n++) vi_du();
getch();
}
void vi_du(void)
{
static int i;
i++;
printf(“
Gọi lần %d ” , i);
}
Biến i khai báo trong hàm vi_du() là biến tĩnh static. Mỗi lần gọi hàm vi_du(), biến i sẽ tăng 1, có nghĩa là giá trị của nó vẫn được giữ lại sau khi ra khỏi hàm. Bạn chạy thử chương trình để kiểm nghiệm sẽ thấy kết quả hiển thị như sau:
Gọi lần 1
Gọi lần 2
Gọi lần 3
Gọi lần 4
[3]. Có thể khai báo biến địa phương sử dụng bộ nhớ thanh ghi bằng cách thêm từ khoá register và trước khai báo biến địa phương. Các biến này được gọi là biến địa phương thanh ghi (hoặc biến địa phương register). Các thanh ghi (register) là các ô nhớ đặc biệt, có tốc độ cực nhanh, đặt ngay trong CPU của các máy tính để phục vụ quá trình xử lí thông tin của CPU với chức năng cơ bản là ghi địa chỉ các toán hạng, các kết quả tính toán trung gian, ... Với ưu điểm là tốc độ cao, khi có một biến kiểu int hoặc kiểu char nào đó được sử dụng nhiều, có thể khai báo bố trí bộ nhớ register cho biến để tăng tốc độ tính toán. Tuy nhiên, điều đó nên hạn chế dùng vì số lượng thanh ghi là hữu hạn.
Ví dụ: Hàm vi_du() ở trên có thể viết là:
void vi_du(void)
{
i++;
printf(“
Gọi lần %d ” , i);
}
Cách cấp phát bộ nhớ thanh ghi không khởi tạo giá trị ban đầu cho biến địa phương. Hãy gọi thử nhiều lần hàm trên để có nhận xét.
4.2.2. Tham số hình thức của hàm
Khi một hàm cần nhận các giá trị từ một hàm khác gọi đến nó thì bắt buộc hàm đó phải có các tham số. Tham số là các biến (biến đơn, biến con trỏ, biến mảng), viết cách nhau dấu phảy, dùng làm dữ liệu hình thức để hàm xử lí - gọi là tham số hình thức.
Các tham số hình thức bao giờ cũng là các biến (tượng trưng cho các trường nhớ) để cung cấp giá trị cho hàm khi gọi hàm thực hiện - gọi là tham số vào và lưu trữ các kết quả tính toán trong hàm - gọi là tham số ra. Ngoài ra, có những biến vừa là tham số vào (ban đầu) lại vừa là tham số ra (sau khi gọi hàm).
4.2.3. Truyền tham số
Quá trình làm cho các tham số hình thức nhận các giá trị thực khi gọi đến hàm, gọi là truyền tham số cho hàm. Quá trình này chỉ diễn ra với các hàm có tham số hình thức.
Những tham số hình thức của hàm sẽ nhận các giá trị thực sự nhờ việc truyền tham số mỗi khi hàm được gọi đến. Những giá trị thực sự, các biến ta ghi vào sau tên hàm trong lời gọi hàm để hàm thực hiện, được gọi là tham số thực sự. Tham số thực sự của hàm có thể là các giá trị cụ thể (các đại lượng, các biểu thức tổng quát), con trỏ hay biến mảng nhưng phải phù hợp về kiểu với tham số hình thức tương ứng của hàm được gọi.
4.2.3.1. Truyền tham số bởi tham trị
Là cách truyền tham số thực sự cho hàm bằng các giá trị thật của các hằng, các biến (thể hiện qua tên biến), các hàm hay các biểu thức.
Ví dụ: Xem ví dụ 3 phần 4.1.2. - Tính tổ hợp chập k của n.
4.2.3.2. Truyền tham số bằng tham chiếu
Là cách truyền tham số thực sự cho hàm thông qua các biến con trỏ để tham chiếu tới các vùng dữ liệu trong bộ nhớ.
Điểm đáng lưu ý là: Chỉ những tham số thực sự khi truyền cho hàm là biến con trỏ (thông qua địa chỉ) mới có thể ghi lại được các kết quả vừa tính toán được trong hàm khi kết thúc.
Ví dụ: Xem ví dụ 2 và 3 phần 4.1.5. - “Tráo đổi giá trị hai số” và “Tìm một phần tử trong một dãy số”.
4.2.3.3. Truyền tham số với mảng
- Có thể dùng các phần tử mảng làm tham số thực sự để truyền tham số cho hàm. Khi đó có thể coi mỗi phần tử mảng như một biến và có thể truy nhập qua tên hay con trỏ.
- Ngoài ra, khi tham số hình thức của hàm là một mảng, có thể truyền cả một mảng cho hàm bằng cách cung cấp cho hàm tham số thực sự là địa chỉ của mảng thông qua tên mảng hoặc con trỏ hay địa chỉ phần tử đầu của mảng.
- Khi muốn truyền một dãy con trong mảng cho hàm, cũng làm tương tự như truyền cả mảng nhưng hãy cung cấp cho hàm địa chỉ của phần tử đầu tiên của dãy con để hàm nhận các phần tử bắt đầu từ đó.
Ví dụ: Nhập một dãy số vào mảng trong chương trình chính, truyền mảng cho hàm rồi hàm sẽ hiển thị dãy số đó.
#include <stdio.h>
void DAYSO(float a[], int k)
{ int i;
clrscr();
printf(“DAY SO RA:
”);
for (i = 0; i < k; i++)
printf(“%.2f ”, a[k]);
getch();
}
main()
{
in i, n; float x[100];
clrscr();
printf(“Nhập số phần tử của dãy n =:”);
scanf(“%f”, &n);
for (i = 0; i < n; i++)
{ printf(“a[%d] = ”, i+1);
scanf(“%f”, &x[i]);
}
DAYSO(x, n); /*Dòng (*) */
}
Dòng (*) để gọi hàm có thể viết là:
DAYSO(&x[0], n);
Nếu muốn truyền từ phần tử thứ 3 trở đi, có thể viết là:
DAYSO(x+2, n);
hoặc: DAYSO(&x[2], n);
4.3. Hàm đệ quy
Trong lập trình, người ta có thể sử dụng tính chất đệ quy của hàm. Hàm đệ quy là hàm mà trong thân hàm có chứa lời gọi đến chính nó một cách tường minh hay tiềm ẩn. Khi hàm gọi tới chính nó trong quá trình đệ quy, mỗi lần gọi hàm, máy sẽ tạo ra một tập các biến cấp phát tự động mới độc lập tuyệt đối với các biến địa phương đã được tạo ra trong những lần gọi trước. Có bao nhiêu lần gọi hàm thì cũng sẽ có bấy nhiêu lần thoát khỏi hàm và cứ mỗi lần thoát khỏi hàm, một tập các biến cấp phát tự động sẽ bị giải phóng (xoá khỏi bộ nhớ). Sự tương ứng giữa những lần gọi hàm và thoát khỏi hàm được thực hiện theo thứ tự ngược lại, nghĩa là lần thoát khỏi hàm đầu tiên ứng với lần gọi hàm cuối cùng, ..., lần thoát khỏi hàm cuối cùng ứng với lần gọi hàm đầu tiên. Ta minh hoạ điều đó trong các ví dụ sau:
Ví dụ 1: Tính giai thừa của số tự nhiên n nhập từ bàn phím.
Ta có hai cách tính giai thừa:
Theo định nghĩa toán học: n! = 1.2.3. ... . (n-1).n
Theo định nghĩa đệ quy:
n! =
{
1 khi n = 0
n.(n-1)! khi n ≥ 1
Khi đó, hàm tính giai thừa được định nghĩa như sau:
long int GIAI_THUA(int n)
{ if (n= =0) return 1;
else return (n*GIAI_THUA(n-1)) ;
}
Chương trình C được viết như sau:
# include <stdio.h>
# include <conio.h>
long int GIAI_THUA(int n);
main()
{
clrscr();
printf(“ So tu nhien n = ?”); scanf(“%d”, &n);
printf(“
%d ! = %ld”, n, GIAI_THUA(n));
getch();
}
long int GIAI_THUA(int n)
{
if (n= =0) return 1;
else return (n*GIAI_THUA(n-1)) ;
}
Ví dụ 2: Hiển thị một xâu rồi xuống dòng, hiển thị lại xâu đó từ cuối lên.
# include <stdio.h>
# include <conio.h>
void TIEN_LUI(char s[], int i);
void main()
{
char s[80] = “
HOC VIEN TAI CHINH
”;
int i =0;
clrscr();
TIEN_LUI(s, i);
getch();
}
void TIEN_LUI(char s[], int i)
{
if ( s([i] )
{printf(“%c ”, s[i] ); i++; TIEN_LUI(s,i); }
printf(“%c”,s[i]);
}
Ví dụ 3: Tìm ước số chung lớn nhất của hai số tự nhiên a, b.
Ta có thể định nghĩa cho ước số chung lớn nhất của hai số như sau:
USCLN(a,b) =
{
a nếu b = 0
USCLN(b, phần dư của a/b) nếu b ≠ 0
Với cách hiểu như vậy, hàm tìm USCLN được viết dưới đây:
int USCLN(int x, int y)
{
if (y = =0 ) return (x);
else return (USCLN(y, x%y));
}
Tuy vậy, vẫn có thể sử dụng thuật toán Euclide có khử đệ quy, cách làm này sẽ hiệu quả hơn tuy chương trình dài hơn:
int USCLN(int x, int y)
{
int so_du;
while (y != 0)
{so_du = x%y;
x = y;
y = so_du;
}
return (x);
}
Ví dụ 4: Hàm tính căn bậc 3 của một số thực x có sử dụng đệ quy theo hai hàm exp là ln.
Ta có nhận thấy:
=
{
nếu x≥0
- nếu x<0
float sqrt3(float x)
{
if (x= =0)
return (0);
if (x<0)
return (-sqrt3(-x));
return (exp((log(x)/3)));
}
Nói chung, người ta thích dùng lặp hơn là dùng đệ quy vì lặp có một chương trình tường minh và dễ kiểm soát hơn. Mặc dù trong đệ quy có lặp, nhưng lặp trong đệ quy là lặp các lời gọi hàm, gây ra tốn thời gian của bộ vi xử lí và không gian nhớ. Mỗi lần gọi hàm, máy lại cần thêm một bản sao của hàm, do đó làm tốn thêm các khoảng nhớ cho các biến của hàm, cho địa chỉ trở về, ...
4.4. Macro
Ngoài việc sử dụng hàm, trong C, còn một cách tổ chức chương trình để thay thế cho việc viết lại một đoạn chương trình ở nhiều chỗ, đó là sử dụng macro.
Macro là một tập hợp các kí tự bất kì (thường là các lệnh hoặc biểu thức) được đại diện bằng một tên nào đó. Khi dịch chương trình, nếu gặp các tên macro, C sẽ thay thế tên macro bằng dãy kí tự tương ứng.
Để định nghĩa một macro, ta sử dụng cú pháp:
# define tên_macro tập_hợp_kí_ tự
Macro và hàm giống nhau ở chỗ đều dùng tên khi viết chương trình nhưng có những điểm khác biệt sau đây:
· Chương trình dùng macro dài hơn dùng hàm vì mỗi lần gọi macro, C sẽ chèn đoạn chương trình tương ứng vào vị trí gọi.
· Chương trình dùng macro chạy nhanh hơn dùng hàm vì không mất thủ tục gọi hàm.
· Macro không có tính đệ quy và không phải là một module chương trình độc lập.
Nói chung, chỉ nên dùng macro để thay thế các cụm lệnh đơn giản.
Ví dụ: Chương trình sau có sử dụng macro
# include <stdio.h>
# include <conio.h>
# define Vao(x) printf(“Số vào = %d”, x); scanf(“%d”, &x)
# define Ra(x) printf(“ Đưa ra %d”, x)
# define lap_phuong(x) (x)*(x)*(x)
main()
{
int x, y; clrscr();
Vao(x);
Ra(x);
y=lap_phuong(x);
Ra(y);
}
Bạn đang đọc truyện trên: Truyen247.Pro