Giả sử thầy có câu lệnh sau:
if (x < 0) then a = -x;
else a = x;
Câu lệnh trên có nghĩa là gì? Nếu giá trị của x bé hơn 0, thì gán cho a giá trị đối của x; ngược lại, gán cho a giá trị của x. Nói cách khác, đó là câu lệnh gán cho a giá trị tuyệt đối của x. Câu lệnh đơn giản như thế, nhưng nếu phải viết đi viết lại rất nhiều lần thì đó là một vấn đề, có thể làm cho chương trình của chúng ta trở nên rườm rà và mất thời gian viết. Vì thế, chúng ta cần làm cho việc dùng nó đơn giản hơn, ngắn gọn hơn, và chúng ta có hàm abs() trong ngôn ngữ lập trình C++. Câu lệnh:
abs(x);
sẽ cho chúng ta giá trị tuyệt đối của x, với x là tham số của hàm. Đối với số nguyên, hàm abs() nằm trong thư viện <stdlib.h>, nên khi dùng các em cần khai báo thư viện này ở phần khai báo của chương trình.
Hàm abs() là một ví dụ về chương trình con. Những chương trình này, giúp việc viết, chỉnh sửa cũng như nâng cấp chương trình chính dễ dàng hơn và tốn ít thời gian hơn vì khi cần dùng môt chức năng, lập trình viên chỉ cần gọi tên chương trình con có chức năng tương ra thay vì phải viết lại hết tất cả dòng lệnh để thực hiện chức năng đó.
Vậy ở một chương trình con có các yếu tố cơ bản là: kiểu dữ liệu, tên chương trình, tham số của hàm và phần thân chương trình. Chương trình con sẽ thực hiện một chức năng nào đó phục vụ cho chương trình chính. Trong bài viết này, chúng ta sẽ tìm hiểu về cách khai báo và sử dụng chương trình con trong ngôn ngữ lập trình C++, đồng thời cũng cung cấp kiến thức về biến toàn cục và biến cục bộ.
Trong ngôn ngữ lập trình C++, các chương trình con (subroutine) được gọi là các hàm (function). Việc khai báo hàm được thực hiện theo cú pháp sau:
<kiểu dữ liệu trả về> <tên hàm> (<danh sách tham số>){
/*Phần thân chương trình*/
}
Trong đó:
Kiểu dữ liệu trả về là kiểu dữ liệu của kết quả mà hàm sẽ trả về (return). Nếu hàm không trả về kết quả thì có kiểu dữ liệu là void.
Tên hàm phải được khai báo đúng quy tắc và không trùng với các từ khóa. Các em nên đặt tên hàm sao cho khi đọc vào, chúng ta có thể đoán được chức năng của hàm.
Danh sách tham số có dạng: <kiểu dữ liệu tham số 1> <tên tham số 1>, <kiểu dữ liệu tham số 2> <tên tham số 2>,v.v... Tuy nhiên, một hàm có thể không có tham số. Trong trường hợp đó, danh sách tham số để trống.
Phần thân chương trình là tập hợp các câu lệnh tạo nên chức năng cho hàm. Ở cuối phần thân thường có câu lệnh:
return <giá trị trả về>;
Đối với các hàm void thì không có <giá trị trả về>.
Thầy sẽ lấy ví dụ với hàm abs() tự cài đặt đơn giản dành cho số nguyên:
int abs(int number){
int answer;
if (number < 0) answer = -number;
else answer = number;
return answer;
}
Nhìn vào chương trình trên, ta có thể xác định được:
Hàm trả về giá trị kiểu nguyên.
Tên hàm là abs(). Abs là viết tắt của Absolute Value (Giá trị tuyệt đối).
Hàm có 1 tham số kiểu nguyên tên là number.
Phần thân chương trình sẽ hoạt động như sau: Khai báo biến answer kiểu nguyên. Nếu giá trị của number nhỏ hơn 0; gán giá trị đối của number cho answer; ngược lại, gán giá trị của number cho answer. Trả về giá trị của biến answer.
Nếu các em đã thông thạo việc viết hàm thì các em có thể viết theo cấu trúc của mình. Trong phạm vi trường học, thầy chỉ giới thiệu cách việc dễ cho các em tiếp cận.
Khai báo hàm (function prototype hoặc function declaration) cho chương trình biên dịch biết tên hàm và cách gọi hàm. Phần thân của hàm có thể được định nghĩa ở một vị trí khác với phần khai báo hàm.
Phần khai báo hàm có dạng như sau:
<kiểu dữ liệu trả về> <tên hàm> (<danh sách tham số>);
Ví dụ với hàm abs() ở trên, thầy sẽ có phần khai báo hàm là:
int abs (int number);
Tuy nhiên, phần tên của tham số không bắt buộc, ta chỉ cần kiểu dữ liệu của tham số. Vì thế, phần khai báo trên có thể viết lại là:
int abs (int);
Chương trình biên dịch đọc từng dòng từ trên xuống dưới. Vì thế, nếu các em muốn gọi một hàm nào đó tại một dòng, thì hàm đó phải được khai báo trước dòng đó. Còn phần thân của hàm có thể viết cùng với phần khai báo hoặc viết sau đó.
Khi tạo hàm, các em đã định nghĩa về hàm, kiểu dữ liệu, tên, tham số và chức năng của hàm. Để sử dụng được hàm mà các em định nghĩa, các em cần gọi (call hoặc invoke) nó ra.
Khi một hàm được gọi, luồng điều khiển chương trình sẽ "nhảy" đến hàm đó. Hàm được gọi sẽ thực hiện chức năng của nó cho đến khi gặp câu lệnh return hoặc dấu đóng ngoặc nhọn (}) thì hàm sẽ kết thúc. Luồng điều khiển chương trình quay trở lại và tiếp tục thực hiện chương trình lúc đầu.
Để gọi một hàm, các em chỉ cần gõ tên hàm và điền đủ số lượng tham số. Nếu hàm có trả về giá trị, các em có thể lưu trữ giá trị đó. Thầy sẽ lấy ví dụ với hàm maximum() có chức năng trả về giá trị lớn nhất trong hai giá trị.
#include <iostream>
using namespace std;
//Định nghĩa hàm maximum()
int maximum(int so1, int so2){
//Khai báo biến cục bộ
int ketqua;
//Xử lí
if (so1 < so2) ketqua = so2;
else ketqua = so1;
//Trả về kết quả
return ketqua;
}
int main(){
//Khai báo biến cục bộ
int a, b, dapan;
//Nhập giá trị cho biến
cout << "Nhap gia tri cua a: ";
cin >> a;
cout << "Nhap gia tri cua b: ";
cin >> b;
//Gọi hàm maximum() để lấy giá trị lớn nhất
dapan = maximum(a, b);
cout << "Gia tri lon nhat la: " << dapan;
return 0;
}
Giả sử thầy nhập cho biến a và b lần lượt giá trị là 10 và 20, chương trình sau khi thực hiện sẽ cho kết quả ra màn hình như sau:
Gia tri lon nhat la: 20
Như đã phần trước:
Chương trình biên dịch đọc các dòng lệnh từ trên xuống dưới.
Phần khai báo hàm cho chương trình biên dịch biết tên hàm và cách gọi hàm.
Nếu muốn gọi một hàm nào đó, ta cần khai báo hàm đó trước; phần định nghĩa của hàm có thể viết sau.
Từ đó, ta có viết lại chương trình trên như sau:
#include <iostream>
using namespace std;
//Khai báo hàm maximum()
int maximum(int, int);
int main(){
//Khai báo biến cục bộ
int a, b, dapan;
//Nhập giá trị cho biến
cout << "Nhap gia tri cua a: ";
cin >> a;
cout << "Nhap gia tri cua b: ";
cin >> b;
//Gọi hàm maximum() để lấy giá trị lớn nhất
dapan = maximum(a, b);
cout << "Gia tri lon nhat la: " << dapan;
return 0;
}
//Định nghĩa hàm maximum()
int maxi(int so1, int so2){
//Khai báo biến cục bộ
int ketqua;
//Xử lí
if (so1 < so2) ketqua = so2;
else ketqua = so1;
//Trả về kết quả
return ketqua;
}
Để bắt đầu phần này, thầy sẽ đưa ra một bài toán:
Cho hai số nguyên dương có giá trị không quá 106 được nhập từ bàn phím. Hoán đổi giá trị của hai số sau đó in ra màn hình.
Đây là một vấn đề tuy đơn giản, nhưng quan trọng, đặc biệt là trong hầu hết thuật toán sắp xếp. Trước tiên, chúng ta phải định hình được cách hoán đổi giá trị. Giả sử có hai biến so1 và so2, thầy có một giải pháp đơn giản như sau:
int tmp;
tmp = so1;
so1 = so2;
so2 = tmp;
Với cách giải trên, ta sẽ dùng 1 biến trung gian là tmp (temporary - tạm thời) để thực hiện hoán đổi.
Bây giờ, từ cách giải trên, chúng ta sẽ hình thành 1 hàm gọi là hoandoi(). Các em sẽ cần trả lời các câu hỏi sau để có thể xây dựng một hàm chính xác nhất có thể:
Hàm này có trả về giá trị không? Nếu có thì kiểu dữ liệu của giá trị đó là gì?
Trong trường hợp này, hàm không trả về giá trị nào, nên kiểu dữ liệu trả về sẽ là void.
Hàm này cần bao nhiêu tham số? Các tham số đó có kiểu như thế nào?
Trong trường hợp này, ta có thể nhận thấy chức năng của hàm là hoán đổi giá trị của hai số, như vậy sẽ có ít nhất hai tham số và cũng từ đề bài ta xác định được đó là hai số nguyên, tức là tham số có kiểu int.
Bây giờ thầy sẽ bắt đầu định nghĩa hàm hoandoi() như sau:
void hoandoi(int so1, int so2){
//Khai báo biến cục bộ
int tmp;
//Xử lí
tmp = so1;
so1 = so2;
so2 = tmp;
return;
}
Hãy thử viết một chương trình hoàn chỉnh và dùng hàm hoandoi() này nhé, các em có thể chỉnh sửa cho phù hợp "phong cách" của bản thân. Sau đây là một chương trình hoàn chỉnh thầy đã viết:
#include <iostream>
using namespace std;
void hoandoi(int so1, int so2){
//Khai báo biến cục bộ
int tmp;
//Xử lí
tmp = so1;
so1 = so2;
so2 = tmp;
return;
}
int main(){
//Khai báo biến cục bộ
int a, b;
//Nhập giá trị cho biến
cout << "Nhap gia tri cua so thu nhat: ";
cin >> a;
cout << "Nhap gia tri cua so thu hai: ";
cin >> b;
//Gọi hàm swap() để thực hiện hoán đổi
hoandoi(a, b);
//In ra kết quả
cout << "Gia tri cua so thu nhat sau khi hoan doi: " << a << endl;
cout << "Gia tri cua so thu hai sau khi hoan doi: " << b;
return 0;
}
Các em đã viết xong chưa? Nếu rồi thì chúng ta cùng thử nhé. Thầy sẽ nhập giá trị cho biến a và b lần lượt là 10 và 99. Sau khi chạy chương trình, thầy nhận được kết quả là:
Gia tri cua so thu nhat sau khi hoan doi: 10
Gia tri cua so thu hai sai khi hoan doi: 99
Có gì đó "sai sai" nhỉ? Sai lầm chí mạng ở đây là do chúng ta dùng phương pháp truyền tham trị.
Như các em có thể thấy ở trên, nếu dùng truyền tham trị, cú pháp khai báo tham số sẽ là:
<kiểu dữ liệu tham số> <tên tham số>
Đây là phương pháp cơ bản trong việc truyền tham số của hàm. Trong ví dụ trên, ta có:
hoandoi(a, b);
với a và b gọi là đối số. Phương pháp này sẽ sao chép giá trị thực của đối số và gán cho tham số của hàm, được gọi là tham số hình thức (formal parameter). Tham số hình thức tức là biến lưu trữ giá trị của đối số, biến này được tạo ra khi chương trình con được thực hiện và bị xóa bỏ khi chương trình con kết thúc.
Như vậy, giá trị được truyền vào hàm chỉ là giá trị của đối số. Việc hoán đổi giá trị của chúng ta chỉ diễn ra trong hàm, và hoán đổi giá trị của hai tham số hình thức chứ không ảnh hưởng gì đến chương trình chính. Do đó, việc hoán đổi diễn ra không như ta mong muốn.
Để có thể hoán đổi giá trị của biến a và b bằng hàm hoandoi(), ta cần dùng đến phương pháp truyền tham biến.
Cú pháp khai báo tham số của phương pháp này là:
<kiểu dữ liệu tham số> &<tên tham số>
Các em nhớ dấu & ở phía trước tên tham số.
Với phương pháp này, mọi sự thay đổi giá trị của một tham số trong hàm đều tác động lên các biến đối số tương ứng. Ví dụ thầy sửa lại hàm hoandoi() như sau:
void hoandoi(int &so1, int &so2){
//Khai báo biến cục bộ
int tmp;
//Xử lí
tmp = so1;
so1 = so2;
so2 = tmp;
return;
}
Và thử với giá trị cũ là 10 và 99, sau khi chạy chương trình, thầy nhận được kết quả là:
Gia tri cua so thu nhat sau khi hoan doi: 99
Gia tri cua so thu hai sai khi hoan doi: 10
Như vậy, chúng ta đã giải quyết được bài toán. Tuy nhiên, thầy có thêm môt số lưu ý cho các em:
Nếu dùng phương pháp truyền tham trị, kiểu dữ liệu của đối số và kiểu dữ liệu của tham số không bắt buộc tương đương nhau. Ví dụ: đối số a có kiểu char có thể truyền tham trị cho tham số so1 có kiểu int.
Nếu dùng phương pháp truyền tham biến, kiểu dữ liệu của đối số và kiểu dữ liệu của tham bắt buộc tương đương nhau. Ví dụ: đối số a có kiểu char không thể truyền tham biến cho tham số so1 có kiểu int.
Với phương pháp truyền con trỏ, cú pháp khai báo tham số sẽ là:
<kiểu dữ liệu tham số> *<tên tham số>
Phương pháp này chỉ nhắc đến cho các em biết vì để hiểu nó các em cần có những kiến thức về con trỏ. Ở mức độ giới thiệu, các em có thể hiểu phương pháp này truyền địa chỉ ô nhớ của biến đối số cho tham số hình thức của hàm.
Khu vực trong chương trình mà biến hoạt động gọi là phạm vi (scope). Nhìn chung, có ba khu vực mà biến có thể được khai báo:
Bên trong một hàm hoặc một khối lệnh. Các biến này được gọi là biến cục bộ (local variable).
Ở phần khai báo hàm. Các biến này, như đã biết, được gọi là tham số hình thức.
Bên ngoài hàm và phần khai báo hàm. Các biến này được gọi là biến toàn cục (global variable).
Các em đã biết về tham số hình thức ở trên. Trong phần này, thầy sẽ nói về biến cục bộ và biến toàn cục.
Biến cục bộ là các biến được khai báo trong một hàm hoặc một khối lệnh. Các biến này chỉ có thể sử dụng bên trong hàm hoặc khối lệnh đó. Nói cách khác, các biến đó không thể được truy cập bên ngoài hàm hoặc khối lệnh mà nó được khai báo. Đây là một ví dụ về biến cục bộ:
#include <iostream>
using namespace std;
int main(){
//Khai báo biến cục bộ
int a, b, c;
//Khởi tạo giá trị của biến cục bộ
a = 10;
b = 5;
c = a * b;
return 0;
}
Còn sau đây là môt ví dụ về việc dùng sai biến cục bộ:
#include <iostream>
using namespace std;
int main(){
{
//Khai báo biến cục bộ
int a, b;
//Khởi tạo giá trị
a = 10;
b = 5;
}
//Khai báo biến cục bộ và khởi tạo giá trị
int c = a*b;
return 0;
}
Biến a và b nằm trong một khối lệnh, có nghĩa chúng là biến cục bộ của khối lệnh đó. Biến c nằm ngoài khối lệnh cố truy cập vào hai biến a và b nhưng chương trình đã báo lỗi.
Biến toàn cục là các biến được khai báo bên ngoài các hàm, thường là ở đầu phần thân chương trình. Các biến này có thể truy cập bởi bất kì hàm nào. Nói cách khác, một biến toàn cục có thể được sử dụng ở bất kì đâu trong chương trình sau khi nó được khai báo. Sau đây là một ví dụ:
#include <iostream>
using namespace std;
//Khai báo biến toàn cục
int a, b, c;
int main(){
//Khởi tạo giá trị trong hàm
a = 10;
b = 5;
c = a*b;
return 0;
}
Và giá trị khởi tạo của một biến toàn cục gần như không thay đổi trong suốt chương trình, trừ khi chúng ta gán giá trị cho nó trong hàm, nhưng giá trị đó chỉ có nghĩa trong hàm đó.
Lưu ý: Nếu môt chương trình nếu có một biến toàn cục và môt biến cục bộ của một hàm nào đó có tên biến giống nhau, trong hàm đó, giá trị của biến cục bộ sẽ được ưu tiên. Ví dụ:
#include <iostream>
using namespace std;
//Khai báo biến toàn cục và khởi tạo giá trị
int a = 2, b = 5, c;
int main(){
//Khai báo biến cục bộ và khởi tạo giá trị
int a = 10, b = 5;
c = a*b;
cout << c;
return 0;
}
Chương trình trên sau khi thực hiện cho ta kết quả là:
50
Biến cục bộ khi được khai báo không có giá trị khởi tạo, nên các em cần khởi tạo giá trị cho chúng (trong bài "Cấu trúc lặp" thầy cũng đã có một lưu ý nho nhỏ về việc này). Biến toàn cục khi được khai báo sẽ có giá trị khởi tạo tùy vào kiểu dữ liệu của nó. Một số ví dụ như sau:
Các em cũng nên tập thói quen khởi tạo giá trị cho biến sau khi khai báo. Đây là một thói quen tốt và cần thiết để sau này không mắc phải những sai lầm chí mạng hay chương trình chạy không đúng mong muốn.
Người viết: Thái Dương Bảo Duy