Giá trị trả về
Sau bài này bạn sẽ làm được
- Giải thích vì sao thiếu return trong hàm non-void là hành vi không xác định
- Trả về nhiều giá trị bằng tham số ra hoặc bằng struct
- Chọn đúng quy ước báo lỗi cho từng loại hàm
- Luôn kiểm tra giá trị trả về của hàm có thể thất bại
Một hàm C chỉ trả về được đúng một giá trị. Nghe thì hạn chế, nhưng thực tế lập trình luôn cần trả về nhiều thứ cùng lúc: kết quả tính toán, và thông tin cho biết việc tính toán đó có thành công hay không. Bài này trình bày cách C giải quyết mâu thuẫn đó.
#return làm hai việc
Câu lệnh return vừa đặt giá trị trả về, vừa kết thúc hàm ngay lập tức. Mọi dòng phía sau nó trong cùng nhánh sẽ không bao giờ chạy.
int lon_hon(int a, int b)
{
if (a > b)
return a; /* thoát luôn tại đây */
return b;
}Một hàm được phép có nhiều câu lệnh return. Có hai trường phái về chuyện này. Trường phái một lối ra duy nhất cho rằng dễ theo dõi hơn, đặc biệt khi hàm phải dọn tài nguyên. Trường phái thoát sớm cho rằng xử lý xong trường hợp đặc biệt rồi thoát ngay sẽ làm thân hàm phẳng và dễ đọc hơn nhiều.
int phan_loai(int diem)
{
int kq;
if (diem < 0 || diem > 100) {
kq = -1;
} else {
if (diem >= 80) {
kq = 3;
} else {
if (diem >= 50) {
kq = 2;
} else {
kq = 1;
}
}
}
return kq;
}int phan_loai(int diem)
{
if (diem < 0 || diem > 100) return -1;
if (diem >= 80) return 3;
if (diem >= 50) return 2;
return 1;
}#Thiếu return là hành vi không xác định
int f(int x)
{
if (x > 0)
return 1;
/* x <= 0 chạy tới đây rồi rơi khỏi hàm mà không có return */
}Đây không phải lỗi cú pháp. Trình biên dịch cho qua, chương trình chạy, và f(-5) trả về bất cứ giá trị nào đang nằm sẵn trong thanh ghi rax lúc đó. Kết quả có thể trông hợp lý trong lần chạy đầu và đổi hoàn toàn sau khi bạn sửa một dòng ở chỗ khác.
bai.c:6:1: warning: control reaches end of non-void function [-Wreturn-type]
Hai ngoại lệ hợp pháp
- Hàm
voidđược phép kết thúc mà không córeturnnào. - Hàm
mainđược phép rơi khỏi thân, chuẩn C99 quy định khi đó nó trả về 0. Nhưng viết rõreturn 0;vẫn tốt hơn.
#Ba cách trả về nhiều giá trị
Bài toán mẫu: hàm chia lấy nguyên và lấy dư, cần trả về cả thương, cả số dư, và cả thông tin mẫu số có bằng 0 hay không. Ba giá trị, một khe trả về.
Cách 1: tham số ra, phổ biến nhất
/* Trả về 0 khi thành công, khác 0 khi thất bại.
Kết quả thật đi ra qua hai con trỏ. */
int chia(int a, int b, int *thuong, int *du)
{
if (b == 0)
return -1;
*thuong = a / b;
*du = a % b;
return 0;
}
int t, d;
if (chia(17, 5, &t, &d) == 0)
printf("%d du %d\n", t, d); /* 3 du 2 */
else
printf("Khong chia duoc cho 0\n");Đây là cách của thư viện chuẩn và của gần như mọi thư viện hệ thống. Bạn đã gặp nó ở scanf từ Bài 3.3: hàm trả về số mục đọc được, còn dữ liệu thật đi ra qua các con trỏ bạn truyền vào.
Cách 2: trả về struct
typedef struct {
int thuong;
int du;
int ok; /* 1 nếu hợp lệ, 0 nếu mẫu số bằng 0 */
} KetQua;
KetQua chia2(int a, int b)
{
KetQua k = { 0, 0, 0 };
if (b == 0)
return k;
k.thuong = a / b;
k.du = a % b;
k.ok = 1;
return k;
}
KetQua k = chia2(17, 5);
if (k.ok)
printf("%d du %d\n", k.thuong, k.du);| Tham số ra | Trả về struct | |
|---|---|---|
| Chỗ gọi đọc dễ không | Phải nhìn nguyên mẫu mới biết cái nào là ra | Rất rõ, mọi thứ nằm trong kết quả |
| Cần con trỏ | Có, người gọi phải viết dấu và | Không |
| Chi phí sao chép | Không đáng kể | Sao chép cả struct, đáng kể nếu struct lớn |
| Quên khởi tạo biến nhận | Có thể quên, giá trị là rác | Không thể quên |
| Hợp với | Kết quả lớn, hoặc ghi vào bộ đệm có sẵn | Kết quả nhỏ, vài trường |
Với struct nhỏ, quy ước gọi hàm đặt cả struct vào một hoặc hai thanh ghi nên chi phí gần như bằng không. Ngưỡng thường thấy là mười sáu byte. Lớn hơn ngưỡng đó, trình biên dịch chuyển sang chép qua bộ nhớ.
Cách 3: biến toàn cục, nên tránh
int g_thuong, g_du; /* tránh cách này */
int chia3(int a, int b)
{
if (b == 0) return -1;
g_thuong = a / b;
g_du = a % b;
return 0;
}Cách này không an toàn khi có nhiều luồng, không cho phép hai lời gọi lồng nhau, và biến kết quả có thể bị bất cứ đoạn mã nào ghi đè. Bài 8.8 nói rõ vì sao biến toàn cục là lựa chọn cuối cùng.
#Ba quy ước báo lỗi
| Quy ước | Ví dụ trong thư viện chuẩn | Cách kiểm tra |
|---|---|---|
| 0 là thành công, khác 0 là mã lỗi | fclose, remove, pthread_create | if (ham() != 0) { xu ly loi } |
| Trả về con trỏ, NULL là lỗi | malloc, fopen, strchr | if ((p = ham()) == NULL) { xu ly loi } |
| Trả về giá trị, dùng errno để phân biệt | strtol, fread, printf | errno = 0; v = strtol(...); if (errno) { ... } |
Ví dụ đầy đủ với errno
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *s = "12345678901234567890";
char *het;
errno = 0; /* bắt buộc: xóa trạng thái cũ */
long v = strtol(s, &het, 10);
if (het == s) {
fprintf(stderr, "Khong co chu so nao\n");
} else if (errno == ERANGE) {
fprintf(stderr, "So qua lon, bi kep ve %ld\n", v);
} else if (*het != '\0') {
fprintf(stderr, "Con thua ky tu: %s\n", het);
} else {
printf("Doc duoc %ld\n", v);
}
return 0;
}Ba trường hợp lỗi khác nhau được phân biệt rõ ràng, và tất cả đều lộ ra qua đúng một lần gọi. Dòng errno = 0; trước lời gọi là bắt buộc, vì thư viện chuẩn chỉ đặt errno khi có lỗi chứ không bao giờ xóa nó về 0.
#Luôn kiểm tra giá trị trả về
FILE *f = fopen("khong-ton-tai.txt", "r");
fgets(dong, sizeof dong, f); /* f là NULL, chương trình sập tại đây */
int n;
scanf("%d", &n);
printf("%d\n", n * 2); /* nếu người dùng gõ chữ, n là rác */
int *a = malloc(1000000000);
a[0] = 1; /* nếu cấp phát hỏng, a là NULL */FILE *f = fopen("du-lieu.txt", "r");
if (f == NULL) {
perror("du-lieu.txt"); /* in kèm lý do lấy từ errno */
return 1;
}
int n;
if (scanf("%d", &n) != 1) {
fprintf(stderr, "Can nhap mot so nguyen\n");
return 1;
}
int *a = malloc(1000000 * sizeof *a);
if (a == NULL) {
fprintf(stderr, "Khong du bo nho\n");
return 1;
}| Hàm | Trả về gì khi lỗi | Kiểm tra thế nào |
|---|---|---|
| malloc, calloc, realloc | NULL | if (p == NULL) |
| fopen | NULL | if (f == NULL) |
| scanf | Số mục đọc được, hoặc EOF | if (scanf(...) != so_muc_mong_doi) |
| fgets | NULL khi hết file hoặc lỗi | if (fgets(...) == NULL) |
| fread, fwrite | Số phần tử thật sự xử lý | if (fread(...) != n) |
| fclose | EOF khi lỗi | if (fclose(f) == EOF) |
| printf | Số ký tự in ra, âm khi lỗi | Thường bỏ qua được |
Nếu bạn cố tình muốn bỏ qua kết quả, hãy nói rõ điều đó bằng cách ép kiểu về void. Người đọc sau sẽ biết đây là quyết định có ý thức chứ không phải sơ suất.
(void)printf("chi la thong bao\n");Tự làm thử
- Viết hàm thiếu
returnở một nhánh, biên dịch với-Wall, rồi chạy nhiều lần xem giá trị trả về có ổn định không. - Cài
chiatheo cả ba cách trong bài, viếtmaingọi cả ba và so sánh mã ở chỗ gọi. - Viết
int an_toan_cong(int a, int b, int *kq)phát hiện tràn số trước khi cộng, trả về khác 0 nếu sẽ tràn. - Viết hàm đọc số nguyên từ chuỗi dùng
strtol, xử lý đủ ba trường hợp lỗi như ví dụ trong bài. - Rà lại một chương trình cũ của bạn, tìm mọi lời gọi
scanf,malloc,fopenchưa kiểm tra kết quả và bổ sung.
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
returnvừa đặt giá trị trả về vừa kết thúc hàm ngay lập tức.- Hàm non-void mà rơi khỏi thân không có
returnlà hành vi không xác định. Bật-Werror=return-type. - Ba cách trả về nhiều giá trị: tham số ra, trả về struct, và biến toàn cục. Hai cách đầu là lựa chọn thật.
- Ba quy ước báo lỗi: 0 là thành công,
NULLlà lỗi với hàm trả về con trỏ, và dùngerrnokhi mọi giá trị trả về đều hợp lệ. - Luôn kiểm tra kết quả của
malloc,fopen,scanfvàfread. Nếu cố tình bỏ qua thì ép kiểu(void)cho rõ ý.