Bài 15: Hàm [Lý Thuyết]
Mục tiêu:
Kết thúc bài học này, bạn có thể:
Ø Tìm hiểu về cách sử dụng các hàm
Ø Tìm hiều về cấu trúc của một hàm
Ø Khai báo hàm và các nguyên mẫu hàm
Ø Thảo luận các kiểu khác nhau của biến
Ø Tìm hiểu cách gọi các hàm:
·
Gọi
bằng giá trị
·
Gọi
bằng tham chiếu
Ø Tìm hiểu về các qui tắc về phạm vi của hàm
Ø Tìm hiểu các hàm trong các chương trình có
nhiều tập tin
Ø Tìm hiểu về các lớp lưu trữ
Ø Tìm hiểu về con trỏ hàm.
Giới thiệu
Một hàm là một đoạn chương trình thực hiện một tác vụ được định
nghĩa cụ thể. Chúng thực chất là những đoạn chương trình nhỏ giúp giải quyết
một vấn đề lớn.
15.1 Sử dụng các hàm
Nói chung, các hàm được sử dụng trong C để thực thi một chuỗi các
lệnh liên tiếp. Tuy nhiên, cách sử dụng các hàm thì không giống với các vòng
lặp. Các vòng lặp có thể lặp lại một chuỗi các chỉ thị với các lần lặp liên
tiếp nhau. Nhưng việc gọi một hàm sẽ sinh ra một chuỗi các chỉ thị được thực
thi tại vị trí bất kỳ trong chương trình. Các hàm có thể được gọi nhiều lần khi
có yêu cầu. Giả sử một phần của mã lệnh trong một chương trình dùng để tính tỉ
lệ phần trăm cho một vài con số. Nếu sau đó, trong cùng chương trình, việc tính
toán như vậy cần phải thực hiện trên những con số khác, thay vì phải viết lại
các chỉ thị giống như trên, một hàm có thể được viết ra để tính tỉ lệ phần trăm
của bất kỳ các con số. Sau đó chương trình có thể nhảy đến hàm đó, để thực hiện
việc tính toán (trong hàm) và trở về nơi nó đã được gọi. Điều này sẽ được giải
thích rõ ràng hơn khi thảo luận về cách hoạt động của các hàm.
Một điểm quan trọng khác là các hàm thì dễ viết và dễ hiểu. Các
hàm đơn giản có thể được viết để thực hiện các tác vụ xác định. Việc gỡ rối
chương trình cũng dễ dàng hơn khi cấu trúc chương trình dễ đọc, nhờ vào
sự đơn giản hóa hình thức của nó. Mỗi hàm có thể được kiểm tra một cách độc lập
với các dữ liệu đầu vào, với dữ liệu hợp lệ cũng như không hợp lệ. Các chương
trình chứa các hàm cũng dễ bảo trì hơn, bởi vì những sửa đổi, nếu yêu cầu, có
thể được giới hạn trong các hàm của chương trình. Một hàm không chỉ được gọi từ
các vị trí bên trong chương trình, mà các hàm còn có thể đặt vào một thư viện
và được sử dụng bởi nhiều chương trình khác, vì vậy tiết kiệm được thời gian
viết chương trình.
15.2 Cấu trúc hàm
Cú pháp tổng quát của một hàm trong C là:
type_specifier function_name (arguments)
{
//body of the function
//return statement
}
type_specifier xác
định kiểu dữ liệu của giá trị sẽ được trả về bởi hàm. Nếu không có kiểu được
đưa ra, hàm cho rằng trả về một kết quả số nguyên. Các đối số được phân cách
bởi dấu phẩy. Một cặp dấu ngoặc rỗng () vẫn phải xuất hiện sau tên hàm ngay cả
khi nếu hàm không chứa bất kỳ đối số nào. Các tham số xuất hiện trong cặp dấu
ngoặc () được gọi là tham số hình thức hoặc đối
số hình thức. Phần thân của hàm có thể chứa một hoặc nhiều câu lệnh.
Một hàm nên trả về một giá trị và vì vậy ít nhất một lệnh return phải có trong hàm.
15.2.1 Các đối số của một
hàm
Trước khi thảo luận chi tiết về các đối số, xem ví dụ sau:
#include <stdio.h>
main()
{
int i;
for(i =1; i <=10; i++)
printf(“\nSquare of %d is %d “,
i,squarer (i));
}
squarer(int
x)
/*
int x; */
{
int j;
j = x * x;
return(j);
}
Chương trình trên tính tính bình phương các số từ 1 đến 10. Điều
này được thực hiện bằng việc gọi hàm squarer. Dữ liệu được truyền từ thủ tục gọi (trong trường hợp trên
là hàm main()) đến hàm được gọi squarer thông qua các đối số. Trong thủ tục
gọi, các đối số được biết như là các
đối số thực và trong định nghĩa của hàm được gọi (squarer()) các đối số được gọi là các đối số hình thức.
Kiểu dữ liệu của các đối số thực phải cùng kiểu với các đối số hình
thức. Hơn nữa, số lượng và thứ tự của các tham số thực phải giống như của các
tham số hình thức.
Khi một hàm được gọi, quyền điều khiển sẽ được chuyển đến cho nó,
ở đó các đối số hình thức được thay
thế bởi các đối số thực. Sau đó hàm
được thực thi và khi bắt gặp câu lệnh return, nó sẽ
chuyển quyền điều khiển cho chương trình gọi nó.
Hàm squarer() được gọi bằng cách truyền số cần được
tính bình phương. Đối số x có thể được khai báo theo một trong
các cách sau khi định nghĩa hàm:
Phương pháp 1:
squarer(int x)
//x được định nghĩa cùng với kiểu dữ
liệu trong cặp dấu ngoặc ().
Phương pháp 2:
squarer(x)
int x;
//x được đặt trong cặp dấu ngoặc (), và
kiểu của nó được khai báo ngay sau tên hàm.
Chú ý:
·
Trong trường hợp 1: Khi các đối số được khai báo trong cặp
dấu ngoặc (), mỗi
đối số phải được định nghĩa riêng lẻ, cho dù chúng có cùng kiểu dữ liệu. Ví
dụ, nếu x và y là hai đối số của một hàm abc(),
thì abc(char x, char
y) là một khai báo đúng
và abc(char x, y) là sai.
·
Trong trường hợp 2: x phải được định nghĩa
ngay sau tên hàm, trước khối lệnh. Điều này thật tiện lợi khi có nhiều tham số có cùng kiểu dữ liệu được
truyền. Trong trường hợp như vậy, chỉ phải chỉ rõ kiểu đề một lần duy nhất tại
điểm bắt đầu.
15.2.2 Sự trả về từ
hàm
Lệnh return có hai mục đích:
Ø Ngay lập tức trả điều khiển từ hàm về
chương trình gọi.
Ø Bất kỳ cái gì bên trong cặp dấu ngoặc () theo sau return được trả về như là một giá trị
cho chương trình gọi.
Trong hàm squarer(), một biến j kiểu
int được định nghĩa để lưu giá trị bình
phương của đối số truyền vào. Giá trị của biến này được trả về cho hàm gọi
thông qua lệnh return. Một hàm có thể thực hiện một tác vụ xác định
và trả quyền điều khiển về cho thủ tục gọi nó mà không cần trả về bất kỳ giá
trị nào. Trong trường hợp như vậy, lệnh return có thể được
viết dạng return(0) hoặc return. Chú ý rằng, nếu
một hàm cung cấp một giá trị trả về và nó không làm điều đó thì nó sẽ trả về
giá trị không thích hợp.
Trong chương trình tính bình phương của các số, chương trình
truyền dữ liệu tới hàm squarer thông qua các đối số. Có thể có
các hàm được gọi mà không cần bất kỳ đối số nào. Ở đây, hàm thực hiện một chuỗi
các lệnh và trả về giá trị, nếu được yêu cầu.
Chú ý rằng, hàm squarer() cũng có thể được viết
như sau
squarer(int x)
{
return(x*x);
}
Ở đây một biểu thức hợp lệ được xem như một đối số trong câu lệnh return. Trong thực tế, lệnh return có thể
được sử dụng theo một trong các cách sau đây:
return;
return(hằng);
return(biến);
return(biểu thức);
return(câu lệnh đánh giá); ví dụ: return(a>b?a:b);
Tuy nhiên, giới hạn của lệnh return là nó chỉ có
thể trả về một giá trị duy nhất.
15.2.3 Kiểu của một
hàm
type-specifier được
sử dụng để xác định kiểu dữ liệu trả về của một hàm. Trong ví dụ trên, type-specifier không
được viết bên cạnh hàm squarer(), vì squarer() trả về
một giá trị kiểu int. type-specifier
là không bắt buộc nếu một giá trị kiểu số nguyên được trả về hoặc nếu không có
giá trị nào được trả về. . Tuy nhiên, tốt hơn nên chỉ ra kiểu dữ liệu trả về là
int nếu một giá trị số nguyên được trả về và
tương tự dùng void nếu hàm không trả về giá trị nào.
15.3 Gọi hàm
Có thể gọi một hàm từ chương trình chính bằng cách sử dụng tên của
hàm, theo sau là cặp dấu ngoặc (). Cặp dấu ngoặc là cần thiết để nói với trình biên dịch là
đây là một lời gọi hàm. Khi một tên hàm được sử dụng trong chương trình gọi,
tên hàm có thể là một phần của một một lệnh hoặc chính nó là một câu lệnh. Mà
ta đã biết một câu lệnh luôn kết thúc với một dấu chấm phẩy (;). Tuy nhiên, khi định nghĩa hàm, không được dùng dấu chấm phầy ở cuối phần định nghĩa. Sự vắng mặt
của dấu chấm phẩy nói với trình biên dịch đây là phần định nghĩa của hàm và
không được gọi hàm.
Một số điểm cần nhớ:
Ø Một dấu chấm phẩy được dùng ở cuối câu
lệnh khi một hàm được gọi, nhưng nó không được dùng sau một sự định nghĩa hàm.
Ø Cặp dấu ngoặc () là bắt buộc theo sau tên hàm, cho dù hàm
có đối số hay không.
Ø Hàm gọi đến một hàm khác được gọi là hàm
gọi hay thủ tục gọi. Và hàm được gọi đến còn được gọi
là hàm được gọi hay thủ tục được gọi.
Ø Các hàm không trả về một giá trị số nguyên
cần phải xác định kiểu của giá trị được trả về.
Ø Chỉ một giá trị có thể được trả về bởi một
hàm.
Ø Một chương trình có thể có một hoặc nhiều
hàm.
15.4 Khai báo hàm
Một hàm nên được khai báo trong hàm main()
trước khi nó được định nghĩa hoặc sử dụng. Điều này phải được thực hiện trong
trường hợp hàm được gọi trước khi nó được định nghĩa.
Xem ví dụ:
#include <stdio.h>
main()
{
…
address(…);
…
}
address(…)
{
…
}
Hàm main() gọi hàm address() và hàm address()
được gọi trước khi nó được định nghĩa. Mặc dù, nó không được khai báo trong
hàm main() thì điều này có thể thực hiện được trong một số trình
biên dịch C, hàm address() được gọi mà không cần khai báo gì thêm
cả. Đây là sự khai báo không tường minh của một hàm.
Trong trình biên dịch Dev-C++,
ta cần phải khai báo nguyên mẫu hàm
trước hàm main() nếu như muốn định nghĩa hàm đó sau hàm main(), ví dụ:
#include <stdio.h>
address(…);
main()
{
…
address();
…
}
address(…)
{
…
}
15.5 Các nguyên mẫu
hàm
Một nguyên mẫu hàm là một khai báo hàm trong đó xác định rõ kiểu
dữ liệu của các đối số và giá trị trả về. Thông thường, các hàm được khai báo
bằng cách xác định kiểu của giá trị được trả về bởi hàm, và tên hàm. Tuy nhiên,
chuẩn ANSI C cho phép số lượng và kiểu dữ liệu của các đối số hàm được khai
báo. Một hàm abc() có hai đối
số kiểu int là x
và y, và trả về một giá trị kiểu char, có thể được khai báo như sau:
char
abc();
hoặc
char
abc(int x, nt y);
Cách định nghĩa sau được gọi là nguyên mẫu hàm. Khi
các nguyên mẫu được sử dụng, C có thể tìm và thông báo bất kỳ kiểu dữ liệu
không hợp lệ khi chuyển đổi giữa các đối số được dùng để gọi một hàm với sự
định nghĩa kiểu của các tham số. Một lỗi sẽ được thông báo ngay khi có sự khác
nhau giữa số lượng các đối số được sử dụng để gọi hàm và số lượng các tham số
khi định nghĩa hàm.
Cú pháp tổng quát của một
nguyên mẫu hàm:
type function_name(type parm_namel,type
parm_name2,..type
parm_nameN);
Khi hàm được khai báo không có các thông tin nguyên mẫu, trình
biên dịch cho rằng không có thông tin về các tham số được đưa ra. Một hàm không
có đối số có thể gây ra lỗi khi khai báo không có thông tin nguyên mẫu. Để
tránh điều này, khi một hàm không có tham số, nguyên mẫu của nó sử dụng void trong cặp dấu ngoặc (). Như đã nói ở trên, void cũng được sử dụng để khai báo tường minh một hàm
không có giá trị trả về.
Ví dụ, nếu một hàm noparam()
trả về kiểu dữ liệu char và không có các tham số được gọi, có thể
được khai báo như sau:
char noparam(void);
Khai báo trên chỉ ra rằng hàm không có tham số, và bất kỳ lời gọi
có truyền tham số đến hàm đó là không đúng.
Khi một hàm không nguyên mẫu được gọi, tất cả các kiểu char được đổi thành kiểu int và
tất cả kiểu float được đổi thành kiểu double. Tuy nhiên, nếu một hàm là nguyên mẫu, thì các kiểu đã đưa
ra trong nguyên mẫu được giữ nguyên và không có sự tăng cấp kiểu xảy ra.
15.6 Các biến
Như đã thảo luận, các biến là những vị trí được đặt tên trong bộ
nhớ, được sử dụng để chứa giá trị có thể hoặc không thể được sửa đổi bởi một
chương trình hoặc một hàm. Có ba loại biến cơ bản: biến cục bộ, tham số
hình thức, và biến toàn cục.
1-Biến cục bộ: là những biến được khai báo bên trong một
hàm.
2-Tham số hình thức: được khai báo trong một định nghĩa hàm như
là các tham số.
3-Biến toàn cục: được khai báo bên ngoài các hàm.
15.6.1 Biến cục bộ
Biến cục bộ còn được gọi là biến động, từ khoá auto được sử dụng để khai báo chúng. Chúng chỉ được tham
chiếu đến bởi các lệnh bên trong của khối lệnh mà biến được khai báo. Để rõ
hơn, một biến cục bộ được tạo ra trong lúc vào một khối và bị huỷ trong lúc đi
ra khỏi khối đó. Khối lệnh thông thường nhất mà trong đó một biến cục bộ được
khai báo chính là hàm.
Xem đoạn mã lệnh sau:
void blkl(void) /* void denotes no
value returned*/
{
char ch;
ch = ‘a’;
…
}
void
blk2(void)
{
char ch;
ch = ‘b’;
…
}
Biến ch được khai báo hai lần, trong blk1() và blk2(). ch trong blk1() không
có liên quan đến ch trong blk2() bởi vì
mỗi ch chỉ được biết đến trong khối lệnh mà nó được khai báo.
Vì các biến cục bộ được tạo ra và huỷ đi trong một khối mà chúng
được khai báo, nên nội dung của chúng bị mất bên ngoài phạm vi của khối. Điều
này có nghĩa là chúng không thể duy trì
giá trị của chúng giữa các lần gọi hàm.
Từ khóa auto có thể được dùng để khai báo các biến cục bộ, nhưng thường
nó không được dùng vì mặc nhiên các biến
không toàn cục được xem như là biến cục bộ.
Các biến cục bộ được sử dụng bởi các hàm thường được khai báo ngay
sau dấu ngoặc mở ‘{‘ của hàm và trước tất cả các câu lệnh. Tuy nhiên, các khai báo có
thể ở bên trong một khối của một hàm. Ví dụ:
void blk1(void)
{
int t;
t = 1;
if(t > 5)
{
char ch;
…
}
}
Trong ví dụ trên biến ch được tạo ra và chỉ hợp lệ bên trong khối mã lệnh if.
Nó không thể được tham chiếu đến trong một phần khác của hàm blk1().
Một trong những thuận lợi của sự khai báo một biến theo cách này
đó là bộ nhớ sẽ chỉ được cấp phát cho nó khi nếu điều kiện để đi vào khối
lệnh if được thoả. Điều này là bởi vì các biến cục bộ chỉ được
khai báo khi đi vào khối lệnh mà các biến được định nghĩa trong đó.
Chú ý: Điều quan trọng cần nhớ là tất cả các biến cục bộ phải
được khai báo tại điểm bắt đầu của khối mà trong đó chúng được định nghĩa,
và trước tất cả các câu lệnh thực thi.
15.6.2 Tham số hình
thức
Một hàm sử dụng các đối số phải khai báo các biến để nhận các giá
trị của các đối số. Các biến này được gọi là tham số hình thức của
hàm và hoạt động giống như bất kỳ một biến cục bộ bên trong hàm.
Các biến này được khai báo bên trong cặp dấu ngoặc ()
theo sau tên hàm. Xem ví
dụ sau:
blk1(char ch, int i)
{
if(i > 5)
ch = ‘a’;
else
i = i +1;
return;
}
Hàm blk1() có hai tham số: ch và i.
Các tham số hình thức phải được khai báo cùng với kiểu của chúng.
Như trong ví dụ trên, ch có kiều char và i có kiểu int. Các biến này có thể được sử dụng bên trong hàm như các
biến cục bộ bình thường. Chúng bị huỷ đi khi ra khỏi hàm. Cần chú ý là các tham
số hình thức đã khai báo có cùng kiểu dữ liệu với các đối số được sử dụng khi
gọi hàm. Trong trường hợp có sai, C
có thể không hiển thị lỗi nhưng có thể đưa ra một kết quả không mong muốn. Điều
này là vì, C vẫn đưa ra một vài kết
quả trong các tình huống khác thường. Người lập trình phải đảm bảo rằng không
có các lỗi về sai kiểu.
Cũng giống như với các biến cục bộ, các phép gán cũng có thể được
thực hiện với tham số hình thức của hàm và chúng cũng có thể được sử dụng bất
kỳ biểu thức nào mà C cho phép.
15.6.3 Biến toàn cục
Các biến toàn cục là biến được thấy bởi toàn bộ chương trình, và
có thể được sử dụng bởi một mã lệnh bất kỳ. Chúng được khai báo bên ngoài các
hàm của chương trình và lưu giá trị của chúng trong suốt sự thực thi của chương
trình. Các biến này có thể được khai báo bên ngoài main() hoặc
khai báo bất kỳ nơi đâu trước lần sử dụng đầu tiên. Tuy nhiên, nơi tốt nhất để
khai báo các biến toàn cục là tại đầu chương trình, nghĩa là trước hàm main().
int
ctr;
/* ctr is global */
void
blk1(void);
void
blk2(void);
void
main(void)
{
ctr = 10;
blk1 ();
…
}
void
blk1(void)
{
int rtc;
if (ctr > 8)
{
rtc = rtc + 1;
blk2();
}
}
void
blk2(void)
{
int ctr;
ctr = 0;
}
Trong đoạn mã lệnh trên, ctr là một biến toàn cục
và được khai báo bên ngoài hàm main() và blk1(), nó có
thể được tham chiếu đến trong các hàm. Biến ctr trong blk2(),
là một biến cục bộ và không có liên quan với biến toàn cục ctr. Nếu một biến toàn cục và cục bộ có cùng tên:
tất cả các tham chiếu đến tên đó bên trong khối chứa định nghĩa biến cục bộ sẽ
được kết hợp với biến cục bộ mà
không phải là biến toàn cục.
Các biến toàn cục được lưu trữ trong các vùng cố định của bộ nhớ.
Các biến toàn cục hữu dụng khi nhiều hàm trong chương trình sử dụng cùng dữ
liệu. Tuy nhiên, nên tránh sử dụng biến toàn cục nếu không cần thiết, vì
chúng giữ bộ nhớ trong suốt thời gian thực hiện chương trình. Vì vậy việc sử
dụng một biến toàn cục ở nơi mà một biến cục bộ có khả năng đáp ứng cho hàm sử
dụng là không hiệu quả. Ví dụ sau sẽ giúp làm rõ hơn điều này:
void
addgen(int i, int j)
{
return(i + j);
}
int
i, j;
void
addspe(void)
{
return(i + j);
}
Cả hai hàm addgen() và addspe() đều
trả về tổng của các biến i và j. Tuy nhiên,
hàm addgen() được sử dụng để trả về tổng của hai số bất kỳ;
trong khi hàm addspe() chỉ trả về tổng của các biến toàn
cục i và j.
15.7 Lớp lưu trữ
(Storage Class)
Mỗi biến trong C có một
đặc trưng được gọi là lớp lưu trữ. Lớp lưu trữ xác định hai
khía cạnh của biến: thời gian sống của biến và phạm
vi của biến. Thời gian sống của một biến là thời gian
mà giá trị của biến tồn tại. Phạm vi của một biến xác định
các phần của một chương trình sẽ có thể nhận ra biến. Một biến có thể xuất hiện
trong một khối, một hàm, một tập tin, một nhóm các tập tin, hoặc toàn bộ chương
trình
Theo cách nhìn của trình biên dịch C, một tên biến xác định một vài vị trí vật lý bên trong máy tính,
ở đó một chuỗi các bit biểu diễn giá trị được lưu trữ của biến. Có hai loại vị
trí trong máy tính mà ở đó giá trị của biến có thể được lưu trữ: bộ nhớ hoặc
thanh ghi CPU. Lớp lưu trữ của biến
xác định vị trí biến được lưu trữ là trong bộ nhớ hay trong một thanh ghi. C có
bốn lớp lưu trữ. Đó là:
Ø auto
Ø external
Ø static
Ø register
Đó là các từ khoá. Cú pháp tổng quát cho khai báo biến như sau:
storage_specifier
type var_name;
15.7.1 Biến tự động (auto)
Biến tự động thật ra là biến cục bộ mà chúng ta đã nói ở trên.
Phạm vi của một biến tự động có thể nhỏ hơn hàm, nếu nó được khai báo bên trong
một câu lệnh ghép: phạm vi của nó bị giới hạn trong câu lệnh ghép đó. Chúng có
thể được khai báo bằng từ khóa auto,
nhưng sự khai báo này là không cần thiết. Bất kỳ một biến được khai báo bên
trong một hàm hoặc một khối lệnh thì mặc nhiên là thuộc lớp auto và hệ thống cung cấp vùng bộ nhớ được yêu
cầu cho biến đó.
15.7.2 Biến ngoại (extern)
Trong C, một chương
trình lớn có thể được chia thành các
module nhỏ hơn, các module này có thể được biên dịch riêng lẻ và được liên
kết lại với nhau. Điều này được thực hiện nhằm tăng tốc độ quá trình biên dịch
các chương trình lớn.
Tuy nhiên, khi các module được liên kết,
các tập tin phải được chương trình thông báo cho biết về các biến toàn cục được
yêu cầu. Một biến toàn cục chỉ có thể
được khai báo một lần. Nếu hai biến toàn cục có cùng tên được khai báo
trong cùng một tập tin, một thông điệp lỗi ‘duplicate variable name’
(tên biến trùng) có thể được hiển thị hoặc đơn giản trình biên dịch C chọn một biến khác. Một lỗi tương tự
xảy ra nếu tất cả các biến toàn cục được yêu cầu bởi chương trình chứa trong
mỗi tập tin.
Mặc dù trình biên dịch không đưa ra bất kỳ
một thông báo lỗi nào trong khi biên dịch, nhưng sự thật các bản sao của cùng một biến đang được tạo
ra. Tại thời điểm liên kết các tập tin, bộ liên kết sẽ hiển thị một thông báo
lỗi như sau ‘duplicate label’ (nhãn trùng nhau) vì nó không biết sử dụng biến nào.
Lớp extern được
dùng trong trường hợp này. Tất cả các biến toàn cục được khai báo trong một tập
tin và các biến giống nhau được khai báo là biến ngoại, trong tất cả các tập tin.
Xem đoạn mã lệnh sau:
File1 File2
int
i,j;
extern int i,j;
char
a;
extern char a;
main()
xyz()
{
{
…
i
= j * 5
… …
}
}
abc()
pqr()
{
{
i =
123;
j
= 50;
… …
}
}
File2 có
các biến toàn cục giống như File1, ngoại trừ một điểm là các
biến này có từ khóa extern được thêm vào sự khai báo của chúng.
Từ khóa này nói với trình biên dịch là tên và kiểu của biến toàn cục được sử
dụng mà không cần phải tạo lại sự lưu
trữ cho chúng. Khi hai module được liên kết, các tham chiếu đến các biến
ngoại được giải quyết.
Nếu một biến không được khai báo trong một hàm, trình biên dịch sẽ
kiểm tra nó có so khớp với bất kỳ biến toàn cục nào không. Nếu khớp với một
biến toàn cục, thì trình biên dịch sẽ xem như một biến toàn cục đang được tham
chiếu đến.
15.7.3 Biến tĩnh (static)
Các biến tĩnh là
các biến cố định bên trong các hàm
và các tập tin. Không giống như các biến toàn cục, chúng không được biết đến
bên ngoài hàm hoặc tập tin của chúng, nhưng chúng giữ được giá trị của chúng giữa
các lần gọi. Điều này có nghĩa là, nếu một hàm kết thúc và sau đó được gọi
lại, các biến tĩnh đã định nghĩa trong hàm đó vẫn giữ được giá trị của chúng.
Sự khai báo biến tĩnh được bắt đầu với từ khóa static.
Có thể định nghĩa các biến tĩnh có cùng tên như hướng dẫn với
các biến ngoại. Các biến cục bộ
(biến tĩnh cũng như biến động) có độ ưu tiên cao hơn các biến ngoại và giá trị
của các biến ngoại sẽ không ảnh hưởng bởi bất kỳ sự thay đổi nào các biến cục
bộ. Các biến ngoại có cùng tên với các biến nội trong một hàm không thể được
truy xuất trực tiếp bên trong hàm đó.
Các giá trị khởi tạo có thể
được gán cho các biến trong sự khai báo các biến tĩnh, nhưng các giá trị này phải là các hằng hoặc các biểu
thức. Trình biên dịch tự động gán một giá trị mặc nhiên 0 đến các biến tĩnh không được khởi tạo. Sự khởi
tạo thực hiện ở đầu chương trình.
Xem hai chương trình sau. Sự khác nhau giữa 2 biến cục bộ: tự động
và tĩnh sẽ được làm rõ.
Ví dụ về biến
tự động:
#include <stdio.h>
#include <conio.h>
void incre();
main()
{
incre();
incre();
incre();
getch();
}
void incre()
{
char var =
65; /* var is automatic
variable*/
printf("\nThe character stored
in var is %c", var++);
}
Kết quả của chương trình
trên sẽ là:
Ví dụ về biến
tĩnh:
#include<stdio.h>
#include <conio.h>
void incre();
main()
{
incre();
incre();
incre();
getch();
}
void incre()
{
static char var = 65; /* var is
static variable */
printf("\nThe character stored in
var is %c", var++);
}
Kết quả của chương trình
trên sẽ là:
Cả hai chương trình gọi incre() ba lần. Trong chương
trình thứ nhất, mỗi lần incre() được gọi, biến var với
lớp lưu trữ auto (lớp lưu trữ mặc định) được khởi tạo
lại là 65 (là mã ASCII tương ứng của ký tự A). Vì vậy khi kết thúc hàm, giá trị
mới của var (66) bị mất đi (ASCII ứng với ký tự B).
Trong chương trình thứ hai, var là của lớp lưu
trữ static. Ở đây var được khởi tạo
là 65 chỉ một lần duy nhất khi biên dịch chương trình. Cuối lần gọi hàm đầu
tiên, var có giá trị 66 (ASCII B) và tương tự ở lần gọi kế
tiếp var có giá trị 67 (ASCII C). Sau lần gọi hàm cuối
cùng, var được tăng giá trị theo sự thi hành của lệnh printf().
Giá trị này bị mất khi chương trình kết thúc.
15.7.4 Biến thanh ghi (register)
Các máy tính có các thanh
ghi trong bộ số học logic - Arithmetic Logic Unit (ALU), các thanh ghi này
được sử dụng để tạm thời lưu trữ dữ liệu được truy xuất thường xuyên. Kết quả tức thời của phép tính toán cũng
được lưu vào các thanh ghi. Các thao tác thực hiện trên dữ liệu lưu trữ trong
các thanh ghi thì nhanh hơn dữ liệu
trong bộ nhớ. Trong ngôn ngữ assembly (hợp ngữ), người lập trình phải truy xuất
đến các thanh ghi này và sử dụng chúng để giúp chương trình chạy nhanh hơn. Các
ngôn ngữ lập trình bậc cao thường không truy xuất đến các thanh ghi của máy
tính. Trong C, việc lựa chọn vị trí
lưu trữ cho một giá trị tùy thuộc vào
người lập trình.
Nếu một giá trị đặc biệt được dùng thường xuyên (ví dụ giá trị điều
khiển của một vòng lặp), lớp lưu trữ của nó có thể khai báo là register.
Sau đó nếu trình biên dịch tìm thấy một
thanh ghi còn trống, và các thanh ghi của máy tính đủ lớn để chứa biến, biến sẽ
được đặt vào thanh ghi đó. Ngược lại, trình biên dịch sẽ xem các biến thanh ghi
như các biến động khác, nghĩa là lưu trữ chúng trong bộ nhớ. Từ khóa register được dùng khi định nghĩa các biến thanh ghi.
Phạm vi và sự
khởi tạo của các biến thanh ghi
giống như các biến động, ngoại trừ vị trí lưu trữ. Các biến thanh ghi là
cục bộ trong một hàm. Nghĩa là, chúng tồn tại khi hàm được gọi và giá trị bị
mất đi một khi thoát khỏi hàm. Sự khởi tạo các biến này được thực hiện bởi
người lập trình.
Vì số lượng các thanh ghi
là có hạn, lập trình viên cần xác định các biến nào trong chương trình được
sử dụng thường xuyên để khai báo
chúng là các biến thanh ghi.
Sự hữu dụng của các biến thanh ghi thay đổi từ máy này đến
một máy khác và từ một trình biên dịch C
này đến một trình biên dịch khác. Đôi khi các biến thanh ghi không được hỗ trợ
bởi tất cả – từ khóa register vẫn được chấp nhận nhưng
được xem giống như là từ khóa auto. Trong các trường hợp khác, nếu
biến thanh ghi được hỗ trợ và nếu lập trình viên sử dụng chúng một cách hợp lý,
chương trình sẽ được thực thi nhanh hơn
gấp đôi.
Các biến thanh ghi được khai báo như bên dưới:
register int x;
register
char c;
Sự khai báo thanh ghi chỉ có thể gắn vào các biến động và tham số
hình thức. Trong trường hợp sau, sự khai báo sẽ giống như sau:
f(c,n)
register
int c, n;
{
register int i;
...
}
Xét một ví dụ sau, ở đó chương trình hiển thị các số có tổng
lập phương các số thành phần bằng chính nó. Ví dụ 370 là một số như vậy, vì:
3^3 + 7^3 +
0^3 = 27 + 343 + 0 = 370
Chương trình sau in ra các con số như vậy trong khoảng 1 đến 999:
#include <stdio.h>
#include <conio.h>
main()
{
register int i;
int no, digit, sum;
printf("\nThe numbers whose Sum of
Cubes of Digits \nis Equal to the number itself are:\n\n");
for(i = 1; i < 999; i++)
{
sum = 0;
no = i;
while(no)
{
digit =
no%10;
no =
no/10;
sum = sum
+ digit * digit * digit;
}
if (sum == i)
printf("\t%d\n\n", i);
}
getch();
}
Kết quả của chương trình
trên như sau:
Trong chương trình trên, giá trị của i , thay đổi
từ 1 đến 999. Với mỗi giá trị này, lập phương của từng con số riêng lẻ được
cộng và kết quả tổng được so sánh với i. Nếu hai giá trị này là
bằng nhau, i được hiển thị. Vì i được sử dụng
để điều khiển sự lặp, (phần chính của chương trình), nó được khai báo là của
lớp lưu trữ thanh ghi. Sự khai báo này làm tăng hiệu quả của chương trình.
15.8 Các qui luật về
phạm vi của một hàm
Qui luật về phạm vi là những qui luật quyết định một đoạn mã lệnh
có thể truy xuất đến một đoạn mã lệnh khác hoặc dữ liệu hay không. Trong C, mỗi hàm của chương trình là các khối
lệnh riêng lẻ. Mã lệnh bên trong một hàm là cục bộ với hàm đó và không thể được
truy xuất bởi bất kỳ lệnh nào ở ngoài hàm, ngoại trừ lời gọi hàm. Mã lệnh bên
trong một hàm là ẩn đối với phần còn lại của chương trình, và trừ khi nó sử
dụng biến hoặc dữ liệu toàn cục, nó có thể tác động hoặc bị tác động bởi các
phần khác của chương trình. Để rõ hơn, mã lệnh và dữ liệu được định nghĩa bên
trong một hàm không thể tương tác với mã lệnh hay dữ liệu được định nghĩa trong
hàm khác bởi vì hai hàm có phạm vi khác nhau.
Trong C, tất cả các hàm
có cùng mức phạm vi. Nghĩa là, một hàm không thể được định nghĩa bên trong một hàm
khác. Chính vì lý do này mà C không phải là một ngôn ngữ cấu trúc khối về mặt
kỹ thuật.
15.9 Gọi hàm
Một cách tổng quát, các hàm giao tiếp với nhau bằng cách truyền
tham số. Các tham số được truyền theo một trong hai cách sau:
Ø Truyền
bằng giá trị.
Ø Truyền
bằng tham chiếu.
15.9.1 Truyền bằng giá trị
Mặc nhiên trong C, tất
cả các đối số của hàm được truyền bằng giá trị. Điều này có nghĩa là, khi các
đối số được truyền đến hàm được gọi, các giá trị được truyền thông qua các biến tạm. Mọi sự thao tác chỉ được thực
hiện trên các biến tạm này. Hàm được
gọi không thể thay đổi giá trị của
chúng. Xem ví dụ sau:
#include <stdio.h>
#include <conio.h>
int adder(int a, int b);
main()
{
int a, b, c;
a = b = c = 0;
printf("\nEnter 1st integer: ");
scanf("%d", &a);
printf("\nEnter 2nd integer: ");
scanf("%d", &b);
c = adder(a,b);
printf("\n\na & b in main() are:
%d, % d", a, b);
printf("\n\nc in main() is: %d", c);
/*
c gives the addition of a and b */
getch();
}
int adder(int
a, int b)
{
int c;
c = a + b;
a *= a;
b += 5;
printf("\n\na & b within adder function
are: %d, %d ", a, b);
printf("\nc within adder function is : %d",c);
return(c);
}
Ví dụ về kết quả thực thi
khi nhập vào 2 và 4:
Chương trình trên nhận hai số nguyên, hai số này được truyền đến
hàm adder(). Hàm adder() thực hiện như sau: nó nhận hai
số nguyên như là các đối số của nó, cộng chúng lại, tính bình phương cho số
nguyên thứ nhất, và cộng 5 vào số nguyên thứ hai, in kết quả và trả về tổng của
các đối số thực. Các biến được sử dụng trong hàm main() và adder()
có cùng tên. Tuy nhiên, không có gì là
chung giữa chúng. Chúng được lưu trữ trong các vị trí bộ nhớ khác nhau.
Điều này được thấy rõ từ kết quả của chương trình trên. Các biến a và b trong
hàm adder() được thay đổi từ 2 và 4 thành 4 và 9. Tuy nhiên, sự thay
đổi này không ảnh hưởng đến các giá trị của a và b trong hàm main().
Các biến được lưu ở những vị trí bộ nhớ khác nhau. Biến c trong main()
thì khác với biến c trong adder().
Vì vậy, các đối số được gọi là truyền bằng giá trị khi
giá trị của các biến được truyền đến hàm được gọi và bất kỳ sự thay đổi nào
trên giá trị này cũng không ảnh hưởng
đến giá trị gốc của biến đã truyền.
15.9.2 Truyền bằng tham
chiếu
Khi các đối số được truyền bằng giá trị, các giá trị của đối số
của hàm đang gọi không bị thay đổi. Tuy nhiên, có thể có trường hợp, ở đó giá
trị của các đối số phải được thay đổi. Trong những trường hợp như vậy, truyền
bằng tham chiếu được dùng. Truyền bằng tham chiếu, hàm
được phép truy xuất đến vùng bộ nhớ thực của các đối số và vì vậy có thể thay
đổi giá trị của các đối số của hàm gọi.
Ví dụ, xét một hàm, hàm này nhận hai đối số, hoán vị giá trị của
chúng và trả về các giá trị của chúng. Nếu một chương trình giống như chương
trình dưới đây được viết để giải quyết mục đích này, thì sẽ không bao giờ thực hiện được.
#include <stdio.h>
#include <conio.h>
void swap(int u, int v);
main()
{
int x, y;
x = 15; y = 20;
printf("\nx = %d, y = %d\n",
x, y);
swap(x, y);
printf("\nAfter
interchanging x = %d, y = %d\n", x, y);
getch();
}
void swap(int
u, int v)
{
int temp;
temp = u;
u = v;
v = temp;
return;
}
Kết quả của chương trình
trên như sau:
Hàm swap() hoán vị các giá trị của u và v, nhưng các
giá trị này không được truyền trở về hàm main(). Điều này là bởi vì
các biến u và v trong swap() là
khác với các biến u và v được dùng
trong main(). Truyền bằng tham chiếu có thể được sử dụng trong
trường hợp này để đạt được kết quả mong muốn, bởi vì nó sẽ thay đổi các giá trị
của các đối số thực. Các con trỏ được
dùng khi thực hiện truyền bằng tham chiếu.
Các con trỏ được truyền đến một hàm như là các đối số để cho phép
hàm được gọi của chương trình truy xuất các biến mà phạm vi của nó không
vượt ra khỏi hàm gọi. Khi một con trỏ được truyền đến một hàm, địa chỉ của dữ
liệu được truyền đến hàm nên hàm có thể tự do truy xuất nội dung của địa chỉ
đó. Các hàm gọi nhận ra bất kỳ thay đổi trong nội dung của địa chỉ. Theo cách
này, đối số hàm cho phép dữ liệu được thay đổi trong hàm gọi, cho phép truyền
dữ liệu hai chiều giữa hàm gọi và hàm được gọi. Khi các đối số của hàm là các
con trỏ hoặc mảng, truyền bằng tham chiếu được tạo ra đối nghịch với cách
truyền bằng giá trị.
Các đối số hình thức của một hàm là các con trỏ thì phải có một
dấu ‘*‘ phía
trước, giống như sự khai báo biến con trỏ, để xác định chúng là các con trỏ.
Các đối số thực kiểu con trỏ trong lời gọi hàm có thể được khai báo là một biến
con trỏ hoặc một biến được tham chiếu đến (&var).
Ví dụ, định nghĩa hàm:
getstr(char *ptr_str, int *ptr_int)
đối số ptr_str trỏ đến kiểu char và ptr_int trỏ đến
kiểu int. Hàm có thể được
gọi bằng câu lệnh:
getstr(pstr, &var)
ở đó pstr được khai báo là một con trỏ và địa chỉ
của biến var được truyền. Gán giá trị thông qua:
*ptr_int = var;
Hàm bây giờ có thể gán các giá trị đến biến var trong
hàm gọi, cho phép truyền theo hai chiều đến và từ hàm.
char
*pstr;
Quan sát ví dụ sau của hàm swap(). Bài toán này sẽ
giải quyết được khi con trỏ được truyền thay vì dùng biến.
Mã lệnh tương tự như sau:
#include <stdio.h>
#include <conio.h>
void swap(int *u, int *v);
main()
{
int x, y, *px, *py;
//Storing address of x in px
px
= &x;
//Storing address of y in py
py = &y;
x = 15; y = 20;
printf("\nx = %d, y = %d
\n", x, y);
swap (px, py);
//Passing addresses of x and y
printf("\n After interchanging x
= %d, y = %d\n", x, y);
getch();
}
void swap(int
*u, int *v)
//Accept
the values of px and py into u and v
{
int temp;
temp = *u;
*u = *v;
*v = temp;
return;
}
Kết quả của chương trình
trên như sau:
Hai biến kiểu con trỏ px và py được
khai báo, và địa chỉ của biến x và y được gán
đến chúng. Sau đó các biến con trỏ được truyền đến hàm swap(), hàm
này hoán vị các giá trị lưu trong x và y thông qua các con trỏ.
15.10 Sự lồng nhau của lời gọi hàm
Lời gọi một hàm từ một hàm khác được gọi là sự lồng
nhau của lời gọi hàm. Một chương trình kiểm tra một chuỗi
có phải là chuỗi đọc xuôi - đọc ngược như nhau hay không, là một ví dụ cho các
lời gọi hàm lồng nhau. Từ đọc xuôi - ngược giống nhau là một chuỗi các ký tự
đối xứng. Xem đoạn mã lệnh theo sau đây:
main()
{
…
palindrome();
…
}
palindrome()
{
…
getstr();
reverse();
cmp();
…
}
Trong chương trình trên, hàm main() gọi hàm palindrome().
Hàm palindrome() gọi đến ba hàm khác getstr(), reverse() và cmp().
Hàm getstr() để nhận một chuỗi ký tự từ người dùng, hàm reverse() đảo
ngược chuỗi và hàm cmp() so sánh chuỗi được nhập vào và chuỗi
đã được đảo.
Vì main() gọi palindrome(), hàm palindrome()
lần lượt gọi các hàm getstr(), reverse() và cmp(),
các lời gọi hàm này được gọi là được lồng bên trong palindrome().
Sự lồng nhau của các lời gọi hàm như trên là được phép, trong khi
định nghĩa một hàm bên trong một hàm khác là không được chấp nhận trong C.
15.11 Hàm trong chương trình nhiều tập tin
Các chương trình có thể được tạo bởi nhiều tập tin. Những chương
trình như vậy được tạo bởi các hàm lớn, ở đó mỗi hàm có thể chiếm một tập tin.
Cũng như các biến trong các chương trình nhiều tập tin, các hàm cũng có thể
được định nghĩa là static hoặc extern.
Phạm vi của hàm extern có thể được sử dụng trong tất cả các tập tin của chương
trình, và đó là cách lưu trữ mặc định cho các tập tin. Các hàm static chỉ được nhận biết bên trong tập tin
chương trình và phạm vi của nó không vượt khỏi tập tin chương trình. Phần tiêu
đề (header) của hàm như sau:
static fn _type fn_name (argument list)
hoặc
extern fn_type fn_name (argument list)
Từ khóa extern là một tuỳ chọn (không bắt buộc) vì
nó là lớp lưu trữ mặc định.
15.12 Con trỏ đến hàm
Một đặc tính mạnh mẽ của C
vẫn chưa được đề cập, chính là con trỏ hàm. Dù rằng một hàm không
phải là một biến, nhưng nó có địa chỉ vật lý trong bộ nhớ nơi có thể gán cho
một con trỏ. Một địa chỉ hàm là điểm đi
vào của hàm và con trỏ hàm có thể được sử dụng để gọi hàm.
Để hiểu các con trỏ hàm làm việc như thế nào, thật sự cần phải
hiểu thật rõ một hàm được biên dịch và được gọi như thế nào trong C. Khi mỗi hàm được biên dịch, mã nguồn
được chuyển thành mã đối tượng và một điểm đi vào của hàm được thiết lập. Khi
một lời gọi được thực hiện đến một hàm, một lời gọi ngôn ngữ máy được thực hiện
để chuyển điều khiển đến điểm đi vào của hàm. Vì vậy, nếu một con trỏ chứa địa
chỉ của điểm đi vào của hàm, nó có thể được dùng để gọi hàm đó.
Địa chỉ của
một hàm có thể lấy được bằng cách sử dụng tên hàm không có dấu ngoặc () hay bất
kỳ đối số nào.
Tóm tắt bài học
Ø Trong C, các hàm được dùng để thực thi một
chuỗi các chỉ thị nhiều hơn một lần.
Ø type_specifier xác định kiểu dữ liệu của giá trị sẽ được
trả về bởi hàm.
Ø Các đối số tới hàm có thể là các hằng,
biến, biểu thức hay các hàm.
Ø Các đối số còn được gọi là các đối
số thực trong hàm gọi và đối số hình thức trong hàm
được gọi.
Ø Một hàm phải được khai báo trong hàm
main(), trước khi nó được định nghĩa hay sử dụng.
Ø Trong C, mặc định, tất cả các đối số của
hàm được truyền bằng giá trị.
Ø Có ba loại biến cơ bản: biến cục bộ, tham
số hình thức và biến toàn cục.
·
Biến
cục bộ được khai báo bên trong một hàm.
·
Tham
số hình thức được khai báo trong định nghĩa của tham số hàm.
·
Biến
toàn cục được khai báo bên ngoài tất cả các hàm.
Ø Lớp lưu trữ định nghĩa hai đặc tính của
biến; thời gian sống của biến và tầm nhìn hay phạm
vi.
Ø Các biến tự động giống như các biến cục
bộ.
Ø Tất cả các biến toàn cục được khai báo
trong một tập tin và giống với các biến được khai báo extern trong
tất cả các tập tin.
Ø Các biến static là các
biến cố định bên trong các hàm hoặc tập tin của chúng.
Ø Không giống các biến toàn cục, các biến tĩnh
không được nhận biết bên ngoài hàm hoặc tập tin của nó, nhưng chúng duy trì
được các giá trị của chúng giữa các lần gọi.
Ø Nếu một giá trị đặc biệt được sử dụng
thường xuyên, có thể dùng lớp lưu trữ register cho nó.
Ø Cũng giống như các biến trong các chương
trình có nhiều tập tin, các hàm cũng có thể được định nghĩa là static hay external.
Ø Mã lệnh và dữ liệu được định nghĩa bên
trong một hàm không thể tương tác với mã lệnh hay dữ liệu được định nghĩa trong
hàm khác bởi vì hai hàm có phạm vi khác nhau.
Ø Một hàm không thể được định nghĩa bên
trong một hàm khác.
Ø Một nguyên mẫu hàm là một sự khai báo hàm
để chỉ ra các kiểu dữ liệu của các đối số.
Ø Lời gọi một hàm từ bên trong một hàm khác
được gọi là sự của lời gọi hàm.
Ø Một con trỏ hàm có thể được dùng để gọi
một hàm.
Kiểm tra tiến độ học tập
1. Một_________
là một đoạn chương trình chứa chính nó và thực hiện một tác vụ cụ thể.
2. Các
đối số xuất hiện trong cặp dấu ngoặc () còn được gọi là _____________.
3. Nếu
không có lệnh return, điều khiển chuyển đến chương trình gọi khi gặp dấu ngoặc
đóng } của khối mã lệnh. Điều này được gọi là ____________.
4. Hàm
gọi đến một hàm khác có tên là ________ và hàm đang được gọi đến có tên
là ________.
5. Một
__________ là một sự khai báo hàm để chỉ ra kiểu dữ liệu của các đối số.
6. _________ chỉ
có thể được tham chiếu đến bởi các lệnh bên trong khối lệnh đã khai báo chúng.
7.
________ được nhìn thấy bởi toàn bộ chương trình, và có thể được sử dụng
ở bất kỳ mã lệnh nào.
8. _________
quyết định một đoạn mã lệnh có thể truy xuất đến một đoạn mã lệnh khác hoặc dữ
liệu hay không.
9. Các đối số
được gọi là truyền ________ khi giá trị của các biến được truyền đến hàm được
gọi
10. Trong_________,
hàm được phép truy xuất vị trí bộ nhớ thật của các đối số.
0 nhận xét:
Đăng nhận xét