Nhập chuỗi bằng fgets
Sau bài này bạn sẽ làm được
- Đọc chuỗi an toàn bằng fgets và hiểu ba giá trị trả về của nó
- Bỏ ký tự xuống dòng đúng cách bằng strcspn
- Viết được hàm nhập số nguyên an toàn bằng fgets kèm strtol
- Giải thích vì sao gets bị xóa hẳn khỏi chuẩn C11
Đây là bài quan trọng về mặt thực hành. Cách nhập liệu trình bày ở đây sẽ được dùng trong mọi bài tập và mọi dự án từ giờ tới hết khóa học. Nó dài dòng hơn scanf vài dòng, nhưng đổi lại là an toàn và có thể báo lỗi chính xác.
#Vì sao dùng fgets
Bài trước đã chỉ ra bốn vấn đề của scanf. Gốc rễ của cả bốn nằm ở một chỗ: scanf trộn lẫn hai việc vốn nên tách rời.
| Việc | scanf | fgets kèm phân tích |
|---|---|---|
| Lấy dữ liệu vào | Đọc bao nhiêu tùy theo định dạng, khó đoán | Luôn đọc đúng một dòng |
| Phân tích dữ liệu | Làm cùng lúc với việc đọc | Làm riêng, trên chuỗi đã nằm trong bộ nhớ |
| Khi dữ liệu sai | Bộ đệm ở trạng thái khó lường | Cả dòng đã tiêu thụ gọn, chỉ cần báo lỗi |
| Chuỗi có khoảng trắng | Cần tập ký tự | Tự nhiên |
| Giới hạn tràn bộ đệm | Phải nhớ ghi độ rộng | Là tham số bắt buộc |
#Cách dùng và giá trị trả về
char *fgets(char *s, int n, FILE *stream);Hàm đọc tối đa n - 1 ký tự vào s, hoặc dừng sớm nếu gặp ký tự xuống dòng. Sau đó nó tự thêm ký tự kết thúc chuỗi.
- Ký tự xuống dòng, nếu có, được giữ lại trong chuỗi. Đây là điểm quan trọng nhất cần nhớ.
- Trả về con trỏ tới
snếu đọc được, hoặc con trỏ rỗng nếu gặp cuối luồng hay lỗi mà chưa đọc được ký tự nào. - Tham số
nlà kích thước bộ đệm, tính cả chỗ cho ký tự kết thúc. Nên luôn truyềnsizeof buf.
#include <stdio.h>
#include <string.h>
int main(void)
{
char ten[64];
printf("Nhập họ tên: ");
if (!fgets(ten, sizeof ten, stdin)) {
fprintf(stderr, "Không đọc được dữ liệu\n");
return 1;
}
ten[strcspn(ten, "\n")] = '\0'; /* bỏ ký tự xuống dòng */
printf("Xin chào, %s (dài %zu ký tự)\n", ten, strlen(ten));
return 0;
}Nhập họ tên: Nguyễn Văn A
Xin chào, Nguyễn Văn A (dài 17 ký tự)
#Bỏ ký tự xuống dòng
Việc fgets giữ lại ký tự xuống dòng gây ra một lỗi rất hay gặp: so sánh chuỗi không bao giờ đúng.
char lenh[32];
fgets(lenh, sizeof lenh, stdin);
if (strcmp(lenh, "thoat") == 0) { /* KHÔNG BAO GIỜ đúng */
return 0;
}
/* Vì lenh thật ra là "thoat\n" chứ không phải "thoat" */char lenh[32];
fgets(lenh, sizeof lenh, stdin);
lenh[strcspn(lenh, "\n")] = '\0'; /* bỏ ký tự xuống dòng trước */
if (strcmp(lenh, "thoat") == 0) { /* giờ mới đúng */
return 0;
}#Xử lý dòng quá dài
Nếu người dùng nhập dòng dài hơn bộ đệm, fgets chỉ lấy phần đầu và để phần còn lại nằm trong bộ đệm. Lần đọc tiếp theo sẽ nhận được phần thừa đó, gây ra hành vi rất khó hiểu.
#include <stdio.h>
#include <string.h>
/* Trả về độ dài dòng nếu thành công.
Trả về -1 nếu hết dữ liệu.
Trả về -2 nếu dòng bị cắt vì quá dài. */
int nhap_dong(char *buf, size_t n)
{
if (!fgets(buf, (int)n, stdin)) {
return -1;
}
size_t len = strcspn(buf, "\n");
if (buf[len] != '\n') { /* không tìm thấy xuống dòng: dòng bị cắt */
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* nuốt hết phần thừa để lần đọc sau sạch sẽ */
}
buf[len] = '\0';
return -2;
}
buf[len] = '\0';
return (int)len;
}#Nhập số an toàn
Đây là hàm bạn nên chép vào một file tiện ích và dùng lại suốt khóa học. Nó xử lý đủ mọi trường hợp mà scanf bỏ sót.
Đọc cả dòng
Dùng
fgetsnên luôn tiêu thụ đúng một dòng, dù dữ liệu đúng hay sai.Chuyển đổi bằng strtol
Khác với
atoi, hàmstrtolcho biết nó dừng ở đâu và có tràn hay không.Kiểm tra ba loại lỗi
Không có chữ số nào, có ký tự rác phía sau, và giá trị vượt miền cho phép.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* Trả về 0 nếu thành công, số âm nếu lỗi. */
int nhap_so_nguyen(const char *nhac, long *out)
{
char dong[128];
printf("%s", nhac);
if (!fgets(dong, sizeof dong, stdin)) {
return -1; /* hết dữ liệu */
}
dong[strcspn(dong, "\n")] = '\0';
errno = 0;
char *end;
long v = strtol(dong, &end, 10);
if (end == dong) {
return -2; /* không có chữ số nào */
}
while (*end == ' ' || *end == '\t') {
++end; /* bỏ qua khoảng trắng cuối */
}
if (*end != '\0') {
return -3; /* còn ký tự rác, ví dụ 12abc */
}
if (errno == ERANGE) {
return -4; /* vượt miền của long */
}
*out = v;
return 0;
}
int main(void)
{
long tuoi;
while (nhap_so_nguyen("Nhập tuổi: ", &tuoi) != 0) {
printf("Dữ liệu không hợp lệ, mời nhập lại.\n");
}
if (tuoi < 0 || tuoi > 150) {
printf("Tuổi phải nằm trong khoảng 0 đến 150.\n");
return 1;
}
printf("Tuổi của bạn là %ld\n", tuoi);
return 0;
}#Vì sao gets bị xóa
char *gets(char *s); /* ĐÃ BỊ XÓA khỏi chuẩn C11 */Hàm này đọc một dòng vào bộ đệm, nhưng không nhận tham số kích thước. Nghĩa là không có cách nào để nó biết bộ đệm rộng bao nhiêu, và không có cách nào dùng nó an toàn.
| Cần nhập | Cách đúng |
|---|---|
| Một dòng văn bản | fgets rồi bỏ ký tự xuống dòng bằng strcspn |
| Số nguyên | fgets rồi strtol, kiểm tra đủ ba loại lỗi |
| Số thực | fgets rồi strtod |
| Một từ | fgets rồi sscanf với %63s |
| Một ký tự | fgets rồi lấy phần tử đầu tiên của chuỗi |
| Nhiều trường trên một dòng | fgets rồi sscanf |
Tự làm thử
- Viết hàm
nhap_dongđầy đủ như ở trên, rồi kiểm chứng bằng cách nhập một dòng dài hơn bộ đệm. - Chép hàm
nhap_so_nguyenvào file riêng, rồi kiểm thử với các đầu vào:42,abc,12abc, chuỗi rỗng, và99999999999999999999. - Viết hàm
nhap_so_thuctương tự dùngstrtod. - Viết hàm nhập số nguyên có kiểm tra miền giá trị, nhận thêm hai tham số cận dưới và cận trên, tự hỏi lại khi ngoài miền.
- Chứng minh vấn đề của
fgetskhi dòng quá dài: dùng bộ đệm 8 byte, nhập một dòng 30 ký tự, gọifgetsba lần liên tiếp và quan sát.
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
- fgets luôn tiêu thụ đúng một dòng, nên tách được việc đọc khỏi việc phân tích.
- Nó giữ lại ký tự xuống dòng, phải bỏ bằng
s[strcspn(s, "\n")] = 0. - Dòng dài hơn bộ đệm sẽ để phần thừa lại, phải chủ động nuốt hết.
- Nhập số an toàn là
fgetskèmstrtol, kiểm tra đủ ba lỗi: không có chữ số, còn ký tự rác, và tràn miền. - Không dùng
atoivì nó không báo lỗi được, và không bao giờ dùnggetsvì nó đã bị xóa khỏi chuẩn.