Khái niệm hàm
Sau bài này bạn sẽ làm được
- Giải thích được sáu lợi ích của việc chia chương trình thành hàm
- Áp dụng nguyên tắc một hàm làm một việc
- Nhận ra khi nào một hàm đã quá dài hoặc quá nhiều tham số
- Phân biệt hàm thuần với hàm có tác dụng phụ
Tới hết Phần 2, mọi chương trình bạn viết đều nằm gọn trong main. Cách đó chạy được với vài chục dòng và sụp đổ ở vài trăm dòng. Chương này dạy bạn cắt một chương trình thành nhiều mảnh có tên, và đây cũng là chương chuẩn bị trực tiếp cho con trỏ ở Phần 5.
#Hàm là gì
Bạn đã dùng hàm từ Bài 1.4: printf, scanf, main đều là hàm. Điểm khác biệt duy nhất từ bài này là bạn bắt đầu tự viết chúng thay vì chỉ gọi hàm người khác viết.
#include <stdio.h>
/* Khai báo: báo cho trình biên dịch biết hàm này tồn tại. */
int binh_phuong(int x);
int main(void)
{
for (int i = 1; i <= 5; ++i) {
printf("%d ", binh_phuong(i));
}
printf("\n");
return 0;
}
/* Định nghĩa: phần thân thật sự của hàm. */
int binh_phuong(int x)
{
return x * x;
}Đoạn trên in 1 4 9 16 25. Phép x * x chỉ được viết một lần nhưng chạy năm lần. Đó là toàn bộ ý tưởng.
#Sáu lợi ích
| Lợi ích | Nghĩa là gì | Mất gì nếu không có |
|---|---|---|
| Không lặp lại mã | Viết một lần, gọi ở nhiều nơi | Sửa một lỗi phải sửa mười chỗ, và bạn sẽ quên một chỗ |
| Chia để trị | Bài toán lớn tách thành các bài toán nhỏ hiểu được | Bạn phải giữ toàn bộ chương trình trong đầu cùng lúc |
| Dễ kiểm thử | Gọi riêng từng hàm với dữ liệu tự chọn | Muốn thử một phép tính phải chạy cả chương trình |
| Dễ bảo trì | Đổi cách làm bên trong mà bên ngoài không biết | Mọi thay đổi đều lan ra khắp nơi |
| Trừu tượng hóa | Người gọi chỉ cần biết hàm làm gì, không cần biết làm thế nào | Người đọc phải hiểu từng dòng mới hiểu ý đồ |
| Tái sử dụng | Đóng gói thành thư viện dùng cho dự án sau | Mỗi dự án viết lại từ đầu |
Lợi ích thứ năm là lợi ích lớn nhất và cũng khó cảm nhận nhất khi mới học. Khi bạn viết sqrt(2.0), bạn không cần biết thư viện dùng phương pháp Newton hay bảng tra. Bạn chỉ cần tin nó trả về căn bậc hai. Mỗi hàm bạn viết cũng nên tạo ra được niềm tin đó cho người đọc sau này, kể cả khi người đó chính là bạn ba tháng nữa.
#Sáu nguyên tắc thiết kế
1. Một hàm làm một việc
Nếu bạn phải dùng chữ và để mô tả hàm, gần như chắc chắn nó nên tách làm hai. Hàm tên doc_va_sap_xep_va_in là ba hàm bị dán vào nhau.
2. Tên hàm là động từ
| Tên tốt | Tên xấu | Vì sao |
|---|---|---|
| tinh_tong | tong | Hàm là hành động, biến mới là danh từ |
| la_nguyen_to | nguyen_to | Tiền tố la_ báo ngay đây là câu hỏi đúng sai |
| doc_file | file | Không biết là đọc, ghi hay xóa |
| dem_tu | xu_ly | Xử lý cái gì, xử lý thế nào |
3. Không quá năm mươi dòng
Con số này không thiêng liêng, nhưng nó là một cái chuông báo tốt. Một hàm dài hơn một màn hình buộc người đọc phải cuộn lên cuộn xuống để nhớ biến nào là biến nào. Khi hàm vượt ngưỡng, hãy tìm những khối lệnh liền nhau có thể đặt tên được và tách chúng ra.
4. Không quá bốn tới năm tham số
/* Khó dùng. Người gọi phải nhớ đúng thứ tự bảy thứ. */
void tao_sv(char *ten, int tuoi, float d1, float d2,
float d3, char *lop, char *mssv);
/* Dễ dùng. Gom những thứ đi cùng nhau vào một struct, học ở Chương 15. */
typedef struct {
char ten[64];
char lop[16];
char mssv[16];
int tuoi;
float diem[3];
} SinhVien;
void tao_sv(SinhVien *sv);Nhiều tham số cùng kiểu là nguy hiểm nhất. Trình biên dịch không thể báo lỗi khi bạn đổi chỗ hai tham số float, nên lỗi chỉ lộ ra lúc chạy dưới dạng con số sai.
5. Hạn chế tác dụng phụ
Ưu tiên hàm chỉ nhận vào và trả ra, không đụng vào biến toàn cục, không in ra màn hình, không ghi file. Mục tiếp theo nói kỹ về điều này.
6. Luôn có cách báo lỗi
Nếu một hàm có thể thất bại thì người gọi phải biết được nó đã thất bại. Bài 8.6 trình bày ba quy ước báo lỗi thông dụng của C.
#Hàm thuần và tác dụng phụ
/* Hàm thuần. Gọi bao nhiêu lần cũng cho cùng kết quả. */
int cong(int a, int b)
{
return a + b;
}
/* Không thuần: kết quả phụ thuộc biến ngoài và còn sửa nó. */
int tong_don = 0;
int cong_don(int a)
{
tong_don += a;
return tong_don;
}| Tiêu chí | Hàm thuần | Hàm có tác dụng phụ |
|---|---|---|
| Kiểm thử | Chỉ cần so đầu vào với đầu ra | Phải dựng trạng thái trước, dọn sau |
| Gỡ lỗi | Nhìn đối số là đoán được kết quả | Phải truy xem ai đã đổi trạng thái |
| Đa luồng | An toàn tự nhiên | Cần khóa, dễ tranh chấp |
| Tối ưu | Trình biên dịch được phép gọi ít lần hơn | Bắt buộc gọi đủ số lần |
#Một ví dụ tách hàm đầy đủ
Bài toán: nhập một dãy số nguyên, in ra giá trị lớn nhất, giá trị nhỏ nhất và trung bình cộng. Đây là phiên bản gộp hết vào main.
int main(void)
{
int a[100];
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d", &a[i]);
int max = a[0];
for (int i = 1; i < n; ++i)
if (a[i] > max) max = a[i];
int min = a[0];
for (int i = 1; i < n; ++i)
if (a[i] < min) min = a[i];
long long tong = 0;
for (int i = 0; i < n; ++i)
tong += a[i];
printf("%d %d %.2f\n", max, min, (double)tong / n);
return 0;
}int lon_nhat(const int *a, size_t n)
{
int m = a[0];
for (size_t i = 1; i < n; ++i)
if (a[i] > m) m = a[i];
return m;
}
int nho_nhat(const int *a, size_t n)
{
int m = a[0];
for (size_t i = 1; i < n; ++i)
if (a[i] < m) m = a[i];
return m;
}
double trung_binh(const int *a, size_t n)
{
long long tong = 0;
for (size_t i = 0; i < n; ++i)
tong += a[i];
return (double)tong / n;
}
int main(void)
{
int a[100];
size_t n = doc_day(a, 100);
if (n == 0) {
fprintf(stderr, "Day rong\n");
return 1;
}
printf("%d %d %.2f\n",
lon_nhat(a, n), nho_nhat(a, n), trung_binh(a, n));
return 0;
}Cột phải dài hơn về số dòng. Đó là chuyện bình thường và không phải nhược điểm. Hãy đếm thứ khác: ở cột trái, muốn kiểm tra hàm tìm giá trị lớn nhất có đúng không, bạn phải chạy cả chương trình và gõ số vào bằng tay. Ở cột phải, bạn viết lon_nhat(mau, 5) với một mảng cố định và so kết quả ngay.
Tự làm thử
- Viết
int la_chan(int n)trả về 1 nếu n chẵn, 0 nếu lẻ. Dùng nó để đếm số chẵn trong dãy 1 tới 100. - Lấy một chương trình bạn đã viết ở Phần 2 và tách nó thành ít nhất ba hàm. Ghi lại số dòng của
maintrước và sau. - Viết
double chu_vi_hinh_tron(double r)vàdouble dien_tich_hinh_tron(double r), mỗi hàm đúng một dòng thân. - Cho danh sách tên hàm sau, sửa lại cho đúng nguyên tắc đặt tên:
x,tinh,data,process2,kiem_tra_va_in_ket_qua. - Chỉ ra hàm nào trong thư viện chuẩn là hàm thuần và hàm nào không:
strlen,printf,abs,rand,toupper. Giải thích từng cái.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Hàm là khối mã có tên, làm một việc, gọi lại được nhiều lần.
- Sáu lợi ích: không lặp mã, chia để trị, dễ kiểm thử, dễ bảo trì, trừu tượng hóa và tái sử dụng.
- Tên hàm nên là động từ, thân không quá năm mươi dòng, không quá bốn tới năm tham số.
- Hàm thuần chỉ phụ thuộc đối số và không đổi gì bên ngoài. Ưu tiên viết hàm thuần, dồn phần nhập xuất ra mép chương trình.
- Khi bạn phải viết chú thích giải thích một khối mã, đó là lúc khối đó nên thành hàm.