Bài 8.1: Định nghĩa và sử dụng hàm trong C++

Bài 8.1: Định nghĩa và sử dụng hàm trong C++
Chào mừng các bạn quay trở lại với chuỗi bài viết về C++ của FullhouseDev! Hôm nay, chúng ta sẽ cùng nhau khám phá một khái niệm cực kỳ quan trọng và là "xương sống" của mọi ngôn ngữ lập trình hiện đại: Hàm (Function).
Nếu ví một chương trình lớn như việc xây dựng một ngôi nhà, thì các hàm chính là những "người thợ" chuyên trách các công việc cụ thể: người lát gạch, người đi dây điện, người sơn tường... Mỗi người thợ làm một việc riêng, phối hợp với nhau để hoàn thành công trình. Trong lập trình, hàm giúp chúng ta chia nhỏ chương trình thành các khối logic độc lập, dễ quản lý và tái sử dụng.
Hàm là gì? Tại sao chúng ta cần dùng hàm?
Đơn giản nhất, một hàm là một khối mã được đặt tên, được thiết kế để thực hiện một nhiệm vụ cụ thể.
Vậy tại sao lại cần dùng hàm?
- Tính tái sử dụng (Reusability): Thay vì viết cùng một đoạn mã nhiều lần ở các nơi khác nhau trong chương trình, bạn chỉ cần viết nó một lần trong một hàm. Sau đó, bất cứ khi nào cần thực hiện nhiệm vụ đó, bạn chỉ việc gọi hàm.
- Tính mô-đun (Modularity): Hàm cho phép chia chương trình lớn thành các phần nhỏ, độc lập. Điều này làm cho mã nguồn dễ đọc, dễ hiểu và dễ quản lý hơn rất nhiều. Khi gặp lỗi, bạn cũng dễ dàng khoanh vùng và sửa chữa trong từng hàm cụ thể.
- Tính dễ bảo trì (Maintainability): Nếu logic của một nhiệm vụ cần thay đổi, bạn chỉ cần sửa chữa duy nhất phần mã bên trong hàm đó, mà không ảnh hưởng đến những nơi khác gọi hàm.
- Trừu tượng hóa (Abstraction): Khi gọi một hàm, chúng ta chỉ cần biết nó làm gì (nhiệm vụ của nó) mà không nhất thiết phải quan tâm chi tiết nó làm như thế nào (cài đặt bên trong). Điều này giúp tập trung vào bức tranh lớn hơn của chương trình.
Nghe hấp dẫn đúng không nào? Bây giờ chúng ta sẽ đi sâu vào cách định nghĩa và sử dụng hàm trong C++.
Cú pháp định nghĩa hàm trong C++
Một hàm trong C++ thường có cấu trúc như sau:
kieu_du_lieu_tra_ve ten_ham(danh_sach_tham_so) {
// Thân hàm - Các câu lệnh thực hiện nhiệm vụ
// Có thể có câu lệnh 'return' để trả về giá trị
}
Chúng ta hãy cùng phân tích từng thành phần:
kieu_du_lieu_tra_ve
(Return Type): Kiểu dữ liệu của giá trị mà hàm sẽ trả về sau khi hoàn thành nhiệm vụ. Có thể làint
,double
,string
,bool
, hoặc bất kỳ kiểu dữ liệu nào khác. Nếu hàm không trả về bất kỳ giá trị nào, chúng ta sử dụng từ khóavoid
.ten_ham
(Function Name): Tên dùng để định danh và gọi hàm. Tên hàm nên mang ý nghĩa để dễ hiểu nhiệm vụ của nó (ví dụ:tinhTong
,inLoiChao
,kiemTraSoNguyenTo
). Quy tắc đặt tên giống như đặt tên biến.danh_sach_tham_so
(Parameter List): Một danh sách các biến mà hàm nhận làm đầu vào để thực hiện nhiệm vụ của mình. Mỗi tham số bao gồm kiểu dữ liệu và tên biến, cách nhau bởi dấu phẩy. Danh sách này có thể rỗng (hàm không nhận tham số nào). Khi đó, cặp dấu ngoặc đơn()
vẫn là bắt buộc.{}
(Thân hàm - Function Body): Chứa tất cả các câu lệnh sẽ được thực thi khi hàm được gọi. Đây là nơi chứa logic chính của hàm.return
(Tùy chọn): Nếu kiểu dữ liệu trả về không phải làvoid
, hàm phải sử dụng câu lệnhreturn
để trả về một giá trị có kiểu tương ứng. Khi gặpreturn
, hàm sẽ kết thúc ngay lập tức và trả về giá trị đó cho nơi gọi hàm.
Ví dụ cơ bản: Hàm không có tham số, không có giá trị trả về (void
)
Đây là dạng hàm đơn giản nhất, chỉ thực hiện một hành động và không cần thông tin đầu vào hay trả về kết quả.
#include <iostream>
void chaoMung() {
cout << "Chao mung ban den voi the gioi C++!\n";
cout << "Chung ta dang hoc ve ham.\n";
}
int main() {
chaoMung();
chaoMung();
return 0;
}
Output:
Chao mung ban den voi the gioi C++!
Chung ta dang hoc ve ham.
Chao mung ban den voi the gioi C++!
Chung ta dang hoc ve ham.
Thật tiện lợi! Chúng ta chỉ cần viết khối lệnh "Chào mừng..." một lần và gọi nó bất cứ khi nào cần.
Ví dụ 2: Hàm có tham số, không có giá trị trả về (void
)
Lúc này, hàm cần thông tin đầu vào để thực hiện nhiệm vụ. Thông tin này được truyền qua các tham số.
#include <iostream>
#include <string>
void chaoTen(string s) {
cout << "Xin chao, " << s << "!\n";
}
int main() {
chaoTen("Nguyen Van A");
chaoTen("Tran Thi B");
string s = "Le Van C";
chaoTen(s);
return 0;
}
Output:
Xin chao, Nguyen Van A!
Xin chao, Tran Thi B!
Xin chao, Le Van C!
Ví dụ 3: Hàm có tham số và có giá trị trả về
Đây là dạng hàm rất phổ biến, nó nhận đầu vào, xử lý và trả về kết quả.
#include <iostream>
int tinhTong(int a, int b) {
int kq = a + b;
return kq;
}
double tinhTrungBinh(double d1, double d2) {
return (d1 + d2) / 2.0;
}
int main() {
int tong = tinhTong(10, 20);
cout << "Tong cua 10 va 20 la: " << tong << endl;
cout << "Tong cua 5 va 7 la: " << tinhTong(5, 7) << endl;
double dtb = tinhTrungBinh(8.5, 9.0);
cout << "Diem trung binh la: " << dtb << endl;
return 0;
}
Output:
Tong cua 10 va 20 la: 30
Tong cua 5 va 7 la: 12
Diem trung binh la: 8.75
Lưu ý về return
:
- Khi câu lệnh
return
được thực thi, hàm sẽ dừng ngay lập tức. Các câu lệnh phía saureturn
trong cùng khối lệnh sẽ không được thực thi. - Nếu hàm có kiểu trả về không phải
void
, thì bắt buộc phải có ít nhất một câu lệnhreturn
trả về giá trị đúng kiểu dữ liệu.
Ví dụ 4: Hàm Prototype (Khai báo hàm)
Trong C++, khi bạn gọi một hàm, trình biên dịch cần phải biết về sự tồn tại và signature (kiểu trả về, tên hàm, danh sách kiểu tham số) của hàm đó tại điểm gọi. Nếu bạn định nghĩa toàn bộ hàm sau hàm main
(hoặc sau một hàm khác gọi nó), trình biên dịch sẽ báo lỗi "function not declared".
Để khắc phục, chúng ta sử dụng Prototype hàm (hay còn gọi là Khai báo hàm). Prototype là một dòng code đơn giản chỉ khai báo chữ ký của hàm, kết thúc bằng dấu chấm phẩy ;
.
Cú pháp Prototype:
kieu_du_lieu_tra_ve ten_ham(danh_sach_kieu_du_lieu_tham_so); // Chỉ cần kiểu dữ liệu, tên tham số là tùy chọn
Ví dụ:
#include <iostream>
int truHaiSo(int a, int b);
void xinChao() {
cout << "Xin chao tu ham xinChao!\n";
}
int main() {
xinChao();
int hieu = truHaiSo(50, 15);
cout << "Hieu cua 50 va 15 la: " << hieu << endl;
return 0;
}
int truHaiSo(int a, int b) {
return a - b;
}
Output:
Xin chao tu ham xinChao!
Hieu cua 50 va 15 la: 35
Giải thích:
- Hàm
xinChao()
được định nghĩa trước hàmmain
, nên khimain
gọi nó, trình biên dịch đã biết về sự tồn tại của nó. - Hàm
truHaiSo()
được định nghĩa sau hàmmain
. Nếu không có dòngint truHaiSo(int a, int b);
ở trên cùng (trướcmain
), khimain
gọitruHaiSo
, trình biên dịch sẽ không tìm thấy định nghĩa và báo lỗi. - Dòng
int truHaiSo(int a, int b);
(hoặc đơn giản hơn làint truHaiSo(int, int);
) là prototype. Nó thông báo cho trình biên dịch rằng "sẽ có một hàm tên làtruHaiSo
, nhận hai tham số kiểuint
và trả về một giá trị kiểuint
. Phần định nghĩa đầy đủ sẽ có sau".
Việc sử dụng prototype rất quan trọng khi:
- Bạn muốn tổ chức mã nguồn theo cách định nghĩa các hàm sau hàm
main
đểmain
ở trên cùng, dễ đọc hơn. - Các hàm gọi lẫn nhau một cách "chéo" (ví dụ: hàm A gọi hàm B, và hàm B lại gọi hàm A). Trong trường hợp này, ít nhất một trong hai hàm cần có prototype trước hàm còn lại được định nghĩa.
- Làm việc với các file
.h
(header files) trong các dự án lớn (sẽ học ở bài sau).
Ví dụ 5: Kết hợp các loại hàm
Hãy thử một ví dụ kết hợp các khái niệm đã học để giải quyết một bài toán nhỏ: Tính diện tích hình chữ nhật và in kết quả.
#include <iostream>
double tinhDienTichHCN(double d, double r);
void inKqDienTich(double dt);
int main() {
double d = 15.5;
double r = 10.0;
double dt = tinhDienTichHCN(d, r);
inKqDienTich(dt);
inKqDienTich(tinhDienTichHCN(5.0, 8.0));
return 0;
}
double tinhDienTichHCN(double d, double r) {
if (d <= 0 || r <= 0) {
cerr << "Loi: Chieu dai va chieu rong phai lon hon 0.\n";
return 0.0;
}
return d * r;
}
void inKqDienTich(double dt) {
cout << "Dien tich hinh chu nhat la: " << dt << endl;
}
Output:
Dien tich hinh chu nhat la: 155
Dien tich hinh chu nhat la: 40
(Nếu bạn gọi tinhDienTichHCN(-5, 10)
, kết quả sẽ in lỗi ra cerr
và trả về 0).
Tóm lại về các lợi ích khi sử dụng hàm
Qua các ví dụ trên, bạn có thể thấy rõ:
- Mã nguồn trong
main
trở nên gọn gàng và dễ đọc hơn rất nhiều vì nó chỉ chứa các lời gọi hàm mô tả các bước chính của chương trình (tính toán, in kết quả). - Logic chi tiết cho từng bước được "đóng gói" bên trong các hàm riêng biệt.
- Các hàm có thể được gọi lại bất cứ lúc nào cần.
Bài tập ví dụ: C++ Bài 4.A1: GCD và LCM
Trong toán học \(GCD(a,b)\) được hiểu là ước chung lớn nhất của hai số \(a, b\). Tương tự như vậy ta có \(LCM(a,b)\) là bội chung nhỏ nhất của hai số \(a,b\).
Hãy viết chương trình tính \(GCD(a,b)\) và \(LCM(a,b)\).
INPUT FORMAT
Dòng đầu tiên chứa giá trị của \(T (1 \leq T \leq 100)\) là số lượng testcase của bài toán.
\(T\) dòng tiếp theo chứa giá trị của \(a,b (1 \leq a,b \leq 10^9)\) là giá trị cần tính.
OUTPUT FORMAT
In ra \(T\) dòng mỗi chứa hai giá trị lần lượt là \(GCD\) và \(LCM\) mỗi số cách nhau một dấu cách.
Ví dụ 1:
Input
2
3 6
6 26
Ouput
3 6
2 78
Giải thích ví dụ mẫu:
Ví dụ 1:
- Input:
3 6
- Giải thích:
GCD(3, 6)
là 3 vàLCM(3, 6)
là 6.
- Input:
Ví dụ 2:
- Input:
6 26
- Giải thích:
GCD(6, 26)
là 2 vàLCM(6, 26)
là 78.
- Input:
Đây là hướng dẫn giải bài tập tính GCD và LCM trong C++ dựa trên yêu cầu của bạn, tập trung vào hướng giải và sử dụng các công cụ có sẵn trong thư viện chuẩn (std) khi thích hợp, không đưa ra code hoàn chỉnh.
Phân tích bài toán:
- Đầu vào:
T
test case, mỗi test case gồm hai số nguyên dươnga
vàb
(lên đến10^9
). - Đầu ra: Với mỗi test case, in ra
GCD(a, b)
vàLCM(a, b)
. - Yêu cầu: Tính toán hiệu quả vì
a
vàb
có thể rất lớn. Sử dụngstd
khi có thể. Không đưa code hoàn chỉnh.
Hướng giải:
Kiểu dữ liệu: Vì
a
vàb
có thể lên tới10^9
, tícha * b
có thể lên tới10^{18}
. Kiểu dữ liệuint
thông thường (32-bit) chỉ chứa được giá trị khoảng2 * 10^9
, sẽ bị tràn số (overflow). Do đó, bạn cần sử dụng kiểu dữ liệulong long
để lưu trữa
,b
, và kết quảLCM
. Kết quảGCD
sẽ không vượt quámin(a, b)
, nên có thể lưu tronglong long
hoặcint
nếu bạn chắc chắnmin(a,b)
không vượt quá2*10^9
. Tuy nhiên, để an toàn và nhất quán, tốt nhất nên dùnglong long
cho cảa
,b
, GCD, và LCM.Tính GCD (Ước chung lớn nhất):
- Phương pháp hiệu quả nhất để tính GCD của hai số là Thuật toán Euclid. Ý tưởng cơ bản là
GCD(a, b) = GCD(b, a % b)
cho đến khib
bằng 0. Khi đó,GCD(a, 0) = a
. - Sử dụng
std
: Từ C++17 trở đi, thư viện chuẩn<numeric>
cung cấp hàmgcd(a, b)
giúp bạn tính GCD một cách trực tiếp và hiệu quả. Đây là cách được khuyến khích sử dụng nếu môi trường biên dịch hỗ trợ C++17 hoặc mới hơn.
- Phương pháp hiệu quả nhất để tính GCD của hai số là Thuật toán Euclid. Ý tưởng cơ bản là
Tính LCM (Bội chung nhỏ nhất):
- Có một công thức rất hữu ích liên hệ giữa GCD và LCM của hai số nguyên dương
a
vàb
:a * b = GCD(a, b) * LCM(a, b)
- Từ đó, ta có thể tính LCM bằng công thức:
LCM(a, b) = (a * b) / GCD(a, b)
- Cẩn thận tràn số: Khi tính
(a * b) / GCD(a, b)
, nếu bạn tínha * b
trước, kết quả có thể bị tràn số nếua
vàb
đều lớn (gần10^9
). Để tránh điều này, bạn có thể sử dụng công thức tương đương sau, tính toán trên các giá trị nhỏ hơn trước:LCM(a, b) = (a / GCD(a, b)) * b
(Hoặc(b / GCD(a, b)) * a
). VìGCD(a, b)
luôn là ước củaa
(vàb
), phép chiaa / GCD(a, b)
(hoặcb / GCD(a, b)
) luôn cho kết quả nguyên. Kết quả này sau đó nhân với số còn lại sẽ cho LCM mà khả năng bị tràn số trước khi phép chia là rất thấp. - Sử dụng
std
: Tương tự như GCD, từ C++17, thư viện<numeric>
cung cấp hàmlcm(a, b)
để tính LCM. Hàm này thường sử dụng nội bộ mối quan hệa * b = GCD * LCM
và có thể được thiết kế để xử lý vấn đề tràn số (hoặc yêu cầu kiểu dữ liệu đủ lớn).
- Có một công thức rất hữu ích liên hệ giữa GCD và LCM của hai số nguyên dương
Cấu trúc chương trình:
- Đọc số lượng test case
T
. - Sử dụng một vòng lặp chạy
T
lần. - Trong mỗi lần lặp:
- Đọc hai số
a
vàb
(sử dụng kiểulong long
). - Tính GCD của
a
vàb
(ví dụ:long long gcd_val = gcd(a, b);
). - Tính LCM của
a
vàb
sử dụng công thức và giá trị GCD vừa tính (ví dụ:long long lcm_val = (a / gcd_val) * b;
hoặclong long lcm_val = lcm(a, b);
). - In ra
gcd_val
vàlcm_val
, cách nhau bởi một dấu cách.
- Đọc hai số
- Đọc số lượng test case
Tóm lại các bước cần thực hiện:
- Include các header cần thiết (
<iostream>
cho nhập/xuất,<numeric>
chogcd
vàlcm
). - Sử dụng
long long
cho các biến lưu giá trịa
,b
, GCD, và LCM. - Đọc
T
. - Vòng lặp
T
lần. - Trong vòng lặp, đọc
a
,b
. - Tính GCD (ví dụ:
gcd(a, b)
). - Tính LCM (ví dụ:
(a / GCD) * b
hoặclcm(a, b)
). - In kết quả GCD và LCM.
Comments