Khai báo hàm
Sau bài này bạn sẽ làm được
- Viết nguyên mẫu hàm đúng cú pháp
- Giải thích vì sao thiếu nguyên mẫu là lỗi từ C99 trở đi
- Phân biệt khai báo với định nghĩa
- Hiểu vì sao int f() khác int f(void) trước C23
Trình biên dịch C đọc file đúng một lượt, từ trên xuống dưới, và không bao giờ quay lại. Nguyên mẫu hàm tồn tại chỉ vì lý do đó. Hiểu được câu này thì mọi quy tắc còn lại trong bài đều tự suy ra được.
#Cú pháp nguyên mẫu
int cong(int a, int b); /* dạng đầy đủ, có tên tham số */
int cong(int, int); /* tên tham số được phép lược bỏ */
void chao(void); /* không nhận gì, không trả về gì */
double dien_tich(double r);Hai dòng đầu tương đương hoàn toàn với trình biên dịch. Tên tham số trong nguyên mẫu chỉ có tác dụng với người đọc, nhưng đó là một tác dụng đáng giá: void ve(int, int, int, int) không nói được gì, còn void ve(int x, int y, int rong, int cao) thì nói đủ.
| Thành phần | Ví dụ | Bắt buộc |
|---|---|---|
| Kiểu trả về | int | Có. Bỏ trống là lỗi từ C99 |
| Tên hàm | cong | Có |
| Danh sách kiểu tham số | (int, int) | Có. Không nhận gì thì ghi (void) |
| Tên tham số | a, b | Không, nhưng nên có |
| Dấu chấm phẩy | ; | Có. Thiếu là biến khai báo thành định nghĩa |
#Vì sao trình biên dịch cần nó
Xem chương trình sau. Nó trông hoàn toàn hợp lý với mắt người, vì add có được định nghĩa ở phía dưới.
#include <stdio.h>
int main(void)
{
printf("%d\n", add(3, 4));
return 0;
}
int add(int a, int b)
{
return a + b;
}thieu-prototype.c:5:20: error: implicit declaration of function 'add' [-Wimplicit-function-declaration]
Tới dòng 5, trình biên dịch chưa từng nhìn thấy chữ add. Nó không có cách nào biết add nhận mấy tham số, kiểu gì, trả về gì. Mà nó cần biết cả ba thứ đó ngay lập tức để làm ba việc:
- Kiểm tra số lượng và kiểu đối số. Gọi
add(3)hayadd(1, 2, 3)phải bị bắt ngay tại chỗ. - Sinh mã ép kiểu cần thiết. Nếu bạn gọi
add(3.7, 4.2)thì phải sinh lệnh chuyển số thực thành số nguyên trước khi truyền. - Biết đọc kết quả trả về ở đâu và rộng bao nhiêu. Hàm trả về
intvà hàm trả vềdoubleđặt kết quả ở hai thanh ghi khác nhau hoàn toàn.
Khai báo khác định nghĩa ở chỗ nào
| Khai báo | Định nghĩa | |
|---|---|---|
| Có thân hàm | Không | Có |
| Kết thúc bằng | Dấu chấm phẩy | Dấu ngoặc nhọn đóng |
| Được lặp lại | Bao nhiêu lần cũng được | Đúng một lần trong cả chương trình |
| Sinh ra mã máy | Không | Có |
| Đặt ở đâu | Thường trong file .h | Trong file .c |
#Ba cách đặt nguyên mẫu
Cách 1: nguyên mẫu ở đầu file, chuẩn mực nhất
#include <stdio.h>
/* Toàn bộ giao diện của file nằm gọn ở đây, đọc một cái là nắm hết. */
int cong(int a, int b);
int tru(int a, int b);
void in_ket_qua(const char *nhan, int gia_tri);
int main(void)
{
in_ket_qua("tong", cong(7, 3));
in_ket_qua("hieu", tru(7, 3));
return 0;
}
int cong(int a, int b) { return a + b; }
int tru(int a, int b) { return a - b; }
void in_ket_qua(const char *nhan, int gia_tri)
{
printf("%s = %d\n", nhan, gia_tri);
}Ưu điểm lớn nhất là thứ tự định nghĩa bên dưới không còn quan trọng. Hàm nào gọi hàm nào cũng được, kể cả gọi vòng: a gọi b và b gọi a. Không có nguyên mẫu thì trường hợp gọi vòng là không viết được.
Cách 2: định nghĩa hàm trước khi dùng
#include <stdio.h>
int cong(int a, int b) /* định nghĩa trước, không cần nguyên mẫu riêng */
{
return a + b;
}
int main(void)
{
printf("%d\n", cong(3, 4));
return 0;
}Cách này hợp với file rất ngắn. Nhược điểm là main bị đẩy xuống cuối, người đọc phải cuộn qua hết mọi chi tiết trước khi thấy được ý đồ tổng thể, và gọi vòng thì bó tay.
Cách 3: nguyên mẫu trong file tiêu đề
#ifndef MATHLIB_H
#define MATHLIB_H
int la_nguyen_to(long n);
long uoc_chung_lon_nhat(long a, long b);
long boi_chung_nho_nhat(long a, long b);
#endif#include "mathlib.h"
int la_nguyen_to(long n)
{
if (n < 2) return 0;
for (long i = 2; i * i <= n; ++i)
if (n % i == 0) return 0;
return 1;
}
/* ... các hàm còn lại ... */#include <stdio.h>
#include "mathlib.h"
int main(void)
{
printf("%d\n", la_nguyen_to(97));
return 0;
}1
Đây là cách của mọi dự án thật. File .h là bản hợp đồng, file .c là phần thực hiện hợp đồng đó. Cả hai bên cùng #include file tiêu đề nên nếu bạn sửa nguyên mẫu mà quên sửa định nghĩa, trình biên dịch báo lỗi ngay chứ không đợi tới lúc chạy. Chương 20 nói kỹ về mô hình nhiều file.
#int f() khác int f(void)
Đây là một trong những cái bẫy lâu đời nhất của C và vẫn còn bẫy người học tới tận hôm nay.
int f(); /* nghĩa là: tôi chưa cho bạn biết f nhận gì */
int main(void)
{
f(1, 2, 3); /* trình biên dịch không có căn cứ để báo lỗi */
f("xin chao"); /* cũng qua luôn */
return 0;
}int f(void); /* nghĩa là: f không nhận tham số nào cả */
int main(void)
{
f(1, 2, 3); /* error: too many arguments to function 'f' */
return 0;
}Vì lý do này, hãy tập thói quen viết int main(void) chứ không phải int main(). Bạn sẽ thấy suốt giáo trình này dùng dạng có void.
#Bốn cờ cảnh báo nên bật
| Cờ | Bắt lỗi gì |
|---|---|
| -Wmissing-prototypes | Hàm không phải static mà không có nguyên mẫu khai báo trước |
| -Wstrict-prototypes | Khai báo kiểu cũ dạng ngoặc rỗng như int f() |
| -Wmissing-declarations | Hàm được định nghĩa nhưng chưa từng khai báo ở file tiêu đề nào |
| -Wold-style-definition | Định nghĩa hàm theo cú pháp K&R từ thập niên 1970 |
Tự làm thử
- Viết chương trình gọi một hàm trước khi khai báo nó, đọc thông báo lỗi thật của trình biên dịch trên máy bạn và chép lại nguyên văn.
- Khai báo
int f();rồi định nghĩaint f(int a). Thử gọif(),f(1)vàf(1, 2). Cái nào biên dịch được, cái nào chạy sai. - Tách một chương trình sẵn có thành ba file:
tienich.h,tienich.cvàmain.c. Biên dịch bằng một dòng lệnh duy nhất. - Cố tình gõ sai tên hàm ở phần định nghĩa để tạo ra lỗi
undefined reference, rồi tự tìm ra nó chỉ bằng cách đọc thông báo. - Viết hai hàm gọi lẫn nhau:
la_chan(n)gọila_le(n - 1)và ngược lại. Giải thích vì sao bắt buộc phải có nguyên mẫu.
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
- Trình biên dịch đọc file một lượt từ trên xuống, nên phải biết hàm trước khi thấy lời gọi.
- Nguyên mẫu cho biết tên, kiểu trả về và kiểu từng tham số, kết thúc bằng dấu chấm phẩy và không có thân.
- Ba cách đặt: đầu file, định nghĩa trước khi dùng, hoặc trong file tiêu đề. Dự án thật luôn dùng cách thứ ba.
int f()không có nghĩa là không tham số, mà là chưa nói gì về tham số. Luôn viết(void).- Bật
-Wmissing-prototypesvà-Wstrict-prototypesđể trình biên dịch bắt lỗi thay bạn.