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

Định nghĩa hàm

Sau bài này bạn sẽ làm được

  • Viết định nghĩa hàm đầy đủ bốn thành phần
  • Giải thích quy tắc một định nghĩa và lỗi multiple definition
  • Dùng static để giới hạn hàm trong một file
  • Biết khi nào nên dùng static inline thay cho macro

Nguyên mẫu là lời hứa, định nghĩa là phần thực hiện lời hứa đó. Bài này đi hết bốn thành phần của một định nghĩa, quy tắc chỉ được định nghĩa một lần, và hai từ khóa hay đi kèm là static với inline.

#Bốn thành phần

dinh-nghia.c
double dien_tich_hinh_tron(double ban_kinh)
{
    return 3.14159265358979 * ban_kinh * ban_kinh;
}
Bốn thành phần của một định nghĩa hàm. Ba thành phần đầu ghép lại chính là nguyên mẫu.
Thành phầnTrong ví dụGhi chú
Kiểu trả vềdoubleGhi void nếu hàm không trả về gì
Tên hàmdien_tich_hinh_tronNên là động từ hoặc cụm danh từ mô tả kết quả
Danh sách tham số(double ban_kinh)Ghi (void) nếu không nhận tham số nào
Thân hàm{ ... }Luôn có ngoặc nhọn, kể cả khi chỉ một câu lệnh
int    cong(int a, int b)   { return a + b; }
void   chao(void)           { printf("Xin chao\n"); }
double nghich_dao(double x) { return 1.0 / x; }
char   chu_hoa(char c)      { return c >= 'a' && c <= 'z' ? c - 32 : c; }

#Kiểu trả về

Kiểu trả về quyết định chỗ đặt kết quả ở mức mã máy. Trên nền tảng x86 sáu tư bit theo quy ước System V, số nguyên và con trỏ trả về qua thanh ghi rax, còn số thực trả về qua xmm0. Đó là lý do trình biên dịch bắt buộc phải biết kiểu trả về trước khi sinh mã cho lời gọi.

terminal
# Xem mã hợp ngữ do trình biên dịch sinh ra
gcc -S -O1 -masm=intel cong.c -o -
cong:
        lea     eax, [rdi+rsi]     ; kết quả vào eax
        ret

Hàm void

void in_dong_ke(int n)
{
    if (n <= 0)
        return;             /* thoát sớm, không kèm giá trị */

    for (int i = 0; i < n; ++i)
        putchar('-');

    putchar('\n');
}

Câu lệnh return; trong hàm void là cách thoát sớm rất hữu ích. Nó cho phép bạn xử lý các trường hợp đặc biệt ngay đầu hàm rồi để phần thân chính không phải lồng thêm tầng if nào.

Lồng sâu vì gom hết vào một if
void xu_ly(int *a, size_t n)
{
    if (a != NULL) {
        if (n > 0) {
            for (size_t i = 0; i < n; ++i) {
                /* thân thật sự nằm ở tầng thứ tư */
            }
        }
    }
}
Thoát sớm, thân chính phẳng
void xu_ly(int *a, size_t n)
{
    if (a == NULL) return;
    if (n == 0)    return;

    for (size_t i = 0; i < n; ++i) {
        /* thân thật sự nằm ở tầng thứ hai */
    }
}

Trả về kiểu gì thì hợp lý

Hàm làm gìKiểu trả về nên chọnVì sao
Trả lời câu hỏi đúng saiint hoặc boolTừ C99 có <stdbool.h> cho true và false
Đếm số lượng phần tửsize_tKhông bao giờ âm, khớp với sizeof và chỉ số mảng
Trả về mã lỗiintQuy ước 0 là thành công, khác 0 là mã lỗi
Trả về vùng nhớ vừa cấpcon trỏNULL nghĩa là thất bại
Không có gì để trảvoidĐừng trả về int rồi luôn trả 0 cho có

#Quy tắc một định nghĩa

Quy tắc một định nghĩa
Trong toàn bộ chương trình sau khi liên kết, mỗi hàm chỉ được có đúng một thân. Khai báo thì lặp lại bao nhiêu lần cũng được, định nghĩa thì không.
a.c
int cong(int a, int b) { return a + b; }
b.c
int cong(int a, int b) { return a + b; }   /* cùng nội dung vẫn là lỗi */
terminal
gcc a.c b.c main.c -o app
/usr/bin/ld: b.o: in function `cong':
b.c:1: multiple definition of `cong'; a.o:a.c:1: first defined here

#Hàm static

Đặt static trước một định nghĩa hàm sẽ giới hạn hàm đó trong đúng file đang chứa nó. Các file khác không thấy được, không gọi được, và tên hàm không lọt ra bảng ký hiệu chung.

thongke.c
#include "thongke.h"

/* Hàm phụ trợ, chỉ dùng nội bộ trong file này. */
static int so_sanh(const void *x, const void *y)
{
    int a = *(const int *)x;
    int b = *(const int *)y;

    return (a > b) - (a < b);
}

/* Hàm công khai, có khai báo trong thongke.h. */
double trung_vi(int *a, size_t n)
{
    qsort(a, n, sizeof a[0], so_sanh);

    return n % 2 ? a[n / 2] : (a[n / 2 - 1] + a[n / 2]) / 2.0;
}
Lợi ích của staticGiải thích
Tránh trùng tênHai file cùng có hàm tên so_sanh mà không xung đột lúc liên kết
Nói rõ đâu là giao diệnNgười đọc thấy ngay hàm nào là API công khai, hàm nào là chi tiết bên trong
Trình biên dịch tối ưu mạnh hơnVì biết chắc không ai ngoài file này gọi, nó có thể nhúng thẳng hàm vào chỗ gọi hoặc xóa hàm nếu không ai dùng
Giảm kích thước file thực thiHàm static không dùng tới sẽ bị loại bỏ hoàn toàn
terminal
# Kiểm chứng: chữ T là ký hiệu công khai, chữ t là nội bộ
gcc -c thongke.c && nm thongke.o
0000000000000000 t so_sanh
0000000000000030 T trung_vi

#inline và khi nào dùng

Mỗi lời gọi hàm có chi phí: đẩy đối số, nhảy đi, lập khung, thu khung, nhảy về. Với hàm thân rất ngắn và bị gọi trong vòng lặp nóng, chi phí đó có thể lớn hơn cả phần việc thật. Từ khóa inline là lời gợi ý cho trình biên dịch chép thẳng thân hàm vào chỗ gọi.

static inline int lon_hon(int a, int b)
{
    return a > b ? a : b;
}
Macrostatic inline
Kiểm tra kiểuKhông cóCó đầy đủ
Đánh giá đối sốCó thể nhiều lần, sinh lỗi ngầmĐúng một lần
Gỡ lỗi được bằng gdbKhông
Nằm trong phạm vi khốiKhông, có hiệu lực tới cuối file
Tốc độ sau khi tối ưuNhanhNhanh tương đương
Macro: đối số bị tính hai lần
#define LON_HON(a, b) ((a) > (b) ? (a) : (b))

int i = 7;
int m = LON_HON(i++, 5);

/* Bung thành ((i++) > (5) ? (i++) : (5)).
   Điều kiện 7 > 5 đúng nên nhánh (i++) chạy tiếp, i tăng HAI lần thành 9,
   và m nhận 8 chứ không phải 7. */
static inline: an toàn và nhanh như nhau
static inline int lon_hon(int a, int b)
{
    return a > b ? a : b;
}

int i = 7;
int m = lon_hon(i++, 5);   /* i tăng đúng một lần thành 8, m nhận 7 */

Trường hợp static inline trong file tiêu đề là ngoại lệ hợp lệ duy nhất của quy tắc một định nghĩa: mỗi file nạp nó sẽ có một bản riêng, nội bộ, nên không xung đột lúc liên kết.

Tự làm thử

  1. Viết void in_bang_cuu_chuong(int n) in bảng nhân của n từ 1 tới 10, có thoát sớm khi n nằm ngoài khoảng hợp lệ.
  2. Tạo hai file cùng định nghĩa một hàm trùng tên để tự gây ra lỗi multiple definition, rồi sửa bằng static.
  3. Chạy nm lên file đối tượng của bạn, đối chiếu chữ T và chữ t với các hàm có và không có static.
  4. Viết macro BINH_PHUONG(x) rồi gọi nó với i++. Giải thích kết quả nhận được, sau đó viết lại bằng static inline.
  5. Dùng gcc -S để xem mã hợp ngữ của một hàm cộng đơn giản khi biên dịch với -O0 và với -O2. Đếm số lệnh của mỗi bản.

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

  • Định nghĩa hàm gồm kiểu trả về, tên, danh sách tham số và thân hàm.
  • Trong hàm void, câu lệnh return; đứng một mình dùng để thoát sớm và làm thân hàm phẳng hơn.
  • Mỗi hàm chỉ được định nghĩa đúng một lần trong cả chương trình. File tiêu đề chỉ nên chứa khai báo.
  • static trước hàm giới hạn nó trong một file, giúp tránh trùng tên và cho trình biên dịch tối ưu mạnh hơn.
  • static inline an toàn hơn macro về kiểu và về số lần đánh giá đối số, nhưng chỉ nên dùng khi đã đo được lợi ích.