Bài 8.2: Tham số và kiểu trả về của hàm trong C++

Bài 8.2: Tham số và kiểu trả về của hàm trong C++
Chào mừng các bạn trở lại với chuỗi bài học về C++! Sau khi đã làm quen với khái niệm cơ bản về hàm ở bài trước, hôm nay chúng ta sẽ đào sâu vào hai thành phần cực kỳ quan trọng, quyết định cách hàm hoạt động và tương tác với phần còn lại của chương trình: tham số (parameters) và kiểu trả về (return type).
Hãy hình dung một hàm như một "cỗ máy" chuyên biệt. Để cỗ máy đó hoạt động, chúng ta cần đưa nguyên liệu vào (đó chính là tham số). Sau khi xử lý xong, cỗ máy sẽ cho ra sản phẩm (đó là giá trị được trả về). Việc hiểu rõ cách đưa nguyên liệu vào và nhận sản phẩm ra là chìa khóa để sử dụng sức mạnh của hàm một cách hiệu quả nhất.
1. Tham số của hàm (Function Parameters)
Tham số là những giá trị mà bạn truyền vào một hàm khi gọi nó. Chúng cho phép hàm hoạt động với các dữ liệu khác nhau mỗi lần được gọi, làm cho hàm trở nên linh hoạt và tái sử dụng được.
Khi định nghĩa một hàm, bạn khai báo các tham số bên trong cặp dấu ngoặc đơn ()
, sau tên hàm. Mỗi tham số cần có kiểu dữ liệu và tên.
Cú pháp tổng quát:
kieu_tra_ve ten_ham(kieu_du_lieu1 ten_tham_so1, kieu_du_lieu2 ten_tham_so2, ...) {
// Nội dung hàm
}
Ví dụ:
int tinh_tong(int a, int b) { // 'a' và 'b' là các tham số
return a + b;
}
Trong C++, có ba cách chính để truyền tham số vào hàm, mỗi cách có mục đích và hiệu ứng khác nhau:
1.1. Truyền tham số theo giá trị (Pass by Value)
Đây là cách truyền tham số mặc định. Khi bạn truyền một biến theo giá trị, một bản sao của giá trị của biến đó được tạo ra và đưa vào hàm. Bất kỳ thay đổi nào được thực hiện trên tham số bên trong hàm sẽ không ảnh hưởng đến biến gốc bên ngoài hàm.
Ưu điểm: An toàn, hàm không thể vô tình thay đổi dữ liệu gốc. Nhược điểm: Tốn kém bộ nhớ và thời gian nếu truyền các đối tượng lớn (vì phải tạo bản sao).
Hãy xem ví dụ minh họa:
#include <iostream>
// Hàm nhận tham số theo giá trị
void tang_gia_tri(int so) {
cout << "Ben trong ham (truoc): " << so << endl;
so = so + 10; // Thay đổi giá trị của bản sao 'so'
cout << "Ben trong ham (sau): " << so << endl;
}
int main() {
int diem = 5;
cout << "Ben ngoai ham (truoc): " << diem << endl;
tang_gia_tri(diem); // Truyền 'diem' theo giá trị
cout << "Ben ngoai ham (sau): " << diem << endl; // diem vẫn là 5
return 0;
}
Giải thích code:
- Biến
diem
có giá trị ban đầu là 5. - Khi gọi
tang_gia_tri(diem)
, một bản sao của giá trị 5 được tạo ra và gán cho tham sốso
bên trong hàmtang_gia_tri
. - Hàm thay đổi giá trị của bản sao
so
lên 15. - Khi hàm kết thúc, bản sao
so
bị hủy. - Biến
diem
bên ngoài hàm hoàn toàn không bị ảnh hưởng và vẫn giữ nguyên giá trị 5.
1.2. Truyền tham số theo tham chiếu (Pass by Reference)
Khi truyền tham số theo tham chiếu, bạn không truyền một bản sao mà là một bí danh (alias) hoặc tên gọi khác cho biến gốc. Mọi thay đổi được thực hiện trên tham số bên trong hàm sẽ ảnh hưởng trực tiếp đến biến gốc bên ngoài hàm.
Để khai báo tham số theo tham chiếu, bạn sử dụng ký hiệu &
trước tên tham số.
Ưu điểm:
- Cho phép hàm thay đổi giá trị của biến gốc.
- Hiệu quả hơn khi truyền các đối tượng lớn vì không phải tạo bản sao.
- Tránh được các vấn đề với con trỏ (sẽ nói sau).
Nhược điểm: Hàm có thể vô tình thay đổi dữ liệu gốc nếu không cẩn thận.
Ví dụ minh họa:
#include <iostream>
// Hàm nhận tham số theo tham chiếu
void tang_gia_tri_ref(int &so) { // Sử dụng & để truyền theo tham chiếu
cout << "Ben trong ham (truoc): " << so << endl;
so = so + 10; // Thay đổi giá trị của biến gốc 'so'
cout << "Ben trong ham (sau): " << so << endl;
}
int main() {
int diem = 5;
cout << "Ben ngoai ham (truoc): " << diem << endl;
tang_gia_tri_ref(diem); // Truyền 'diem' theo tham chiếu
cout << "Ben ngoai ham (sau): " << diem << endl; // diem bây giờ là 15
return 0;
}
Giải thích code:
- Biến
diem
có giá trị ban đầu là 5. - Khi gọi
tang_gia_tri_ref(diem)
, tham sốso
bên trong hàm trở thành một bí danh cho biếndiem
bên ngoài. Chúng cùng tham chiếu đến một ô nhớ duy nhất. - Hàm thay đổi giá trị của
so = so + 10;
. Thực tế là nó đang thay đổi trực tiếp giá trị của biếndiem
bên ngoài lên 15. - Khi hàm kết thúc, biến
diem
giữ giá trị đã được thay đổi là 15.
1.3. Truyền tham số theo con trỏ (Pass by Pointer)
Truyền tham số theo con trỏ là cách truyền địa chỉ bộ nhớ của biến gốc vào hàm. Để truy cập hoặc thay đổi giá trị tại địa chỉ đó, bạn cần sử dụng toán tử giải tham chiếu (*
).
Để khai báo tham số theo con trỏ, bạn sử dụng ký hiệu *
trước tên tham số. Khi gọi hàm, bạn truyền địa chỉ của biến bằng toán tử &
.
Ưu điểm:
- Cho phép hàm thay đổi giá trị của biến gốc (tương tự tham chiếu).
- Có thể truyền con trỏ
nullptr
(hoặcNULL
trong C cũ) để chỉ ra rằng không có đối tượng nào được truyền. - Cần thiết trong một số trường hợp lập trình cấp thấp hoặc giao tiếp với C API.
Nhược điểm:
- Cú pháp phức tạp hơn (cần toán tử
*
và&
). - Nguy hiểm hơn (có thể truy cập vào vùng nhớ không hợp lệ nếu con trỏ sai hoặc là
nullptr
).
Ví dụ minh họa:
#include <iostream>
// Hàm nhận tham số theo con trỏ
void tang_gia_tri_ptr(int *so_ptr) { // Sử dụng * để truyền theo con trỏ
cout << "Ben trong ham (truoc - dia chi): " << so_ptr << endl;
cout << "Ben trong ham (truoc - gia tri): " << *so_ptr << endl; // Giải tham chiếu để lấy giá trị
*so_ptr = *so_ptr + 10; // Giải tham chiếu để thay đổi giá trị tại địa chỉ
cout << "Ben trong ham (sau - gia tri): " << *so_ptr << endl;
}
int main() {
int diem = 5;
cout << "Ben ngoai ham (truoc): " << diem << endl;
cout << "Dia chi cua diem: " << &diem << endl; // Lấy địa chỉ của diem
tang_gia_tri_ptr(&diem); // Truyền địa chỉ của 'diem' bằng toán tử &
cout << "Ben ngoai ham (sau): " << diem << endl; // diem bây giờ là 15
return 0;
}
Giải thích code:
- Biến
diem
có giá trị ban đầu là 5. - Khi gọi
tang_gia_tri_ptr(&diem)
, địa chỉ của biếndiem
được truyền vào hàm và gán cho con trỏso_ptr
. - Bên trong hàm,
so_ptr
giữ địa chỉ củadiem
. Để làm việc với giá trị tại địa chỉ đó (tức là giá trị củadiem
), chúng ta dùng toán tử giải tham chiếu*
. - Dòng
*so_ptr = *so_ptr + 10;
đọc giá trị tại địa chỉ màso_ptr
đang trỏ tới (là 5), cộng thêm 10 (thành 15), và ghi lại giá trị 15 đó vào chính địa chỉ màso_ptr
đang trỏ tới (tức là ô nhớ củadiem
). - Kết quả là biến
diem
bên ngoài hàm bị thay đổi thành 15.
Khi nào sử dụng Pass by Value, Reference, hoặc Pointer?
- Pass by Value: Khi bạn chỉ cần giá trị của biến và không muốn (hoặc không cần) hàm làm thay đổi biến gốc. Tốt cho các kiểu dữ liệu cơ bản nhỏ (int, float, bool...).
- Pass by Reference: Khi bạn muốn hàm có thể thay đổi giá trị của biến gốc hoặc khi bạn muốn truyền các đối tượng lớn một cách hiệu quả (tránh sao chép). Thường là lựa chọn ưu tiên hơn con trỏ trong C++ hiện đại khi cần thay đổi giá trị gốc. Sử dụng
const &
nếu bạn muốn truyền tham chiếu để hiệu quả nhưng không cho phép hàm thay đổi giá trị. - Pass by Pointer: Khi bạn cần làm việc với địa chỉ bộ nhớ, có thể cần con trỏ
nullptr
, hoặc khi giao tiếp với các thư viện C. Cần cẩn trọng hơn.
2. Kiểu trả về của hàm (Function Return Type)
Kiểu trả về là kiểu dữ liệu của giá trị mà hàm sẽ gửi lại cho phần chương trình đã gọi nó sau khi hoàn thành công việc.
Khi định nghĩa hàm, bạn khai báo kiểu trả về trước tên hàm.
Cú pháp:
kieu_tra_ve ten_ham(...) {
// Nội dung hàm
return gia_tri_co_cung_kieu_tra_ve; // Sử dụng từ khóa return
}
2.1. Trả về một giá trị cụ thể
Nếu hàm thực hiện một phép tính hoặc tạo ra một kết quả nào đó, nó có thể trả về giá trị đó cho nơi gọi hàm.
Ví dụ:
#include <iostream>
// Hàm trả về tổng của hai số nguyên
int tinh_tong(int a, int b) {
int ket_qua = a + b;
return ket_qua; // Trả về giá trị của 'ket_qua'
}
// Hàm trả về diện tích hình chữ nhật
double tinh_dien_tich_hcn(double dai, double rong) {
if (dai <= 0 || rong <= 0) {
// Xử lý lỗi hoặc trả về giá trị không hợp lệ
return -1.0; // Ví dụ: trả về -1.0 cho diện tích không hợp lệ
}
return dai * rong; // Trả về kết quả tính toán
}
int main() {
int so1 = 10, so2 = 20;
int tong = tinh_tong(so1, so2); // Gọi hàm và lưu giá trị trả về vào biến 'tong'
cout << "Tong cua " << so1 << " va " << so2 << " la: " << tong << endl;
double chieu_dai = 5.5, chieu_rong = 3.2;
double dien_tich = tinh_dien_tich_hcn(chieu_dai, chieu_rong); // Gọi hàm và lưu giá trị trả về
cout << "Dien tich HCN (" << chieu_dai << "x" << chieu_rong << ") la: " << dien_tich << endl;
double dien_tich_sai = tinh_dien_tich_hcn(-2.0, 5.0);
cout << "Dien tich HCN (-2.0x5.0) la: " << dien_tich_sai << endl; // In ra -1.0
return 0;
}
Giải thích code:
- Hàm
tinh_tong
được khai báo với kiểu trả về làint
. Nó tính tổng và sử dụngreturn ket_qua;
để gửi giá trịket_qua
(kiểuint
) trở lại nơi gọi hàm. - Trong
main
,int tong = tinh_tong(so1, so2);
gọi hàmtinh_tong
, nhận giá trị 30 được trả về, và gán 30 đó vào biếntong
. - Hàm
tinh_dien_tich_hcn
có kiểu trả về làdouble
. Nó kiểm tra đầu vào và trả vềdai * rong
hoặc-1.0
. - Trong
main
, giá trịdouble
được trả về từtinh_dien_tich_hcn
được lưu vào biếndien_tich
hoặcdien_tich_sai
.
Từ khóa return
không chỉ trả về giá trị mà còn kết thúc việc thực thi của hàm ngay lập tức. Mọi dòng lệnh sau return
trong cùng một khối lệnh sẽ không được thực thi.
2.2. Kiểu trả về void
Nếu một hàm thực hiện một tác vụ (như in ra màn hình, ghi vào file, v.v.) nhưng không cần trả về một giá trị cụ thể nào cho nơi gọi hàm, bạn có thể khai báo kiểu trả về là void
.
Ví dụ:
#include <iostream>
#include <string>
// Hàm không trả về giá trị nào
void chao_mung(string ten) {
cout << "Xin chao, " << ten << "!" << endl;
// Không có lệnh 'return' giá trị
}
int main() {
chao_mung("The Anh"); // Gọi hàm
chao_mung("Minh Ngoc"); // Gọi hàm
// Không thể gán kết quả của hàm void cho biến:
// int ket_qua = chao_mung("Test"); // Lỗi biên dịch!
return 0;
}
Giải thích code:
- Hàm
chao_mung
được khai báo với kiểu trả về làvoid
. - Nó thực hiện công việc in lời chào ra màn hình dựa vào tham số
ten
. - Hàm không trả về bất kỳ giá trị nào cho nơi gọi, nên không thể sử dụng kết quả của nó trong biểu thức hoặc gán cho biến.
2.3. Sử dụng return;
trong hàm void
Mặc dù hàm void
không trả về giá trị, bạn vẫn có thể sử dụng từ khóa return;
(không có giá trị sau nó) để thoát khỏi hàm sớm. Điều này hữu ích khi bạn muốn dừng thực thi hàm dựa trên một điều kiện nào đó.
Ví dụ:
#include <iostream>
void kiem_tra_tuoi(int tuoi) {
if (tuoi < 0) {
cout << "Tuoi khong the la so am!" << endl;
return; // Thoát khỏi hàm sớm nếu tuổi âm
}
if (tuoi < 18) {
cout << "Ban chua du tuoi de truy cap." << endl;
} else {
cout << "Chao mung ban!" << endl;
}
// Các dòng lệnh khác nếu có sẽ không được thực thi nếu đã return;
}
int main() {
kiem_tra_tuoi(25);
kiem_tra_tuoi(16);
kiem_tra_tuoi(-5); // Sẽ in ra thông báo lỗi và thoát hàm sớm
return 0;
}
Giải thích code:
- Hàm
kiem_tra_tuoi
nhận một tham sốtuoi
và có kiểu trả về làvoid
. - Nếu
tuoi
nhỏ hơn 0, một thông báo lỗi được in ra và lệnhreturn;
được thực thi. Điều này làm cho hàm dừng lại ngay lập tức, bỏ qua phần kiểm tra tuổi 18 bên dưới. - Nếu
tuoi
không âm, hàm tiếp tục kiểm tra điều kiệntuoi < 18
.
3. Kết hợp Tham số và Kiểu trả về
Trong thực tế, hầu hết các hàm hữu ích đều sử dụng cả tham số (nhận đầu vào) và kiểu trả về (xuất kết quả).
Ví dụ về một hàm tính bình phương của một số (nhận tham số, trả về kết quả):
#include <iostream>
// Hàm nhận một số (tham số) và trả về bình phương của nó (kiểu trả về)
int tinh_binh_phuong(int so) { // Tham số 'so' (truyền theo giá trị), kiểu trả về 'int'
return so * so;
}
int main() {
int gia_tri = 7;
int ket_qua_bp = tinh_binh_phuong(gia_tri); // Gọi hàm, truyền tham số và nhận kết quả trả về
cout << "Binh phuong cua " << gia_tri << " la: " << ket_qua_bp << endl;
return 0;
}
Giải thích code:
- Hàm
tinh_binh_phuong
nhận một tham sốint so
. - Nó tính
so * so
và trả về kết quả này. Vìso
làint
, kết quảso * so
cũng làint
, phù hợp với kiểu trả vềint
của hàm. - Trong
main
,tinh_binh_phuong(gia_tri)
được gọi. Giá trị 7 củagia_tri
được truyền vào hàm theo giá trị (một bản sao 7 được dùng). Hàm tính 7 * 7 = 49 và trả về giá trị 49. - Giá trị 49 được gán vào biến
ket_qua_bp
.
Comments