Đến bài này, mọi chương trình đều nằm gọn trong main. Cách đó ổn cho tới khi cùng một việc phải làm nhiều lần. Ví dụ đếm số cách chọn bạn từ bạn: toán học cho sẵn công thức , trong đó (đọc là “ giai thừa”) là tích . Công thức cần ba giai thừa, tức ba vòng lặp gần như giống hệt nhau. Chép ba lần thì code dài gấp ba, và sửa lỗi ở một bản rất dễ quên hai bản còn lại. Hàm (function) tránh chuyện đó: ta viết đoạn code một lần, đặt cho nó một cái tên, rồi gọi tên ấy bao nhiêu lần tùy ý, mỗi lần với dữ liệu khác.
Các bài tập của bài này đều yêu cầu viết phần xử lý trong hàm riêng, đúng tên đề cho, và gọi từ main; máy chấm chỉ so kết quả, còn việc tổ chức hàm do giáo viên đọc code.
Định nghĩa và gọi hàm
Muốn in số cách chọn bạn từ bạn với , ta tính , và . Viết thẳng trong main sẽ thành ba vòng lặp:
long long f1 = 1;
for (int i = 2; i <= n; i++) f1 *= i;
long long f2 = 1;
for (int i = 2; i <= k; i++) f2 *= i;
long long f3 = 1;
for (int i = 2; i <= n - k; i++) f3 *= i;
cout << f1 / (f2 * f3) << '\n';
Ba đoạn khác nhau đúng một chỗ: con số đứng sau i <=. Chỗ khác đó là dữ liệu đưa vào, phần còn lại là việc chung. Hàm tách hai thứ ấy ra: việc chung nằm một lần trong thân hàm, dữ liệu đưa vào là tham số.
#include <bits/stdc++.h>
using namespace std;
long long fact(int m) {
long long res = 1;
for (int i = 2; i <= m; i++) res *= i;
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
cout << fact(n) / (fact(k) * fact(n - k)) << '\n';
}
5 2
10
Với , chương trình in : có 10 cách chọn 2 bạn từ 5 bạn, đúng như khi liệt kê tay. Muốn đổi cách tính giai thừa, ta chỉ sửa một chỗ.
Dòng đầu của hàm fact gồm ba phần, đọc từ trái sang phải:
long longlà kiểu trả về (return type): loại giá trị hàm gửi lại cho nơi gọi.factlà tên hàm. Đặt tên theo việc hàm làm để đọc lời gọi là hiểu.(int m)là danh sách tham số (parameter): những biến nhận dữ liệu vào. Mỗi tham số có kiểu riêng, cách nhau bằng dấu phẩy, nhưint add(int a, int b). Viếtint add(int a, b)là lỗi vìbthiếu kiểu.
Phần trong { } là thân hàm. Lệnh return res; gửi giá trị của res về nơi gọi và kết thúc hàm. main cũng là một hàm: hệ thống gọi nó đầu tiên, và nếu thân main không có return thì C++ tự coi như return 0;.
Lời gọi hàm là tên hàm kèm cặp ngoặc, trong ngoặc là các đối số (argument). Trong fact(n - k), đối số là n - k. Tham số là biến nằm trong định nghĩa, đối số là giá trị đưa vào lúc gọi. Gặp một lời gọi, chương trình làm bốn việc theo thứ tự:
- Tính giá trị các đối số. Với , , đối số
n - kbằng 3. - Tạo tham số
mvà gán giá trị vừa tính cho nó, ở đâym = 3. - Chạy thân hàm cho tới khi gặp
return. - Thay cả lời gọi bằng giá trị trả về, rồi chạy tiếp từ chỗ vừa gọi.
Nhờ bước 4, fact(k) * fact(n - k) đọc đúng như tích của hai số: lời gọi hàm đặt được ở bất cứ chỗ nào đặt được một giá trị.
Hàm trả giá trị về chứ không tự in nó. Việc in thuộc về nơi gọi, như cout << fact(5). Đặt cout trong hàm thì hàm in ngay lúc chạy, kể cả khi người gọi chỉ muốn lấy giá trị để cộng hay so sánh. Một lỗi cú pháp hay gặp: gõ ; ngay sau ) ở dòng đầu của định nghĩa; trình biên dịch hiểu đó là lời khai báo, nên cặp { } bên dưới không thuộc hàm nào và nó báo lỗi ở dấu {.
Hỏi nhanh
Cho hàm int f(int x) { cout << x; return x * 2; }. Dòng cout << f(3); trong main in ra gì?
Để in giá trị của f(3), chương trình phải chạy f(3) trước. Trong lúc chạy, thân hàm làm cout << x và in ra 3. Hàm trả về 6, rồi cout ở main in tiếp 6: kết quả 36.
Đáp án 6 quên rằng cout bên trong hàm vẫn in; 3 quên rằng giá trị trả về cũng được in; 63 đảo thứ tự, như thể giá trị trả về được in trước khi hàm chạy xong.
Ví dụGọi hàm hai lần
Đề. Viết hàm square nhận và trả về , rồi gọi nó hai lần để in ().
Lời giải. Hàm có một tham số và một lệnh return x * x;. Trong main, hai lời gọi square(a) và square(b) đứng ngay trong biểu thức cộng, như hai con số. âm không cần xử lý gì: bình phương luôn không âm. Giá trị lớn nhất là , vừa int.
#include <bits/stdc++.h>
using namespace std;
int square(int x) {
return x * x;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int a, b;
cin >> a >> b;
cout << square(a) + square(b) << '\n';
}
3 4
25
Giá trị trả về và return
Một lệnh return chạy là hàm kết thúc ngay, mọi lệnh phía sau bị bỏ qua. Điều này tạo ra một mẫu rất hay dùng. Với câu hỏi dạng “có tồn tại... không?”, ta trả lời “có” ngay lúc gặp một trường hợp thỏa, còn “không” chỉ được trả lời sau khi đã xét hết. Ví dụ: có phải số chính phương không? Thử chừng nào ; gặp thì trả true luôn, thử hết mà không thấy thì trả false.
#include <bits/stdc++.h>
using namespace std;
bool isSquare(int n) {
for (int i = 0; i * i <= n; i++) {
if (i * i == n) return true;
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
if (isSquare(n)) cout << "YES\n";
else cout << "NO\n";
}
49
YES
Hàm bool trả true hoặc false; tên thường bắt đầu bằng is để đọc như một câu hỏi. Với , i * i tối đa chừng nên vẫn vừa int.
Hỏi nhanh
Một bạn viết isSquare với vòng for (int i = 0; i * i <= n; i++). Trong thân vòng lặp bạn ấy viết if (i * i == n) return true; else return false;, sau vòng lặp có thêm return false;. Với n = 49 hàm trả về gì?
Không có chuyện “kết quả tạm”: return chạy là hàm dừng hẳn. Ở vòng đầu i = 0, điều kiện 0 == 49 sai nên nhánh else chạy return false, và vòng i = 7 không bao giờ tới. Hai lệnh return trong một vòng lặp hoàn toàn hợp lệ nên không có lỗi biên dịch.
Cách sửa: bỏ nhánh else, để return false đúng một lần sau vòng lặp. Lỗi này hay gặp nhất khi viết hàm kiểm tra số nguyên tố.
Hàm bool có thể trả thẳng một phép so sánh, như return n % 2 != 0; cho câu hỏi “ lẻ không”. Nhớ -3 % 2 bằng -1, nên viết n % 2 == 1 thì mọi số lẻ âm bị coi là chẵn. cout << isOdd(n) in ra 1 hoặc 0, không in true hay false.
Kiểu trả về quyết định kết quả. Phép chia hai số int là chia nguyên, nên double half(int x) { return x / 2; } vẫn cho half(7) bằng 3.
Hỏi nhanh
Với double half(int x) { return x / 2; }, dòng cout << half(7); in ra gì?
Trong x / 2, cả x lẫn 2 đều là int, nên đây là chia nguyên: 7 chia 2 được 3, dư 1. Giá trị 3 chỉ được đổi sang double sau đó, ở lệnh return.
3.5 chỉ có khi một toán hạng là số thực, như x / 2.0. 4 là làm tròn lên, mà chia nguyên không làm tròn. 3.0 đúng về giá trị, nhưng cout mặc định không in phần thập phân thừa.
Kiểu trả về cũng phải đủ rộng cho kết quả. fact ở đầu bài trả long long vì đã vượt int; khai báo int fact(int m) thì từ kết quả sai. Tham số cũng vậy: truyền một số long long lớn vào tham số int thì giá trị bị cắt.
Cuối cùng, hàm không phải void phải gặp return trên mọi đường đi. Quên một đường thì hàm trả về một giá trị không ai đoán trước được: máy bạn có thể chạy đúng còn máy chấm thì không. Hàm dưới đủ ba đường, kể cả :
int sign(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
return 0;
}
Ví dụHàm kiểm tra nguyên tố
Đề. Số nguyên tố là số nguyên lớn hơn 1 có đúng hai ước dương. Viết hàm isPrime nhận (); in YES nếu nguyên tố, ngược lại in NO.
Lời giải. Đúng mẫu “có tồn tại không”, nhưng hỏi ngược: không nguyên tố khi có một ước với . Trước hết loại bằng return false, vì test ví dụ chính là . Rồi thử từng ; gặp ước thì return false ngay, hết vòng mới return true. Nhánh else return true đặt trong vòng là lỗi của câu hỏi nhanh trên: với hàm sẽ trả true ngay ở . Với , vòng chạy tối đa 998 lượt.
#include <bits/stdc++.h>
using namespace std;
bool isPrime(int n) {
if (n < 2) return false;
for (int d = 2; d < n; d++) {
if (n % d == 0) return false;
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
if (isPrime(n)) cout << "YES\n";
else cout << "NO\n";
}
1
NO
Hàm void: làm việc mà không trả giá trị
Có hàm chỉ làm một thao tác, như in một hàng ký tự, mà không có kết quả nào để trả. Hàm đó dùng kiểu trả về void (“không có gì”) và không cần return; muốn thoát sớm thì viết return; không kèm giá trị. Chương trình dưới in tam giác dấu # có hàng; hàm printRow in ký tự rồi xuống dòng.
#include <bits/stdc++.h>
using namespace std;
void printRow(int n, char c) {
for (int i = 0; i < n; i++) cout << c;
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++) printRow(i, '#');
}
4
#
##
###
####
Hàm có hai tham số khác kiểu, int và char. Lời gọi ghép đối số với tham số theo thứ tự, không theo tên. Đảo thành printRow('#', 3) vẫn biên dịch, vì C++ tự đổi qua lại giữa số và ký tự (mã của # là 35), nhưng chương trình in 35 ký tự vô nghĩa thay vì ba dấu #.
Hàm void được gọi như một câu lệnh. Viết int x = printRow(3, '#'); là lỗi vì không có giá trị nào để gán. Còn hàm tính ra kết quả thì nên trả kết quả về để main quyết định dùng nó thế nào: hàm add tự in tổng sẽ không dùng lại được khi cần .
Ví dụHàm in dòng sao
Đề. Viết hàm printStars kiểu void, nhận () và in một dòng gồm ký tự * liền nhau. Đọc và gọi hàm.
Lời giải. Không có giá trị nào để trả: việc in chính là mục đích, nên hàm là void. Thân hàm có vòng for in đúng dấu *, rồi xuống dòng một lần, sau vòng. Đặt '\n' trong vòng thì mỗi dấu * nằm một dòng. main chỉ đọc và gọi, không in gì thêm.
#include <bits/stdc++.h>
using namespace std;
void printStars(int n) {
for (int i = 0; i < n; i++) cout << '*';
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
printStars(n);
}
4
****
Biến cục bộ và truyền theo giá trị
Biến khai báo trong hàm, kể cả tham số, là biến cục bộ (local variable): chỉ dùng được trong hàm đó và biến mất khi hàm kết thúc. Hai hàm cùng đặt tên n vẫn là hai biến khác nhau, nên ta viết từng hàm mà không cần biết các hàm khác dùng tên gì. Biến cục bộ cũng không tự bằng 0: int sum; rồi sum += x cộng vào một giá trị rác, nên luôn viết int sum = 0;.
Khi gọi hàm, tham số được khởi tạo bằng bản sao giá trị của đối số. Cách này gọi là truyền theo giá trị (pass by value). Hàm thoải mái sửa tham số của mình, nhưng biến ở nơi gọi không bị đụng tới.
#include <bits/stdc++.h>
using namespace std;
void addOne(int x) {
x = x + 1;
cout << "x = " << x << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int a = 5;
addOne(a);
cout << "a = " << a << '\n';
}
x = 6
a = 5
Hỏi nhanh
Cho void reset(int n) { n = 0; }. Trong main có int n = 42; reset(n); cout << n;. Chương trình in ra gì?
n trong reset là tham số, một biến cục bộ chỉ là bản sao của n ở main. Hàm gán 0 cho bản sao, rồi bản sao biến mất; n ở main vẫn là 42. Trùng tên giữa hai hàm hoàn toàn hợp lệ vì hai n nằm ở hai nơi khác nhau.
Đáp án 0 sẽ đúng nếu tham số khai báo là int &n, điều ta học ngay dưới đây.
Muốn hàm sửa được biến của nơi gọi, thêm & sau kiểu tham số. Tham số khi đó không phải bản sao mà là cái tên thứ hai của chính biến được truyền vào, gọi là tham chiếu (reference). Ví dụ hoán đổi hai số:
#include <bits/stdc++.h>
using namespace std;
void doiCho(int &a, int &b) {
int t = a;
a = b;
b = t;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int x, y;
cin >> x >> y;
doiCho(x, y);
cout << x << ' ' << y << '\n';
}
3 8
8 3
Đối số của tham số & phải là một biến: doiCho(3, 8) báo lỗi vì không có biến nào để sửa. Thư viện đã có sẵn swap(a, b) làm đúng việc này, nên ví dụ trên đặt tên khác để khỏi lẫn. Chỉ dùng & khi hàm thật sự phải sửa biến gốc.
Ví dụGiữ số ban đầu
Đề. Viết hàm digitSum nhận () và trả về tổng các chữ số của . In ban đầu rồi tổng chữ số trên một dòng; trong main phải giữ nguyên sau khi gọi hàm.
Lời giải. Thân hàm là vòng bóc chữ số của B3: cộng n % 10 vào sum, rồi n /= 10, tới khi . Vòng này phá hủy , nhưng trong hàm chỉ là bản sao: tham số không có & nên n ở main còn nguyên để in. Chính vì vậy tham số không được khai báo int &n; có & thì main sẽ in 0 6. Với vòng không chạy và hàm trả 0.
#include <bits/stdc++.h>
using namespace std;
int digitSum(int n) {
int sum = 0;
while (n > 0) {
sum += n % 10;
n /= 10;
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
cout << n << ' ' << digitSum(n) << '\n';
}
123
123 6
Thứ tự hàm và ghép các hàm nhỏ
Trình biên dịch đọc file từ trên xuống. Gặp lời gọi cube(3) trong main mà chưa từng thấy cube, nó báo 'cube' was not declared in this scope. Cách chữa thông dụng nhất là đặt định nghĩa hàm lên trên chỗ gọi: hàm A gọi hàm B thì B nằm trên A, và main nằm cuối file. Cách thứ hai là khai báo trước bằng nguyên mẫu hàm (function prototype): dòng đầu của hàm kết thúc bằng ;, như int cube(int x);, đặt trên main; thân hàm viết ở đâu phía sau cũng được. Khai báo mà không bao giờ định nghĩa thì chương trình vẫn biên dịch, nhưng lỗi ở bước liên kết (undefined reference) khi có lời gọi. Với bài ngắn, đặt hàm lên trên main là đủ.
Bài toán: cho tháng và năm, in số ngày của tháng đó. Ta chia thành hai việc. isLeap(y) trả lời năm có nhuận không, đúng điều kiện của B2. days(m, y) dựa vào đó để trả số ngày.
#include <bits/stdc++.h>
using namespace std;
bool isLeap(int y) {
return y % 400 == 0 || (y % 4 == 0 && y % 100 != 0);
}
int days(int m, int y) {
if (m == 2) {
if (isLeap(y)) return 29;
return 28;
}
if (m == 4 || m == 6 || m == 9 || m == 11) return 30;
return 31;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int m, y;
cin >> m >> y;
cout << days(m, y) << '\n';
}
2 2024
29
days gọi isLeap, nên isLeap nằm phía trên. Mỗi hàm làm đúng một việc và tên hàm nói đúng việc đó; hàm tính toán không đọc input và không in, main lo đọc, gọi và in. Khi bài dài ra, hãy thử từng hàm riêng trên vài giá trị biên (isLeap(1900), isLeap(2000), days(2, 2023)) trước khi ghép, để khi sai ta biết sai ở hàm nào.
Ví dụHàm kiểm tra đối xứng
Đề. Số đối xứng là số đọc xuôi và đọc ngược như nhau, như 1221 hay 7. Viết hàm reverseNumber đảo các chữ số của , rồi dùng nó trong hàm isPalindrome. Cho (), in YES nếu đối xứng, ngược lại in NO.
Lời giải. đối xứng khi và chỉ khi đảo các chữ số của lại được chính . Việc đảo dùng cách bóc chữ số của ví dụ trước, nhưng chữ số lấy ra không cộng dồn mà được đẩy vào cuối số mới: r = r * 10 + n % 10. Nhân r với 10 dịch các chữ số đã có sang trái một chỗ, rồi chữ số mới chiếm hàng đơn vị. isPalindrome chỉ còn một dòng return reverseNumber(n) == n;, và phải nằm dưới reverseNumber.
Số tận cùng bằng 0 không làm hỏng cách này: 3050 đảo thành 503 vì các số 0 đứng đầu tự mất, 503 khác 3050 nên ra NO, đúng vì chữ số đầu 3 khác chữ số cuối 0. Còn đảo ra 0, ra YES. Số đảo của một số 10 chữ số có thể vượt int (1999999999 đảo thành 9999999991), nên dùng long long cho chắc.
#include <bits/stdc++.h>
using namespace std;
long long reverseNumber(long long n) {
long long r = 0;
while (n > 0) {
r = r * 10 + n % 10;
n /= 10;
}
return r;
}
bool isPalindrome(long long n) {
return reverseNumber(n) == n;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
long long n;
cin >> n;
if (isPalindrome(n)) cout << "YES\n";
else cout << "NO\n";
}
1221
YES
Khi đi thi
- Thấy cùng một đoạn code xuất hiện từ hai lần (tính giai thừa, kiểm tra nguyên tố, đảo số) là tách thành hàm ngay; hàm tính thì
return, cònmainđọc và in. - Chọn kiểu trả về và kiểu tham số đủ rộng cho cả số trung gian (giai thừa, lũy thừa, số đảo đều nên là
long long), rồi thử hàm trên giá trị biên 0, 1 và số âm. - Hàm nhận bản sao nên cứ sửa tham số thoải mái, còn muốn sửa biến ngoài thì thêm
&; mọi đường đi của hàm khôngvoidđều phải kết thúc bằngreturn, và hàm phải nằm trên chỗ gọi nó.
Tóm tắt
- Hàm gồm kiểu trả về, tên, danh sách tham số và thân;
returngửi một giá trị về nơi gọi và kết thúc hàm ngay. - Câu hỏi “có tồn tại không”:
return truengay khi gặp,return falsemột lần sau vòng lặp. - Hàm tính ra kết quả thì trả về, không tự in; hàm
voiddành cho việc mà bản thân thao tác là mục đích, như in. - Tham số là bản sao của đối số: sửa tham số không ảnh hưởng biến ở nơi gọi, trừ khi tham số có
&. - Hàm phải được định nghĩa hoặc khai báo trước chỗ gọi; hàm gọi hàm thì hàm được gọi nằm phía trên.
Bài tập
Đọc thêm
- Introduction to function parameters and argumentslearncpp.com
- Forward declarationslearncpp.com
- Functionscplusplus.com
- 函数OI Wiki
- Hàm trong C++Viblo