Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 8.114 phút đọc

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ì

Hàm
Một khối mã có tên, thực hiện đúng một nhiệm vụ, nhận vào một số giá trị và thường trả về một giá trị. Gọi lại được bao nhiêu lần cũng được.

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.

dau-tien.c
#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 íchNghĩ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ơiSử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 đượcBạ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ọnMuố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ếtMọi thay đổi đều lan ra khắp nơi
Trừu tượng hóaNgười gọi chỉ cần biết hàm làm gì, không cần biết làm thế nàoNgườ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 sauMỗ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ữ để 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ốtTên xấuVì sao
tinh_tongtongHàm là hành động, biến mới là danh từ
la_nguyen_tonguyen_toTiền tố la_ báo ngay đây là câu hỏi đúng sai
doc_filefileKhông biết là đọc, ghi hay xóa
dem_tuxu_lyXử 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
Hàm mà kết quả chỉ phụ thuộc vào các đối số truyền vào, và chạy nó không làm thay đổi bất cứ thứ gì bên ngoài. Gọi mười lần với cùng đối số thì được đúng mười kết quả giống nhau.
/* 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ầnHàm có tác dụng phụ
Kiểm thửChỉ cần so đầu vào với đầu raPhải dựng trạng thái trước, dọn sau
Gỡ lỗiNhìn đối số là đoán được kết quảPhải truy xem ai đã đổi trạng thái
Đa luồngAn toàn tự nhiênCần khóa, dễ tranh chấp
Tối ưuTrình biên dịch được phép gọi ít lần hơnBắ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.

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;
}
Tách thành các hàm có tên
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ử

  1. 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.
  2. 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 main trước và sau.
  3. Viết double chu_vi_hinh_tron(double r) double dien_tich_hinh_tron(double r), mỗi hàm đúng một dòng thân.
  4. 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.
  5. 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.